From 78ae8f20060555df0f06b789515118092d87a732 Mon Sep 17 00:00:00 2001 From: ali <117142933+muhammad-ali-e@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:00:02 +0530 Subject: [PATCH 01/31] Handling grpc.StatusCode.NOT_FOUND exception (#439) * Handling grpc.StatusCode.NOT_FOUND exception seperately from other codes * minor changes --- .../flags/src/unstract/flags/client/evaluation.py | 13 +++++++++---- unstract/flags/src/unstract/flags/feature_flag.py | 3 +-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/unstract/flags/src/unstract/flags/client/evaluation.py b/unstract/flags/src/unstract/flags/client/evaluation.py index fa5347289..3a4b2b785 100644 --- a/unstract/flags/src/unstract/flags/client/evaluation.py +++ b/unstract/flags/src/unstract/flags/client/evaluation.py @@ -51,8 +51,13 @@ def boolean_evaluate_feature_flag( response = self.stub.Boolean(request) return bool(response.enabled) except grpc.RpcError as e: - logger.warning( - f"Error evaluating feature flag '{flag_key}' for {namespace_key}" - f": {str(e)}" - ) + if e.code() == grpc.StatusCode.NOT_FOUND: + logger.warning( + f"Flag key {flag_key} not found in namespace {namespace_key}." + ) + else: + logger.warning( + f"Error evaluating feature flag {flag_key} for {namespace_key}" + f" : {str(e)}" + ) return False diff --git a/unstract/flags/src/unstract/flags/feature_flag.py b/unstract/flags/src/unstract/flags/feature_flag.py index 9133ba826..3094c00ca 100644 --- a/unstract/flags/src/unstract/flags/feature_flag.py +++ b/unstract/flags/src/unstract/flags/feature_flag.py @@ -38,6 +38,5 @@ def check_feature_flag_status( context=context, ) return bool(response) # Wrap the response in a boolean check - except Exception as e: - logger.warning(f"Error evaluating feature flag '{flag_key}': {str(e)}") + except Exception: return False From a8905f833d55476b4cd95024f05e88434ea5a6a3 Mon Sep 17 00:00:00 2001 From: Tahier Hussain <89440263+tahierhussain@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:25:37 +0530 Subject: [PATCH 02/31] FEAT: Refactored Existing Prompt Studio Component to Support Simple Prompt Studio (#437) * Refactored the CombinedOutput component to support both JSON and Table view * Refactored the ToolsMain component to support SPS * Refactored the components in order to be re-used for SPS * Removed duplicated CSS class * Implemented a separate function to return the /prompt URL --- .../combined-output/CombinedOutput.jsx | 48 ++-- .../custom-tools/combined-output/JsonView.jsx | 34 +++ .../document-manager/DocumentManager.jsx | 195 ++++++++----- .../document-parser/DocumentParser.jsx | 26 +- .../custom-tools/prompt-card/PrompDnd.jsx | 4 +- .../custom-tools/prompt-card/PromptCard.jsx | 88 ++++-- .../prompt-card/PromptCardItems.jsx | 261 +++++++++--------- .../custom-tools/tools-main/ToolsMain.jsx | 85 ++---- .../tools-main/ToolsMainActionBtns.jsx | 68 +++++ .../helpers/custom-tools/CustomToolsHelper.js | 1 + frontend/src/helpers/GetStaticData.js | 12 + frontend/src/index.css | 22 ++ frontend/src/routes/Router.jsx | 41 ++- frontend/src/setupProxy.js | 7 + frontend/src/store/custom-tool-store.js | 1 + 15 files changed, 585 insertions(+), 308 deletions(-) create mode 100644 frontend/src/components/custom-tools/combined-output/JsonView.jsx create mode 100644 frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx diff --git a/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx b/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx index 507bf6704..4b0cb5281 100644 --- a/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx +++ b/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx @@ -1,4 +1,3 @@ -import Prism from "prismjs"; import "prismjs/components/prism-json"; import "prismjs/plugins/line-numbers/prism-line-numbers.css"; import "prismjs/plugins/line-numbers/prism-line-numbers.js"; @@ -17,7 +16,18 @@ import { useSessionStore } from "../../../store/session-store"; import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader"; import "./CombinedOutput.css"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; - +import { JsonView } from "./JsonView"; + +let TableView; +let promptOutputApiSps; +try { + TableView = + require("../../../plugins/simple-prompt-studio/TableView").TableView; + promptOutputApiSps = + require("../../../plugins/simple-prompt-studio/helper").promptOutputApiSps; +} catch { + // The component will remain null of it is not available +} function CombinedOutput({ docId, setFilledFields }) { const [combinedOutput, setCombinedOutput] = useState({}); const [isOutputLoading, setIsOutputLoading] = useState(false); @@ -26,6 +36,7 @@ function CombinedOutput({ docId, setFilledFields }) { defaultLlmProfile, singlePassExtractMode, isSinglePassExtractLoading, + isSimplePromptStudio, } = useCustomToolStore(); const { sessionDetails } = useSessionStore(); const { setAlertDetails } = useAlertStore(); @@ -91,14 +102,14 @@ function CombinedOutput({ docId, setFilledFields }) { }); }, [docId, singlePassExtractMode, isSinglePassExtractLoading]); - useEffect(() => { - Prism.highlightAll(); - }, [combinedOutput]); - const handleOutputApiRequest = async () => { + let url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt-output/?tool_id=${details?.tool_id}&document_manager=${docId}&is_single_pass_extract=${singlePassExtractMode}`; + if (isSimplePromptStudio) { + url = promptOutputApiSps(details?.tool_id, null, docId); + } const requestOptions = { method: "GET", - url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt-output/?tool_id=${details?.tool_id}&document_manager=${docId}&is_single_pass_extract=${singlePassExtractMode}`, + url, headers: { "X-CSRFToken": sessionDetails?.csrfToken, }, @@ -115,24 +126,11 @@ function CombinedOutput({ docId, setFilledFields }) { return ; } - return ( -
-
-
-
-
-
- {combinedOutput && ( -
-            
-              {JSON.stringify(combinedOutput, null, 2)}
-            
-          
- )} -
-
-
- ); + if (isSimplePromptStudio && TableView) { + return ; + } + + return ; } CombinedOutput.propTypes = { diff --git a/frontend/src/components/custom-tools/combined-output/JsonView.jsx b/frontend/src/components/custom-tools/combined-output/JsonView.jsx new file mode 100644 index 000000000..4ad2fa7a1 --- /dev/null +++ b/frontend/src/components/custom-tools/combined-output/JsonView.jsx @@ -0,0 +1,34 @@ +import PropTypes from "prop-types"; +import Prism from "prismjs"; +import { useEffect } from "react"; + +function JsonView({ combinedOutput }) { + useEffect(() => { + Prism.highlightAll(); + }, [combinedOutput]); + + return ( +
+
+
+
+
+
+ {combinedOutput && ( +
+            
+              {JSON.stringify(combinedOutput, null, 2)}
+            
+          
+ )} +
+
+
+ ); +} + +JsonView.propTypes = { + combinedOutput: PropTypes.object.isRequired, +}; + +export { JsonView }; diff --git a/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx b/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx index 59e32d40f..249a34edd 100644 --- a/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx +++ b/frontend/src/components/custom-tools/document-manager/DocumentManager.jsx @@ -1,4 +1,9 @@ -import { LeftOutlined, RightOutlined } from "@ant-design/icons"; +import { + FilePdfOutlined, + FileTextOutlined, + LeftOutlined, + RightOutlined, +} from "@ant-design/icons"; import "@react-pdf-viewer/core/lib/styles/index.css"; import "@react-pdf-viewer/default-layout/lib/styles/index.css"; import "@react-pdf-viewer/page-navigation/lib/styles/index.css"; @@ -20,7 +25,7 @@ import usePostHogEvents from "../../../hooks/usePostHogEvents"; const items = [ { key: "1", - label: "Doc View", + label: "PDF View", }, { key: "2", @@ -33,6 +38,7 @@ const viewTypes = { extract: "EXTRACT", }; +// Import components for the summarize feature let SummarizeView = null; try { SummarizeView = @@ -49,6 +55,15 @@ try { // The component will remain null of it is not available } +// Import component for the simple prompt studio feature +let getDocumentsSps; +try { + getDocumentsSps = + require("../../../plugins/simple-prompt-studio/simple-prompt-studio-api-service").getDocumentsSps; +} catch { + // The component will remain null of it is not available +} + function DocumentManager({ generateIndex, handleUpdateTool, handleDocChange }) { const [openManageDocsModal, setOpenManageDocsModal] = useState(false); const [page, setPage] = useState(1); @@ -69,11 +84,33 @@ function DocumentManager({ generateIndex, handleUpdateTool, handleDocChange }) { details, indexDocs, isSinglePassExtractLoading, + isSimplePromptStudio, } = useCustomToolStore(); const { sessionDetails } = useSessionStore(); const axiosPrivate = useAxiosPrivate(); const { setPostHogCustomEvent } = usePostHogEvents(); + useEffect(() => { + if (isSimplePromptStudio) { + items[0] = { + key: "1", + label: ( + + + + ), + }; + items[1] = { + key: "2", + label: ( + + + + ), + }; + } + }, []); + useEffect(() => { setFileUrl(""); setExtractTxt(""); @@ -127,36 +164,56 @@ function DocumentManager({ generateIndex, handleUpdateTool, handleDocChange }) { return; } - const requestOptions = { - method: "GET", - url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/file/${details?.tool_id}?document_id=${selectedDoc?.document_id}&view_type=${viewType}`, - }; + if (isSimplePromptStudio && getDocumentsSps) { + handleGetDocumentsReq(getDocumentsSps, viewType); + } else { + handleGetDocumentsReq(getDocuments, viewType); + } + }; - handleLoadingStateUpdate(viewType, true); - axiosPrivate(requestOptions) + const handleGetDocumentsReq = (getDocsFunc, viewType) => { + getDocsFunc(viewType) .then((res) => { - const data = res?.data?.data; - if (viewType === viewTypes.original) { - const base64String = data || ""; - const blob = base64toBlob(base64String); - setFileUrl(URL.createObjectURL(blob)); - return; - } - - if (viewType === viewTypes?.extract) { - setExtractTxt(data); - } + const data = res?.data?.data || ""; + processGetDocsResponse(data, viewType); }) .catch((err) => { - if (err?.response?.status === 404) { - setErrorMessage(viewType); - } + handleGetDocsError(err, viewType); }) .finally(() => { handleLoadingStateUpdate(viewType, false); }); }; + const getDocuments = async (viewType) => { + const requestOptions = { + method: "GET", + url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/file/${details?.tool_id}?document_id=${selectedDoc?.document_id}&view_type=${viewType}`, + }; + + return axiosPrivate(requestOptions) + .then((res) => res) + .catch((err) => { + throw err; + }); + }; + + const processGetDocsResponse = (data, viewType) => { + if (viewType === viewTypes.original) { + const base64String = data || ""; + const blob = base64toBlob(base64String); + setFileUrl(URL.createObjectURL(blob)); + } else if (viewType === viewTypes.extract) { + setExtractTxt(data); + } + }; + + const handleGetDocsError = (err, viewType) => { + if (err?.response?.status === 404) { + setErrorMessage(viewType); + } + }; + const handleLoadingStateUpdate = (viewType, value) => { if (viewType === viewTypes.original) { setIsDocLoading(value); @@ -243,55 +300,57 @@ function DocumentManager({ generateIndex, handleUpdateTool, handleDocChange }) { moreIcon={<>} />
- -
- {selectedDoc ? ( - - - {selectedDoc?.document_name} - + {!isSimplePromptStudio && ( + +
+ {selectedDoc ? ( + + + {selectedDoc?.document_name} + + + ) : null} +
+
+ + - ) : null} -
-
- +
+
+ - -
-
- - -
-
+
+
+ )}
{activeKey === "1" && ( )} - {SummarizeView && activeKey === "3" && ( + {SummarizeView && !isSimplePromptStudio && activeKey === "3" && ( { + return `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt/${urlPath}`; + }; + const handleChange = async ( event, promptId, @@ -98,9 +111,14 @@ function DocumentParser({ const body = { [`${name}`]: value, }; + + let url = promptUrl(promptDetails?.prompt_id + "/"); + if (isSimplePromptStudio) { + url = promptPatchApiSps(promptDetails?.prompt_id); + } const requestOptions = { method: "PATCH", - url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt/${promptDetails?.prompt_id}/`, + url, headers: { "X-CSRFToken": sessionDetails?.csrfToken, "Content-Type": "application/json", @@ -177,7 +195,7 @@ function DocumentParser({ const handleDelete = (promptId) => { const requestOptions = { method: "DELETE", - url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt/${promptId}/`, + url: promptUrl(promptId + "/"), headers: { "X-CSRFToken": sessionDetails?.csrfToken, }, @@ -226,7 +244,7 @@ function DocumentParser({ const requestOptions = { method: "POST", - url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt/reorder`, + url: promptUrl("reorder"), headers: { "X-CSRFToken": sessionDetails?.csrfToken, "Content-Type": "application/json", diff --git a/frontend/src/components/custom-tools/prompt-card/PrompDnd.jsx b/frontend/src/components/custom-tools/prompt-card/PrompDnd.jsx index d49f82e9d..feb74b71e 100644 --- a/frontend/src/components/custom-tools/prompt-card/PrompDnd.jsx +++ b/frontend/src/components/custom-tools/prompt-card/PrompDnd.jsx @@ -5,6 +5,7 @@ import { useRef } from "react"; import { NotesCard } from "../notes-card/NotesCard"; import { PromptCard } from "./PromptCard"; import { promptType } from "../../../helpers/GetStaticData"; +import { useCustomToolStore } from "../../../store/custom-tool-store"; function PromptDnd({ item, @@ -15,6 +16,7 @@ function PromptDnd({ moveItem, }) { const ref = useRef(null); + const { isSimplePromptStudio } = useCustomToolStore(); const [, drop] = useDrop({ accept: "PROMPT_CARD", @@ -34,7 +36,7 @@ function PromptDnd({ return (
- {item.prompt_type === promptType.prompt && ( + {(item.prompt_type === promptType.prompt || isSimplePromptStudio) && ( { return []; }; +let promptRunApiSps; +let promptOutputApiSps; +try { + promptRunApiSps = + require("../../../plugins/simple-prompt-studio/helper").promptRunApiSps; + promptOutputApiSps = + require("../../../plugins/simple-prompt-studio/helper").promptOutputApiSps; +} catch { + // The component will remain null of it is not available +} + function PromptCard({ promptDetails, handleChange, @@ -59,6 +70,7 @@ function PromptCard({ summarizeIndexStatus, singlePassExtractMode, isSinglePassExtractLoading, + isSimplePromptStudio, } = useCustomToolStore(); const { messages } = useSocketCustomToolStore(); const { sessionDetails } = useSessionStore(); @@ -70,7 +82,7 @@ function PromptCard({ const { getTokenUsage } = useTokenUsage(); useEffect(() => { - const outputTypeData = getDropdownItems("output_type"); + const outputTypeData = getDropdownItems("output_type") || {}; const dropdownList1 = Object.keys(outputTypeData).map((item) => { return { value: outputTypeData[item] }; }); @@ -211,6 +223,9 @@ function PromptCard({ }; const handleDocOutputs = (docId, isLoading, output) => { + if (isSimplePromptStudio) { + return; + } setDocOutputs((prev) => { const updatedDocOutputs = { ...prev }; // Update the entry for the provided docId with isLoading and output @@ -232,7 +247,7 @@ function PromptCard({ // If an error occurs while setting custom posthog event, ignore it and continue } - if (!promptDetails?.profile_manager?.length) { + if (!promptDetails?.profile_manager?.length && !isSimplePromptStudio) { setAlertDetails({ type: "error", content: "LLM Profile is not selected", @@ -310,7 +325,11 @@ function PromptCard({ ); }) .finally(() => { - handleStepsAfterRunCompletion(); + if (isSimplePromptStudio) { + setIsCoverageLoading(false); + } else { + handleStepsAfterRunCompletion(); + } }); }; @@ -380,25 +399,33 @@ function PromptCard({ const promptId = promptDetails?.prompt_id; const runId = generateUUID(); - // Update the token usage state with default token usage for a specific document ID - const tokenUsageId = promptId + "__" + docId; - setTokenUsage(tokenUsageId, defaultTokenUsage); - - // Set up an interval to fetch token usage data at regular intervals - const intervalId = setInterval( - () => getTokenUsage(runId, tokenUsageId), - 5000 // Fetch token usage data every 5000 milliseconds (5 seconds) - ); - const body = { document_id: docId, id: promptId, - run_id: runId, }; + let intervalId; + let tokenUsageId; + let url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/fetch_response/${details?.tool_id}`; + if (!isSimplePromptStudio) { + body["run_id"] = runId; + // Update the token usage state with default token usage for a specific document ID + tokenUsageId = promptId + "__" + docId; + setTokenUsage(tokenUsageId, defaultTokenUsage); + + // Set up an interval to fetch token usage data at regular intervals + intervalId = setInterval( + () => getTokenUsage(runId, tokenUsageId), + 5000 // Fetch token usage data every 5000 milliseconds (5 seconds) + ); + } else { + body["sps_id"] = details?.tool_id; + url = promptRunApiSps; + } + const requestOptions = { method: "POST", - url: `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/fetch_response/${details?.tool_id}`, + url, headers: { "X-CSRFToken": sessionDetails?.csrfToken, "Content-Type": "application/json", @@ -412,21 +439,27 @@ function PromptCard({ throw err; }) .finally(() => { - clearInterval(intervalId); - getTokenUsage(runId, tokenUsageId); + if (!isSimplePromptStudio) { + clearInterval(intervalId); + getTokenUsage(runId, tokenUsageId); + } }); }; const handleGetOutput = () => { - if (!selectedDoc || (!singlePassExtractMode && !selectedLlmProfileId)) { + setIsRunLoading(true); + if ( + !selectedDoc || + (!singlePassExtractMode && !selectedLlmProfileId && !isSimplePromptStudio) + ) { setResult({ promptOutputId: null, output: "", }); + setIsRunLoading(false); return; } - setIsRunLoading(true); handleOutputApiRequest(true) .then((res) => { const data = res?.data; @@ -476,11 +509,20 @@ function PromptCard({ }; const handleOutputApiRequest = async (isOutput) => { - let profileManager = selectedLlmProfileId; - if (singlePassExtractMode) { - profileManager = defaultLlmProfile; + let url; + if (isSimplePromptStudio) { + url = promptOutputApiSps( + details?.tool_id, + promptDetails?.prompt_id, + null + ); + } else { + let profileManager = selectedLlmProfileId; + if (singlePassExtractMode) { + profileManager = defaultLlmProfile; + } + url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt-output/?tool_id=${details?.tool_id}&prompt_id=${promptDetails?.prompt_id}&profile_manager=${profileManager}&is_single_pass_extract=${singlePassExtractMode}`; } - let url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt-output/?tool_id=${details?.tool_id}&prompt_id=${promptDetails?.prompt_id}&profile_manager=${profileManager}&is_single_pass_extract=${singlePassExtractMode}`; if (isOutput) { url += `&document_manager=${selectedDoc?.document_id}`; diff --git a/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx b/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx index a4888e67e..edfd85842 100644 --- a/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx +++ b/frontend/src/components/custom-tools/prompt-card/PromptCardItems.jsx @@ -60,6 +60,7 @@ function PromptCardItems({ singlePassExtractMode, isSinglePassExtractLoading, indexDocs, + isSimplePromptStudio, } = useCustomToolStore(); useEffect(() => { @@ -116,139 +117,145 @@ function PromptCardItems({ />
<> - - -
- - {EvalBtn && !singlePassExtractMode && ( - - )} - + - {isCoverageLoading ? ( - - ) : ( - + {!singlePassExtractMode && ( + )} - - Coverage: {coverage} of {listOfDocs?.length || 0} docs - + handleTypeChange(value)} - /> - -
-
- {!singlePassExtractMode && ( - <> - {llmProfiles?.length > 0 && - promptDetails?.profile_manager?.length > 0 && - selectedLlmProfileId ? ( -
- {llmProfiles - .filter( - (profile) => - profile.profile_id === selectedLlmProfileId - ) - .map((profile, index) => ( -
- {profile.llm} - {profile.vector_store} - {profile.embedding_model} - {profile.x2text} - {`${profile.chunk_size}/${profile.chunk_overlap}/${profile.retrieval_strategy}/${profile.similarity_top_k}/${profile.section}`} -
- ))} -
- ) : ( -
- - No LLM Profile Selected - +
+
+ {!singlePassExtractMode && ( + <> + {llmProfiles?.length > 0 && + promptDetails?.profile_manager?.length > 0 && + selectedLlmProfileId ? ( +
+ {llmProfiles + .filter( + (profile) => + profile.profile_id === selectedLlmProfileId + ) + .map((profile, index) => ( +
+ {profile.llm} + {profile.vector_store} + {profile.embedding_model} + {profile.x2text} + {`${profile.chunk_size}/${profile.chunk_overlap}/${profile.retrieval_strategy}/${profile.similarity_top_k}/${profile.section}`} +
+ ))} +
+ ) : ( +
+ + No LLM Profile Selected + +
+ )} + + )} + {!singlePassExtractMode && ( +
+ +
)} - - )} - {!singlePassExtractMode && ( -
- -
- )} -
- {EvalMetrics && } - + {EvalMetrics && } + + + )} {(isRunLoading || result?.output || result?.output === 0) && ( <> diff --git a/frontend/src/components/custom-tools/tools-main/ToolsMain.jsx b/frontend/src/components/custom-tools/tools-main/ToolsMain.jsx index d32495fb3..7d8953c15 100644 --- a/frontend/src/components/custom-tools/tools-main/ToolsMain.jsx +++ b/frontend/src/components/custom-tools/tools-main/ToolsMain.jsx @@ -1,9 +1,8 @@ -import { BarChartOutlined } from "@ant-design/icons"; -import { Button, Space, Tabs, Tooltip } from "antd"; +import { Tabs, Tooltip } from "antd"; import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { TableOutlined, UnorderedListOutlined } from "@ant-design/icons"; -import { promptType } from "../../../helpers/GetStaticData"; +import { getSequenceNumber, promptType } from "../../../helpers/GetStaticData"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; import { useAlertStore } from "../../../store/alert-store"; @@ -14,14 +13,7 @@ import { DocumentParser } from "../document-parser/DocumentParser"; import { Footer } from "../footer/Footer"; import "./ToolsMain.css"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; - -let RunSinglePassBtn; -try { - RunSinglePassBtn = - require("../../../plugins/run-single-pass-btn/RunSinglePassBtn").RunSinglePassBtn; -} catch { - // The variable is remain undefined if the component is not available -} +import { ToolsMainActionBtns } from "./ToolsMainActionBtns"; function ToolsMain() { const [activeKey, setActiveKey] = useState("1"); @@ -34,23 +26,33 @@ function ToolsMain() { selectedDoc, updateCustomTool, disableLlmOrDocChange, - singlePassExtractMode, - isSinglePassExtractLoading, + isSimplePromptStudio, } = useCustomToolStore(); const { setAlertDetails } = useAlertStore(); const axiosPrivate = useAxiosPrivate(); const handleException = useExceptionHandler(); - const navigate = useNavigate(); const { setPostHogCustomEvent } = usePostHogEvents(); const items = [ { key: "1", - label: "Document Parser", + label: isSimplePromptStudio ? ( + + + + ) : ( + "Document Parser" + ), }, { key: "2", - label: "Combined Output", + label: isSimplePromptStudio ? ( + + + + ) : ( + "Combined Output" + ), disabled: prompts?.length === 0 || disableLlmOrDocChange?.length > 0, }, ]; @@ -69,24 +71,13 @@ function ToolsMain() { return getPromptKey(len + 1); }; - const getSequenceNumber = () => { - let maxSequenceNumber = 0; - prompts.forEach((item) => { - if (item?.sequence_number > maxSequenceNumber) { - maxSequenceNumber = item?.sequence_number; - } - }); - - return maxSequenceNumber + 1; - }; - const defaultPromptInstance = { prompt_key: getPromptKey(prompts?.length + 1), prompt: "", tool_id: details?.tool_id, prompt_type: promptType.prompt, profile_manager: defaultLlmProfile, - sequence_number: getSequenceNumber(), + sequence_number: getSequenceNumber(prompts), }; const defaultNoteInstance = { @@ -94,7 +85,7 @@ function ToolsMain() { prompt: "", tool_id: details?.tool_id, prompt_type: promptType.notes, - sequence_number: getSequenceNumber(), + sequence_number: getSequenceNumber(prompts), }; useEffect(() => { @@ -146,18 +137,6 @@ function ToolsMain() { }); }; - const handleOutputAnalyzerBtnClick = () => { - navigate("outputAnalyzer"); - - try { - setPostHogCustomEvent("ps_output_analyser_seen", { - info: "Clicked on 'Output Analyzer' button", - }); - } catch (err) { - // If an error occurs while setting custom posthog event, ignore it and continue - } - }; - return (
@@ -170,19 +149,7 @@ function ToolsMain() { />
- - {singlePassExtractMode && RunSinglePassBtn && } - -
@@ -197,9 +164,11 @@ function ToolsMain() { )}
-
-
-
+ {!isSimplePromptStudio && ( +
+
+
+ )}
); } diff --git a/frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx b/frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx new file mode 100644 index 000000000..3f0b61229 --- /dev/null +++ b/frontend/src/components/custom-tools/tools-main/ToolsMainActionBtns.jsx @@ -0,0 +1,68 @@ +import { BarChartOutlined } from "@ant-design/icons"; +import { Button, Space, Tooltip } from "antd"; +import { useNavigate } from "react-router-dom"; + +import { useCustomToolStore } from "../../../store/custom-tool-store"; +import usePostHogEvents from "../../../hooks/usePostHogEvents"; + +// Import single pass related components +let RunSinglePassBtn; +try { + RunSinglePassBtn = + require("../../../plugins/run-single-pass-btn/RunSinglePassBtn").RunSinglePassBtn; +} catch { + // The variable will remain undefined if the component is not available +} + +// Import simple prompt studio related components +let AddPromptBtn; +try { + AddPromptBtn = + require("../../../plugins/simple-prompt-studio/AddPromptBtn").AddPromptBtn; +} catch { + // The variable will remain undefined if the component is not available +} + +function ToolsMainActionBtns() { + const { + disableLlmOrDocChange, + singlePassExtractMode, + isSinglePassExtractLoading, + isSimplePromptStudio, + } = useCustomToolStore(); + const navigate = useNavigate(); + const { setPostHogCustomEvent } = usePostHogEvents(); + + const handleOutputAnalyzerBtnClick = () => { + navigate("outputAnalyzer"); + + try { + setPostHogCustomEvent("ps_output_analyser_seen", { + info: "Clicked on 'Output Analyzer' button", + }); + } catch (err) { + // If an error occurs while setting custom posthog event, ignore it and continue + } + }; + + if (isSimplePromptStudio && AddPromptBtn) { + return ; + } + + return ( + + {singlePassExtractMode && RunSinglePassBtn && } + +
- -
- Configuration -
-
- {!specConfig || Object.keys(specConfig)?.length === 0 ? ( - - ) : ( - - )} + {connType !== "MANUALREVIEW" && ( + +
+ Configuration +
+
+ {!specConfig || Object.keys(specConfig)?.length === 0 ? ( + + ) : ( + + )} +
-
- + + )} ); } diff --git a/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx b/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx index bf0d78e4d..7647f5a62 100644 --- a/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx +++ b/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx @@ -63,7 +63,6 @@ function DsSettingsCard({ type, endpointDetails, message }) { const [formDataConfig, setFormDataConfig] = useState({}); const [selectedId, setSelectedId] = useState(""); const [selectedItemName, setSelectedItemName] = useState(""); - const [inputOptions, setInputOptions] = useState([ { value: "API", @@ -84,14 +83,32 @@ function DsSettingsCard({ type, endpointDetails, message }) { const { setAlertDetails } = useAlertStore(); const axiosPrivate = useAxiosPrivate(); const handleException = useExceptionHandler(); - const { flags } = sessionDetails; const icons = { input: , output: , }; - + useEffect(() => { + try { + const inputOption = + require("../../../plugins/dscard-input-options/DsSettingsCardInputOptions").inputOption; + if (flags.manual_review && inputOption) { + setInputOptions((prevInputOptions) => { + // Check if inputOption already exists in prevInputOptions + if (prevInputOptions.some((opt) => opt.value === inputOption.value)) { + return prevInputOptions; // Return previous state unchanged + } else { + // Create a new array with the existing options and the new option + const updatedInputOptions = [...prevInputOptions, inputOption]; + return updatedInputOptions; + } + }); + } + } catch { + // The component will remain null of it is not available + } + }, []); useEffect(() => { try { const inputOption = @@ -114,7 +131,10 @@ function DsSettingsCard({ type, endpointDetails, message }) { } else { // Filter options based on source connection type const filteredOptions = ["API"].includes(source?.connection_type) - ? inputOptions.filter((option) => option.value === "API") + ? inputOptions.filter( + (option) => + option.value === "API" || option.value === "MANUALREVIEW" + ) : inputOptions.filter((option) => option.value !== "API"); setOptions(filteredOptions); @@ -125,7 +145,9 @@ function DsSettingsCard({ type, endpointDetails, message }) { // Remove Database from Source Dropdown const filteredOptions = inputOptions.filter( (option) => - option.value !== "DATABASE" && option.value !== "APPDEPLOYMENT" + option.value !== "DATABASE" && + option.value !== "APPDEPLOYMENT" && + option.value !== "MANUALREVIEW" ); setOptions(filteredOptions); } @@ -376,6 +398,7 @@ function DsSettingsCard({ type, endpointDetails, message }) { disabled={ !endpointDetails?.connection_type || connType === "API" || + connType === "MANUALREVIEW" || connType === "APPDEPLOYMENT" } > @@ -398,7 +421,7 @@ function DsSettingsCard({ type, endpointDetails, message }) { ) : ( <> - {connType === "API" ? ( + {connType === "API" || connType === "MANUALREVIEW" ? ( - {titleCase(type)} set to API successfully + {titleCase(type)} set to {connType} successfully ) : ( @@ -463,7 +486,6 @@ DsSettingsCard.propTypes = { type: PropTypes.string.isRequired, endpointDetails: PropTypes.object.isRequired, message: PropTypes.string, - canUpdate: PropTypes.bool.isRequired, }; export { DsSettingsCard }; diff --git a/frontend/src/components/helpers/auth/RequireAuth.js b/frontend/src/components/helpers/auth/RequireAuth.js index 424967e1f..f745aca17 100644 --- a/frontend/src/components/helpers/auth/RequireAuth.js +++ b/frontend/src/components/helpers/auth/RequireAuth.js @@ -1,11 +1,11 @@ import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { useEffect } from "react"; import { getOrgNameFromPathname, onboardCompleted, } from "../../../helpers/GetStaticData"; import { useSessionStore } from "../../../store/session-store"; -import { useEffect } from "react"; import usePostHogEvents from "../../../hooks/usePostHogEvents"; let ProductFruitsManager; @@ -25,6 +25,7 @@ const RequireAuth = () => { const pathname = location?.pathname; const adapters = sessionDetails?.adapters; const currOrgName = getOrgNameFromPathname(pathname); + const { flags } = sessionDetails; useEffect(() => { if (!sessionDetails?.isLoggedIn) { @@ -38,6 +39,14 @@ const RequireAuth = () => { if (onboardCompleted(adapters)) { navigateTo = `/${orgName}/tools`; } + if (flags.manual_review) { + if ( + sessionDetails.role === "unstract_reviewer" || + sessionDetails.role === "unstract_supervisor" + ) { + navigateTo = `/${orgName}/review`; + } + } if (!isLoggedIn) { return ; diff --git a/frontend/src/components/helpers/auth/RequireGuest.js b/frontend/src/components/helpers/auth/RequireGuest.js index ff5a48ab5..42de4f693 100644 --- a/frontend/src/components/helpers/auth/RequireGuest.js +++ b/frontend/src/components/helpers/auth/RequireGuest.js @@ -5,13 +5,21 @@ import { useSessionStore } from "../../../store/session-store"; const RequireGuest = () => { const { sessionDetails } = useSessionStore(); - const { orgName, adapters } = sessionDetails; + const { orgName, adapters, flags } = sessionDetails; const location = useLocation(); const pathname = location.pathname; let navigateTo = `/${orgName}/onboard`; if (onboardCompleted(adapters)) { navigateTo = `/${orgName}/tools`; } + if (flags.manual_review) { + if ( + sessionDetails.role === "unstract_reviewer" || + sessionDetails.role === "unstract_supervisor" + ) { + navigateTo = `/${orgName}/review`; + } + } return !sessionDetails?.isLoggedIn && publicRoutes.includes(pathname) ? ( diff --git a/frontend/src/components/navigations/top-nav-bar/TopNavBar.css b/frontend/src/components/navigations/top-nav-bar/TopNavBar.css index 511f62628..bb4095d7d 100644 --- a/frontend/src/components/navigations/top-nav-bar/TopNavBar.css +++ b/frontend/src/components/navigations/top-nav-bar/TopNavBar.css @@ -58,3 +58,15 @@ .switch-org-menu .ant-dropdown-menu-title-content .ant-space { display: block; } + +.page-identifier { + vertical-align: super; + color: white; +} + +.page-heading { + color: white; + font-size: 16px; + font-weight: 600; + margin-left: 10px; +} diff --git a/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx b/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx index 5d5339baf..cce90112f 100644 --- a/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx +++ b/frontend/src/components/navigations/top-nav-bar/TopNavBar.jsx @@ -8,8 +8,16 @@ import { Space, Typography, } from "antd"; +import { + UserOutlined, + UserSwitchOutlined, + LogoutOutlined, + DownloadOutlined, + FileProtectOutlined, + LikeOutlined, +} from "@ant-design/icons"; import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import axios from "axios"; import { UnstractLogo } from "../../../assets/index.js"; @@ -36,19 +44,54 @@ try { function TopNavBar() { const navigate = useNavigate(); const { sessionDetails } = useSessionStore(); - const { orgName, remainingTrialDays, allOrganization, orgId } = + const { orgName, remainingTrialDays, allOrganization, orgId, flags } = sessionDetails; const baseUrl = getBaseUrl(); const onBoardUrl = baseUrl + `/${orgName}/onboard`; const logout = useLogout(); const [showOnboardBanner, setShowOnboardBanner] = useState(false); + const [approverStatus, setApproverStatus] = useState(false); + const [reviewerStatus, setReviewerStatus] = useState(false); + const [reviewPageHeader, setReviewPageHeader] = useState(""); const { setAlertDetails } = useAlertStore(); const handleException = useExceptionHandler(); + const location = useLocation(); useEffect(() => { - setShowOnboardBanner(!onboardCompleted(sessionDetails?.adapters)); + const isUnstractReviewer = sessionDetails.role === "unstract_reviewer"; + const isUnstractSupervisor = sessionDetails.role === "unstract_supervisor"; + const isUnstractAdmin = sessionDetails.role === "unstract_admin"; + + setShowOnboardBanner( + !onboardCompleted(sessionDetails?.adapters) && + !isUnstractReviewer && + !isUnstractSupervisor + ); + + setApproverStatus( + (isUnstractAdmin || isUnstractSupervisor) && flags.manual_review + ); + setReviewerStatus(isUnstractReviewer && flags.manual_review); }, [sessionDetails]); + useEffect(() => { + if (flags.manual_review) { + const checkReviewPage = location.pathname.split("review"); + + if (checkReviewPage.length > 1) { + if (checkReviewPage[1].includes("/approve")) { + setReviewPageHeader("Approve"); + } else if (checkReviewPage[1].includes("/download_and_sync")) { + setReviewPageHeader("Download and syncmanager"); + } else { + setReviewPageHeader("Review"); + } + } else { + setReviewPageHeader(null); + } + } + }, [location]); + const cascadeOptions = allOrganization.map((org) => { return { key: org?.id, @@ -92,7 +135,7 @@ function TopNavBar() { }); }; - // Dropdown items + // Profile Dropdown items const items = [ { key: "1", @@ -101,15 +144,7 @@ function TopNavBar() { onClick={() => navigate(`/${orgName}/profile`)} className="logout-button" > - Profile - - ), - }, - { - key: "2", - label: ( - ), }, @@ -126,10 +161,54 @@ function TopNavBar() { }} placement="left" > -
Switch Org
+
+ {" "} + Switch Org +
), }, + (reviewerStatus || approverStatus) && { + key: "4", + label: ( + + ), + }, + approverStatus && { + key: "5", + label: ( + + ), + }, + approverStatus && { + key: "6", + label: ( + + ), + }, + { + key: "2", + label: ( + + ), + }, ]; // Function to get the initials from the user name @@ -144,10 +223,16 @@ function TopNavBar() { return ( - + + {reviewPageHeader && ( + + + {reviewPageHeader} + + )} - + {showOnboardBanner && (
diff --git a/frontend/src/helpers/GetSessionData.js b/frontend/src/helpers/GetSessionData.js index f5b8c288b..5ee6b9291 100644 --- a/frontend/src/helpers/GetSessionData.js +++ b/frontend/src/helpers/GetSessionData.js @@ -26,6 +26,7 @@ function getSessionData(sessionData) { loginOnboardingMessage: sessionData?.loginOnboardingMessage, promptOnboardingMessage: sessionData?.promptOnboardingMessage, flags: sessionData?.flags, + role: sessionData?.role, }; } diff --git a/frontend/src/hooks/useSessionValid.js b/frontend/src/hooks/useSessionValid.js index 16fcf754b..fe34e91f2 100644 --- a/frontend/src/hooks/useSessionValid.js +++ b/frontend/src/hooks/useSessionValid.js @@ -129,6 +129,7 @@ function useSessionValid() { if (isPlatformAdmin) { userAndOrgDetails["isPlatformAdmin"] = await isPlatformAdmin(); } + userAndOrgDetails["role"] = userSessionData.role; // Set the session details setSessionDetails(getSessionData(userAndOrgDetails)); } catch (err) { diff --git a/frontend/src/routes/Router.jsx b/frontend/src/routes/Router.jsx index ab99fde82..47bc7c8ba 100644 --- a/frontend/src/routes/Router.jsx +++ b/frontend/src/routes/Router.jsx @@ -35,7 +35,8 @@ let PlatformAdminPage; let AppDeployments; let ChatAppPage; let ChatAppLayout; - +let ManualReviewPage; +let ReviewLayout; try { TrialRoutes = require("../plugins/subscription/trial-page/TrialEndPage.jsx").TrialEndPage; @@ -52,12 +53,18 @@ try { require("../plugins/app-deployment/AppDeployments.jsx").AppDeployments; ChatAppPage = require("../plugins/app-deployment/chat-app/ChatAppPage.jsx").ChatAppPage; - ChatAppLayout = - require("../plugins/app-deployment/chat-app/ChatAppLayout.jsx").ChatAppLayout; } catch (err) { // Do nothing, Not-found Page will be triggered. } +try { + ManualReviewPage = + require("../plugins/manual-review/page/ManualReviewPage.jsx").ManualReviewPage; + ReviewLayout = + require("../plugins/manual-review/review-layout/ReviewLayout.jsx").ReviewLayout; +} catch (err) { + // Do nothing, Not-found Page will be triggered. +} // Import pages/components related to Simple Prompt Studio. let SimplePromptStudioHelper; let SimplePromptStudio; @@ -178,6 +185,22 @@ function Router() { )} + {ReviewLayout && ManualReviewPage && ( + }> + } + > + } + /> + } + /> + + )} {TrialRoutes && ( } /> diff --git a/unstract/connectors/src/unstract/connectors/connectorkit.py b/unstract/connectors/src/unstract/connectors/connectorkit.py index 1ef216491..5b1d574c1 100644 --- a/unstract/connectors/src/unstract/connectors/connectorkit.py +++ b/unstract/connectors/src/unstract/connectors/connectorkit.py @@ -7,13 +7,14 @@ from unstract.connectors.databases import connectors as db_connectors from unstract.connectors.enums import ConnectorMode from unstract.connectors.filesystems import connectors as fs_connectors +from unstract.connectors.queues import connectors as q_connectors logger = logging.getLogger(__name__) class Connectorkit: def __init__(self) -> None: - self._connectors: ConnectorDict = fs_connectors | db_connectors + self._connectors: ConnectorDict = fs_connectors | db_connectors | q_connectors @property def connectors(self) -> ConnectorDict: diff --git a/unstract/connectors/src/unstract/connectors/enums.py b/unstract/connectors/src/unstract/connectors/enums.py index f28ec5cb1..e68b4b063 100644 --- a/unstract/connectors/src/unstract/connectors/enums.py +++ b/unstract/connectors/src/unstract/connectors/enums.py @@ -6,3 +6,4 @@ class ConnectorMode(Enum): FILE_SYSTEM = "FILE_SYSTEM" DATABASE = "DATABASE" API = "API" + MANUAL_REVIEW = "MANUAL_REVIEW" diff --git a/unstract/connectors/src/unstract/connectors/queues/__init__.py b/unstract/connectors/src/unstract/connectors/queues/__init__.py new file mode 100644 index 000000000..8a6d8d0bd --- /dev/null +++ b/unstract/connectors/src/unstract/connectors/queues/__init__.py @@ -0,0 +1,5 @@ +from unstract.connectors import ConnectorDict +from unstract.connectors.queues.register import register_connectors + +connectors: ConnectorDict = {} +register_connectors(connectors) diff --git a/unstract/connectors/src/unstract/connectors/queues/register.py b/unstract/connectors/src/unstract/connectors/queues/register.py new file mode 100644 index 000000000..4eca532c6 --- /dev/null +++ b/unstract/connectors/src/unstract/connectors/queues/register.py @@ -0,0 +1,38 @@ +import logging +import os +from importlib import import_module +from typing import Any + +from unstract.connectors.constants import Common +from unstract.connectors.queues.unstract_queue import UnstractQueue + +logger = logging.getLogger(__name__) + + +def register_connectors(connectors: dict[str, Any]) -> None: + current_directory = os.path.dirname(os.path.abspath(__file__)) + package = "unstract.connectors.queues" + + for connector in os.listdir(current_directory): + connector_path = os.path.join(current_directory, connector) + # Check if the item is a directory and not a special directory like __pycache__ + if os.path.isdir(connector_path) and not connector.startswith("__"): + try: + full_module_path = f"{package}.{connector}" + module = import_module(full_module_path) + metadata = getattr(module, "metadata", {}) + if metadata.get("is_active", False): + connector_class: UnstractQueue = metadata[Common.CONNECTOR] + connector_id = connector_class.get_id() + if not connector_id or (connector_id in connectors): + logger.warning(f"Duplicate Id : {connector_id}") + else: + connectors[connector_id] = { + Common.MODULE: module, + Common.METADATA: metadata, + } + except ModuleNotFoundError as exception: + logger.error(f"Error while importing connectors : {exception}") + + if len(connectors) == 0: + logger.warning("No connector found.") diff --git a/unstract/connectors/src/unstract/connectors/queues/unstract_queue.py b/unstract/connectors/src/unstract/connectors/queues/unstract_queue.py new file mode 100644 index 000000000..940bfe041 --- /dev/null +++ b/unstract/connectors/src/unstract/connectors/queues/unstract_queue.py @@ -0,0 +1,95 @@ +import logging +from abc import ABC, abstractmethod +from typing import Any + +from unstract.connectors.base import UnstractConnector +from unstract.connectors.enums import ConnectorMode + + +class UnstractQueue(UnstractConnector, ABC): + """Abstract class for queue connector.""" + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(filename)s - %(message)s", + ) + + def __init__(self, name: str): + super().__init__(name) + self.name = name + + @staticmethod + def get_id() -> str: + return "" + + @staticmethod + def get_name() -> str: + return "" + + @staticmethod + def get_description() -> str: + return "" + + @staticmethod + def get_icon() -> str: + return "" + + @staticmethod + def get_json_schema() -> str: + return "" + + @staticmethod + def can_write() -> bool: + return False + + @staticmethod + def can_read() -> bool: + return False + + @staticmethod + def requires_oauth() -> bool: + return False + + @staticmethod + def python_social_auth_backend() -> str: + return "" + + @staticmethod + def get_connector_mode() -> ConnectorMode: + return ConnectorMode.MANUAL_REVIEW + + def test_credentials(self) -> bool: + """Override to test credentials for a connector.""" + return True + + @abstractmethod + def get_engine(self) -> Any: + pass + + @abstractmethod + def enqueue(self, queue_name: str, message: str) -> Any: + pass + + @abstractmethod + def dequeue(self, queue_name: str, timeout: int = 5) -> Any: + pass + + @abstractmethod + def peek(self, queue_name: str) -> Any: + pass + + @abstractmethod + def lset(self, queue_name: str, index: int, value: str) -> None: + pass + + @abstractmethod + def llen(self, queue_name: str) -> int: + pass + + @abstractmethod + def lindex(self, queue_name: str, index: int) -> Any: + pass + + @abstractmethod + def keys(self, pattern: str = "*") -> list[str]: + pass From ce49f153a2e1542bd25ac3d2c0c4588713e066f0 Mon Sep 17 00:00:00 2001 From: Deepak K <89829542+Deepak-Kesavan@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:47:05 +0530 Subject: [PATCH 15/31] Updated postman collection (#454) Co-authored-by: Neha <115609453+nehabagdia@users.noreply.github.com> --- backend/api/constants.py | 1 - backend/api/postman_collection/dto.py | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/api/constants.py b/backend/api/constants.py index ede21ad87..0ec324cc9 100644 --- a/backend/api/constants.py +++ b/backend/api/constants.py @@ -1,7 +1,6 @@ class ApiExecution: PATH: str = "deployment/api" MAXIMUM_TIMEOUT_IN_SEC: int = 300 # 5 minutes - DEFAULT_TIMEOUT_IN_SEC: int = 80 FILES_FORM_DATA: str = "files" TIMEOUT_FORM_DATA: str = "timeout" INCLUDE_METADATA: str = "include_metadata" diff --git a/backend/api/postman_collection/dto.py b/backend/api/postman_collection/dto.py index e905e64f8..c862baf81 100644 --- a/backend/api/postman_collection/dto.py +++ b/backend/api/postman_collection/dto.py @@ -99,7 +99,12 @@ def create( FormDataItem( key=ApiExecution.TIMEOUT_FORM_DATA, type="text", - value=ApiExecution.DEFAULT_TIMEOUT_IN_SEC, + value=ApiExecution.MAXIMUM_TIMEOUT_IN_SEC, + ), + FormDataItem( + key=ApiExecution.INCLUDE_METADATA, + type="text", + value=False, ), ] ) From 48826c4109d0e8671d36035f0fe11de60dd699d1 Mon Sep 17 00:00:00 2001 From: Deepak K <89829542+Deepak-Kesavan@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:27:44 +0530 Subject: [PATCH 16/31] Updated README for Vector Db Milvus (#423) Co-authored-by: Gayathri <142381512+gaya3-zipstack@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1408c983b..95024bd24 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Unstract comes well documented. You can get introduced to the [basics of Unstrac || Weaviate | ✅ Working | || Pinecone | ✅ Working | || PostgreSQL | ✅ Working | -|| Milvus | 🗓️ Coming soon! | +|| Milvus | ✅ Working | From fb567217c0cf0f8820946eb364d08e747cb4b51f Mon Sep 17 00:00:00 2001 From: Rahul Johny <116638720+johnyrahul@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:06:25 +0530 Subject: [PATCH 17/31] Added util for loading file based on the path (#451) --- backend/workflow_manager/endpoint/source.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/workflow_manager/endpoint/source.py b/backend/workflow_manager/endpoint/source.py index e755a697c..4dd096044 100644 --- a/backend/workflow_manager/endpoint/source.py +++ b/backend/workflow_manager/endpoint/source.py @@ -3,6 +3,7 @@ import os import shutil from hashlib import md5, sha256 +from io import BytesIO from pathlib import Path from typing import Any, Optional @@ -384,6 +385,18 @@ def handle_final_result( if connection_type == WorkflowEndpoint.ConnectionType.API: results.append({"file": file_name, "result": result}) + def load_file(self, input_file_path: str) -> tuple[str, BytesIO]: + connector: ConnectorInstance = self.endpoint.connector_instance + connector_settings: dict[str, Any] = connector.connector_metadata + source_fs: fsspec.AbstractFileSystem = self.get_fsspec( + settings=connector_settings, connector_id=connector.connector_id + ) + with source_fs.open(input_file_path, "rb") as remote_file: + file_content = remote_file.read() + file_stream = BytesIO(file_content) + + return remote_file.key, file_stream + @classmethod def add_input_file_to_api_storage( cls, workflow_id: str, execution_id: str, file_objs: list[UploadedFile] From 15d39dfe7fb931f4cfeac375780365fc0152b86e Mon Sep 17 00:00:00 2001 From: Rahul Johny <116638720+johnyrahul@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:26:15 +0530 Subject: [PATCH 18/31] Added back the componet which missed out due to conflict resolution (#455) --- .../src/components/agency/ds-settings-card/DsSettingsCard.jsx | 4 +++- frontend/src/routes/Router.jsx | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx b/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx index 7647f5a62..9681a96c8 100644 --- a/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx +++ b/frontend/src/components/agency/ds-settings-card/DsSettingsCard.jsx @@ -421,7 +421,9 @@ function DsSettingsCard({ type, endpointDetails, message }) { ) : ( <> - {connType === "API" || connType === "MANUALREVIEW" ? ( + {connType === "API" || + connType === "MANUALREVIEW" || + connType === "APPDEPLOYMENT" ? ( Date: Fri, 5 Jul 2024 16:28:12 +0530 Subject: [PATCH 19/31] handled flag not available case in auth helper frontend (#457) --- frontend/src/components/helpers/auth/RequireAuth.js | 2 +- frontend/src/components/helpers/auth/RequireGuest.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/helpers/auth/RequireAuth.js b/frontend/src/components/helpers/auth/RequireAuth.js index f745aca17..36c9f8a90 100644 --- a/frontend/src/components/helpers/auth/RequireAuth.js +++ b/frontend/src/components/helpers/auth/RequireAuth.js @@ -39,7 +39,7 @@ const RequireAuth = () => { if (onboardCompleted(adapters)) { navigateTo = `/${orgName}/tools`; } - if (flags.manual_review) { + if (flags?.manual_review) { if ( sessionDetails.role === "unstract_reviewer" || sessionDetails.role === "unstract_supervisor" diff --git a/frontend/src/components/helpers/auth/RequireGuest.js b/frontend/src/components/helpers/auth/RequireGuest.js index 42de4f693..778b46696 100644 --- a/frontend/src/components/helpers/auth/RequireGuest.js +++ b/frontend/src/components/helpers/auth/RequireGuest.js @@ -12,7 +12,7 @@ const RequireGuest = () => { if (onboardCompleted(adapters)) { navigateTo = `/${orgName}/tools`; } - if (flags.manual_review) { + if (flags?.manual_review) { if ( sessionDetails.role === "unstract_reviewer" || sessionDetails.role === "unstract_supervisor" From 4efcb1aae0c951acfa6749dc5f6808088d1f2821 Mon Sep 17 00:00:00 2001 From: jagadeeswaran-zipstack Date: Mon, 8 Jul 2024 11:07:02 +0530 Subject: [PATCH 20/31] feat/multi llm enhancement (#412) * added multiple llm profiles * made changes in coverage and combined output * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * sonar lint fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * added index output viewer * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * pre-commit fixes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * pre-commit fixes * added timer and profile info bar * added profile limit * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Trigger Build * code clean up * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * code clean up * code optimization * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * removed unwanted f-strings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * added ttl to settings and updated sample.env * added types for func * single pass fix * added total cost and context to UI * code refactor * fix sonar issue * code refactor * code refactor * FE build fix * re-index fix * remove defaults for env * changed display text for chuck * code refactor * code refactor * Update backend/prompt_studio/prompt_studio_core/views.py Signed-off-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> * changed context db type to textfield * simple prompt studio compatability fix * moved polling to FE * sonar fix * sonar fix for nesting * move polling to static func * added enum for index status * reduced timeout value for polling * FE code cleanup --------- Signed-off-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Chandrasekharan M <117059509+chandrasekharan-zipstack@users.noreply.github.com> Co-authored-by: Neha <115609453+nehabagdia@users.noreply.github.com> Co-authored-by: Tahier Hussain <89440263+tahierhussain@users.noreply.github.com> --- backend/adapter_processor/serializers.py | 4 + backend/backend/settings/base.py | 1 + .../prompt_profile_manager/constants.py | 2 + .../profile_manager_helper.py | 11 + .../prompt_profile_manager/serializers.py | 13 + .../prompt_studio_core/constants.py | 10 + .../document_indexing_service.py | 53 ++ .../prompt_studio_core/exceptions.py | 9 + .../prompt_studio_helper.py | 401 +++++++------ .../prompt_studio/prompt_studio_core/views.py | 19 +- .../0013_promptstudiooutputmanager_context.py | 20 + ...alter_promptstudiooutputmanager_context.py | 20 + .../prompt_studio_output_manager/models.py | 3 + .../output_manager_helper.py | 108 +++- backend/sample.env | 3 + backend/usage/constants.py | 1 + backend/usage/helper.py | 2 + frontend/package-lock.json | 33 ++ frontend/package.json | 1 + .../combined-output/CombinedOutput.jsx | 57 +- .../custom-tools/combined-output/JsonView.jsx | 27 +- .../OutputForDocModal.jsx | 68 ++- .../profile-info-bar/ProfileInfoBar.css | 3 + .../profile-info-bar/ProfileInfoBar.jsx | 58 ++ .../custom-tools/prompt-card/Header.jsx | 87 ++- .../prompt-card/OutputForIndex.jsx | 50 ++ .../custom-tools/prompt-card/PromptCard.css | 103 +++- .../custom-tools/prompt-card/PromptCard.jsx | 529 +++++++++++------- .../prompt-card/PromptCardItems.jsx | 520 +++++++++++++---- .../custom-tools/token-usage/TokenUsage.jsx | 9 +- frontend/src/helpers/GetStaticData.js | 65 +++ frontend/src/hooks/useWindowDimensions.js | 24 + 32 files changed, 1770 insertions(+), 544 deletions(-) create mode 100644 backend/prompt_studio/prompt_profile_manager/profile_manager_helper.py create mode 100644 backend/prompt_studio/prompt_studio_core/document_indexing_service.py create mode 100644 backend/prompt_studio/prompt_studio_output_manager/migrations/0013_promptstudiooutputmanager_context.py create mode 100644 backend/prompt_studio/prompt_studio_output_manager/migrations/0014_alter_promptstudiooutputmanager_context.py create mode 100644 frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.css create mode 100644 frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.jsx create mode 100644 frontend/src/components/custom-tools/prompt-card/OutputForIndex.jsx create mode 100644 frontend/src/hooks/useWindowDimensions.js diff --git a/backend/adapter_processor/serializers.py b/backend/adapter_processor/serializers.py index 80637206e..a3f281dcb 100644 --- a/backend/adapter_processor/serializers.py +++ b/backend/adapter_processor/serializers.py @@ -124,6 +124,10 @@ def to_representation(self, instance: AdapterInstance) -> dict[str, str]: rep[common.ICON] = AdapterProcessor.get_adapter_data_with_key( instance.adapter_id, common.ICON ) + adapter_metadata = instance.get_adapter_meta_data() + model = adapter_metadata.get("model") + if model: + rep["model"] = model if instance.is_friction_less: rep["created_by_email"] = "Unstract" diff --git a/backend/backend/settings/base.py b/backend/backend/settings/base.py index ae25fe6b2..902a7d012 100644 --- a/backend/backend/settings/base.py +++ b/backend/backend/settings/base.py @@ -167,6 +167,7 @@ def get_required_setting( "CELERY_BROKER_URL", f"redis://{REDIS_HOST}:{REDIS_PORT}" ) +INDEXING_FLAG_TTL = int(get_required_setting("INDEXING_FLAG_TTL")) # Flag to Enable django admin ADMIN_ENABLED = False diff --git a/backend/prompt_studio/prompt_profile_manager/constants.py b/backend/prompt_studio/prompt_profile_manager/constants.py index 70cf34019..6540b58ee 100644 --- a/backend/prompt_studio/prompt_profile_manager/constants.py +++ b/backend/prompt_studio/prompt_profile_manager/constants.py @@ -7,6 +7,8 @@ class ProfileManagerKeys: VECTOR_STORE = "vector_store" EMBEDDING_MODEL = "embedding_model" X2TEXT = "x2text" + PROMPT_STUDIO_TOOL = "prompt_studio_tool" + MAX_PROFILE_COUNT = 4 class ProfileManagerErrors: diff --git a/backend/prompt_studio/prompt_profile_manager/profile_manager_helper.py b/backend/prompt_studio/prompt_profile_manager/profile_manager_helper.py new file mode 100644 index 000000000..68783b551 --- /dev/null +++ b/backend/prompt_studio/prompt_profile_manager/profile_manager_helper.py @@ -0,0 +1,11 @@ +from prompt_studio.prompt_profile_manager.models import ProfileManager + + +class ProfileManagerHelper: + + @classmethod + def get_profile_manager(cls, profile_manager_id: str) -> ProfileManager: + try: + return ProfileManager.objects.get(profile_id=profile_manager_id) + except ProfileManager.DoesNotExist: + raise ValueError("ProfileManager does not exist.") diff --git a/backend/prompt_studio/prompt_profile_manager/serializers.py b/backend/prompt_studio/prompt_profile_manager/serializers.py index 4d4753561..fc83aaab4 100644 --- a/backend/prompt_studio/prompt_profile_manager/serializers.py +++ b/backend/prompt_studio/prompt_profile_manager/serializers.py @@ -2,6 +2,7 @@ from adapter_processor.adapter_processor import AdapterProcessor from prompt_studio.prompt_profile_manager.constants import ProfileManagerKeys +from prompt_studio.prompt_studio_core.exceptions import MaxProfilesReachedError from backend.serializers import AuditSerializer @@ -38,3 +39,15 @@ def to_representation(self, instance): # type: ignore AdapterProcessor.get_adapter_instance_by_id(x2text) ) return rep + + def validate(self, data): + prompt_studio_tool = data.get(ProfileManagerKeys.PROMPT_STUDIO_TOOL) + + profile_count = ProfileManager.objects.filter( + prompt_studio_tool=prompt_studio_tool + ).count() + + if profile_count >= ProfileManagerKeys.MAX_PROFILE_COUNT: + raise MaxProfilesReachedError() + + return data diff --git a/backend/prompt_studio/prompt_studio_core/constants.py b/backend/prompt_studio/prompt_studio_core/constants.py index 934d9b530..55d61e32e 100644 --- a/backend/prompt_studio/prompt_studio_core/constants.py +++ b/backend/prompt_studio/prompt_studio_core/constants.py @@ -85,6 +85,9 @@ class ToolStudioPromptKeys: NOTES = "NOTES" OUTPUT = "output" SEQUENCE_NUMBER = "sequence_number" + PROFILE_MANAGER_ID = "profile_manager" + CONTEXT = "context" + METADATA = "metadata" class FileViewTypes: @@ -108,6 +111,13 @@ class LogLevel(Enum): FATAL = "FATAL" +class IndexingStatus(Enum): + PENDING_STATUS = "pending" + COMPLETED_STATUS = "completed" + STARTED_STATUS = "started" + DOCUMENT_BEING_INDEXED = "Document is being indexed" + + class DefaultPrompts: PREAMBLE = ( "Your ability to extract and summarize this context accurately " diff --git a/backend/prompt_studio/prompt_studio_core/document_indexing_service.py b/backend/prompt_studio/prompt_studio_core/document_indexing_service.py new file mode 100644 index 000000000..539c5a2dc --- /dev/null +++ b/backend/prompt_studio/prompt_studio_core/document_indexing_service.py @@ -0,0 +1,53 @@ +from typing import Optional + +from django.conf import settings +from prompt_studio.prompt_studio_core.constants import IndexingStatus +from utils.cache_service import CacheService + + +class DocumentIndexingService: + CACHE_PREFIX = "document_indexing:" + + @classmethod + def set_document_indexing(cls, org_id: str, user_id: str, doc_id_key: str) -> None: + CacheService.set_key( + cls._cache_key(org_id, user_id, doc_id_key), + IndexingStatus.STARTED_STATUS.value, + expire=settings.INDEXING_FLAG_TTL, + ) + + @classmethod + def is_document_indexing(cls, org_id: str, user_id: str, doc_id_key: str) -> bool: + return ( + CacheService.get_key(cls._cache_key(org_id, user_id, doc_id_key)) + == IndexingStatus.STARTED_STATUS.value + ) + + @classmethod + def mark_document_indexed( + cls, org_id: str, user_id: str, doc_id_key: str, doc_id: str + ) -> None: + CacheService.set_key( + cls._cache_key(org_id, user_id, doc_id_key), + doc_id, + expire=settings.INDEXING_FLAG_TTL, + ) + + @classmethod + def get_indexed_document_id( + cls, org_id: str, user_id: str, doc_id_key: str + ) -> Optional[str]: + result = CacheService.get_key(cls._cache_key(org_id, user_id, doc_id_key)) + if result and result != IndexingStatus.STARTED_STATUS.value: + return result + return None + + @classmethod + def remove_document_indexing( + cls, org_id: str, user_id: str, doc_id_key: str + ) -> None: + CacheService.delete_a_key(cls._cache_key(org_id, user_id, doc_id_key)) + + @classmethod + def _cache_key(cls, org_id: str, user_id: str, doc_id_key: str) -> str: + return f"{cls.CACHE_PREFIX}{org_id}:{user_id}:{doc_id_key}" diff --git a/backend/prompt_studio/prompt_studio_core/exceptions.py b/backend/prompt_studio/prompt_studio_core/exceptions.py index 666d41241..241418060 100644 --- a/backend/prompt_studio/prompt_studio_core/exceptions.py +++ b/backend/prompt_studio/prompt_studio_core/exceptions.py @@ -1,3 +1,4 @@ +from prompt_studio.prompt_profile_manager.constants import ProfileManagerKeys from prompt_studio.prompt_studio_core.constants import ToolStudioErrors from rest_framework.exceptions import APIException @@ -58,3 +59,11 @@ class PermissionError(APIException): class EmptyPromptError(APIException): status_code = 422 default_detail = "Prompt(s) cannot be empty" + + +class MaxProfilesReachedError(APIException): + status_code = 403 + default_detail = ( + f"Maximum number of profiles (max {ProfileManagerKeys.MAX_PROFILE_COUNT})" + " per prompt studio project has been reached." + ) diff --git a/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py b/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py index caa4e377c..c0f7b73d7 100644 --- a/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py +++ b/backend/prompt_studio/prompt_studio_core/prompt_studio_helper.py @@ -13,9 +13,15 @@ from django.db.models.manager import BaseManager from file_management.file_management_helper import FileManagerHelper from prompt_studio.prompt_profile_manager.models import ProfileManager +from prompt_studio.prompt_profile_manager.profile_manager_helper import ( + ProfileManagerHelper, +) from prompt_studio.prompt_studio.models import ToolStudioPrompt -from prompt_studio.prompt_studio_core.constants import LogLevels +from prompt_studio.prompt_studio_core.constants import IndexingStatus, LogLevels from prompt_studio.prompt_studio_core.constants import ToolStudioPromptKeys as TSPKeys +from prompt_studio.prompt_studio_core.document_indexing_service import ( + DocumentIndexingService, +) from prompt_studio.prompt_studio_core.exceptions import ( AnswerFetchError, DefaultProfileError, @@ -344,6 +350,7 @@ def index_document( is_summary=is_summary, reindex=True, run_id=run_id, + user_id=user_id, ) logger.info(f"[{tool_id}] Indexing successful for doc: {file_name}") @@ -354,7 +361,7 @@ def index_document( "Indexing successful", ) - return doc_id + return doc_id.get("output") @staticmethod def prompt_responder( @@ -364,6 +371,7 @@ def prompt_responder( document_id: str, id: Optional[str] = None, run_id: str = None, + profile_manager_id: Optional[str] = None, ) -> Any: """Execute chain/single run of the prompts. Makes a call to prompt service and returns the dict of response. @@ -374,6 +382,7 @@ def prompt_responder( user_id (str): User's ID document_id (str): UUID of the document uploaded id (Optional[str]): ID of the prompt + profile_manager_id (Optional[str]): UUID of the profile manager Raises: AnswerFetchError: Error from prompt-service @@ -383,44 +392,94 @@ def prompt_responder( """ document: DocumentManager = DocumentManager.objects.get(pk=document_id) doc_name: str = document.document_name - - doc_path = FileManagerHelper.handle_sub_directory_for_tenants( - org_id=org_id, - user_id=user_id, - tool_id=tool_id, - is_create=False, + doc_path = PromptStudioHelper._get_document_path( + org_id, user_id, tool_id, doc_name ) - doc_path = str(Path(doc_path) / doc_name) if id: - prompt_instance = PromptStudioHelper._fetch_prompt_from_id(id) - prompt_name = prompt_instance.prompt_key - logger.info(f"[{tool_id}] Executing single prompt {id}") - PromptStudioHelper._publish_log( - { - "tool_id": tool_id, - "run_id": run_id, - "prompt_key": prompt_name, - "doc_name": doc_name, - }, - LogLevels.INFO, - LogLevels.RUN, - "Executing single prompt", + return PromptStudioHelper._execute_single_prompt( + id, + doc_path, + doc_name, + tool_id, + org_id, + user_id, + document_id, + run_id, + profile_manager_id, + ) + else: + return PromptStudioHelper._execute_prompts_in_single_pass( + doc_path, tool_id, org_id, user_id, document_id, run_id ) - prompts: list[ToolStudioPrompt] = [] - prompts.append(prompt_instance) - tool: CustomTool = prompt_instance.tool_id + @staticmethod + def _execute_single_prompt( + id, + doc_path, + doc_name, + tool_id, + org_id, + user_id, + document_id, + run_id, + profile_manager_id, + ): + prompt_instance = PromptStudioHelper._fetch_prompt_from_id(id) + prompt_name = prompt_instance.prompt_key + PromptStudioHelper._publish_log( + { + "tool_id": tool_id, + "run_id": run_id, + "prompt_key": prompt_name, + "doc_name": doc_name, + }, + LogLevels.INFO, + LogLevels.RUN, + "Executing single prompt", + ) + prompts = [prompt_instance] + tool = prompt_instance.tool_id - if tool.summarize_as_source: - directory, filename = os.path.split(doc_path) - doc_path = os.path.join( - directory, - TSPKeys.SUMMARIZE, - os.path.splitext(filename)[0] + ".txt", - ) + if tool.summarize_as_source: + directory, filename = os.path.split(doc_path) + doc_path = os.path.join( + directory, TSPKeys.SUMMARIZE, os.path.splitext(filename)[0] + ".txt" + ) - logger.info(f"[{tool.tool_id}] Invoking prompt service for prompt {id}") + PromptStudioHelper._publish_log( + { + "tool_id": tool_id, + "run_id": run_id, + "prompt_key": prompt_name, + "doc_name": doc_name, + }, + LogLevels.DEBUG, + LogLevels.RUN, + "Invoking prompt service", + ) + + try: + response = PromptStudioHelper._fetch_response( + doc_path=doc_path, + doc_name=doc_name, + tool=tool, + prompt=prompt_instance, + org_id=org_id, + document_id=document_id, + run_id=run_id, + profile_manager_id=profile_manager_id, + user_id=user_id, + ) + return PromptStudioHelper._handle_response( + response, run_id, prompts, document_id, False, profile_manager_id + ) + except Exception as e: + logger.error( + f"[{tool.tool_id}] Error while fetching response for " + f"prompt {id} and doc {document_id}: {e}" + ) + msg = str(e) PromptStudioHelper._publish_log( { "tool_id": tool_id, @@ -428,130 +487,89 @@ def prompt_responder( "prompt_key": prompt_name, "doc_name": doc_name, }, - LogLevels.DEBUG, + LogLevels.ERROR, LogLevels.RUN, - "Invoking prompt service", + msg, ) + raise e - try: - response = PromptStudioHelper._fetch_response( - doc_path=doc_path, - doc_name=doc_name, - tool=tool, - prompt=prompt_instance, - org_id=org_id, - document_id=document_id, - run_id=run_id, - ) + @staticmethod + def _execute_prompts_in_single_pass( + doc_path, tool_id, org_id, user_id, document_id, run_id + ): + prompts = PromptStudioHelper.fetch_prompt_from_tool(tool_id) + prompts = [prompt for prompt in prompts if prompt.prompt_type != TSPKeys.NOTES] + if not prompts: + logger.error(f"[{tool_id or 'NA'}] No prompts found for id: {id}") + raise NoPromptsFound() - OutputManagerHelper.handle_prompt_output_update( - run_id=run_id, - prompts=prompts, - outputs=response["output"], - document_id=document_id, - is_single_pass_extract=False, - ) - # TODO: Review if this catch-all is required - except Exception as e: - logger.error( - f"[{tool.tool_id}] Error while fetching response for " - f"prompt {id} and doc {document_id}: {e}" - ) - msg: str = ( - f"Error while fetching response for " - f"'{prompt_name}' with '{doc_name}'. {e}" - ) - if isinstance(e, AnswerFetchError): - msg = str(e) - PromptStudioHelper._publish_log( - { - "tool_id": tool_id, - "run_id": run_id, - "prompt_key": prompt_name, - "doc_name": doc_name, - }, - LogLevels.ERROR, - LogLevels.RUN, - msg, - ) - raise e + PromptStudioHelper._publish_log( + {"tool_id": tool_id, "run_id": run_id, "prompt_id": str(id)}, + LogLevels.INFO, + LogLevels.RUN, + "Executing prompts in single pass", + ) - logger.info( - f"[{tool.tool_id}] Response fetched successfully for prompt {id}" + try: + tool = prompts[0].tool_id + response = PromptStudioHelper._fetch_single_pass_response( + file_path=doc_path, + tool=tool, + prompts=prompts, + org_id=org_id, + document_id=document_id, + run_id=run_id, + user_id=user_id, + ) + return PromptStudioHelper._handle_response( + response, run_id, prompts, document_id, True + ) + except Exception as e: + logger.error( + f"[{tool.tool_id}] Error while fetching single pass response: {e}" ) PromptStudioHelper._publish_log( { "tool_id": tool_id, "run_id": run_id, - "prompt_key": prompt_name, - "doc_name": doc_name, + "prompt_id": str(id), }, - LogLevels.INFO, - LogLevels.RUN, - "Single prompt execution completed", - ) - - return response - else: - prompts = PromptStudioHelper.fetch_prompt_from_tool(tool_id) - prompts = [ - prompt for prompt in prompts if prompt.prompt_type != TSPKeys.NOTES - ] - if not prompts: - logger.error(f"[{tool_id or 'NA'}] No prompts found for id: {id}") - raise NoPromptsFound() - - logger.info(f"[{tool_id}] Executing prompts in single pass") - PromptStudioHelper._publish_log( - {"tool_id": tool_id, "run_id": run_id, "prompt_id": str(id)}, - LogLevels.INFO, + LogLevels.ERROR, LogLevels.RUN, - "Executing prompts in single pass", + f"Failed to fetch single pass response. {e}", ) + raise e - try: - tool = prompts[0].tool_id - response = PromptStudioHelper._fetch_single_pass_response( - file_path=doc_path, - tool=tool, - prompts=prompts, - org_id=org_id, - document_id=document_id, - run_id=run_id, - ) - - OutputManagerHelper.handle_prompt_output_update( - run_id=run_id, - prompts=prompts, - outputs=response[TSPKeys.OUTPUT], - document_id=document_id, - is_single_pass_extract=True, - ) - except Exception as e: - logger.error( - f"[{tool.tool_id}] Error while fetching single pass response: {e}" # noqa: E501 - ) - PromptStudioHelper._publish_log( - { - "tool_id": tool_id, - "run_id": run_id, - "prompt_id": str(id), - }, - LogLevels.ERROR, - LogLevels.RUN, - f"Failed to fetch single pass response. {e}", - ) - raise e - - logger.info(f"[{tool.tool_id}] Single pass response fetched successfully") - PromptStudioHelper._publish_log( - {"tool_id": tool_id, "run_id": run_id, "prompt_id": str(id)}, - LogLevels.INFO, - LogLevels.RUN, - "Single pass execution completed", - ) + @staticmethod + def _get_document_path(org_id, user_id, tool_id, doc_name): + doc_path = FileManagerHelper.handle_sub_directory_for_tenants( + org_id=org_id, + user_id=user_id, + tool_id=tool_id, + is_create=False, + ) + return str(Path(doc_path) / doc_name) - return response + @staticmethod + def _handle_response( + response, run_id, prompts, document_id, is_single_pass, profile_manager_id=None + ): + if response.get("status") == IndexingStatus.PENDING_STATUS.value: + return { + "status": IndexingStatus.PENDING_STATUS.value, + "message": IndexingStatus.DOCUMENT_BEING_INDEXED.value, + } + + OutputManagerHelper.handle_prompt_output_update( + run_id=run_id, + prompts=prompts, + outputs=response["output"], + document_id=document_id, + is_single_pass_extract=is_single_pass, + profile_manager_id=profile_manager_id, + context=response["metadata"].get("context"), + ) + return response @staticmethod def _fetch_response( @@ -562,6 +580,8 @@ def _fetch_response( org_id: str, document_id: str, run_id: str, + user_id: str, + profile_manager_id: Optional[str] = None, ) -> Any: """Utility function to invoke prompt service. Used internally. @@ -572,6 +592,9 @@ def _fetch_response( prompt (ToolStudioPrompt): ToolStudioPrompt instance to fetch response org_id (str): UUID of the organization document_id (str): UUID of the document + profile_manager_id (Optional[str]): UUID of the profile manager + user_id (str): The ID of the user who uploaded the document + Raises: DefaultProfileError: If no default profile is selected @@ -580,6 +603,14 @@ def _fetch_response( Returns: Any: Output from LLM """ + + # Fetch the ProfileManager instance using the profile_manager_id if provided + profile_manager = prompt.profile_manager + if profile_manager_id: + profile_manager = ProfileManagerHelper.get_profile_manager( + profile_manager_id=profile_manager_id + ) + monitor_llm_instance: Optional[AdapterInstance] = tool.monitor_llm monitor_llm: Optional[str] = None challenge_llm_instance: Optional[AdapterInstance] = tool.challenge_llm @@ -600,28 +631,33 @@ def _fetch_response( challenge_llm = str(default_profile.llm.id) # Need to check the user who created profile manager - PromptStudioHelper.validate_adapter_status(prompt.profile_manager) + PromptStudioHelper.validate_adapter_status(profile_manager) # Need to check the user who created profile manager # has access to adapters - PromptStudioHelper.validate_profile_manager_owner_access(prompt.profile_manager) + PromptStudioHelper.validate_profile_manager_owner_access(profile_manager) # Not checking reindex here as there might be # change in Profile Manager - vector_db = str(prompt.profile_manager.vector_store.id) - embedding_model = str(prompt.profile_manager.embedding_model.id) - llm = str(prompt.profile_manager.llm.id) - x2text = str(prompt.profile_manager.x2text.id) - prompt_profile_manager: ProfileManager = prompt.profile_manager - if not prompt_profile_manager: + vector_db = str(profile_manager.vector_store.id) + embedding_model = str(profile_manager.embedding_model.id) + llm = str(profile_manager.llm.id) + x2text = str(profile_manager.x2text.id) + if not profile_manager: raise DefaultProfileError() - PromptStudioHelper.dynamic_indexer( - profile_manager=prompt_profile_manager, + index_result = PromptStudioHelper.dynamic_indexer( + profile_manager=profile_manager, file_path=doc_path, tool_id=str(tool.tool_id), org_id=org_id, document_id=document_id, is_summary=tool.summarize_as_source, run_id=run_id, + user_id=user_id, ) + if index_result.get("status") == IndexingStatus.PENDING_STATUS.value: + return { + "status": IndexingStatus.PENDING_STATUS.value, + "message": IndexingStatus.DOCUMENT_BEING_INDEXED.value, + } output: dict[str, Any] = {} outputs: list[dict[str, Any]] = [] @@ -639,16 +675,16 @@ def _fetch_response( output[TSPKeys.PROMPT] = prompt.prompt output[TSPKeys.ACTIVE] = prompt.active - output[TSPKeys.CHUNK_SIZE] = prompt.profile_manager.chunk_size + output[TSPKeys.CHUNK_SIZE] = profile_manager.chunk_size output[TSPKeys.VECTOR_DB] = vector_db output[TSPKeys.EMBEDDING] = embedding_model - output[TSPKeys.CHUNK_OVERLAP] = prompt.profile_manager.chunk_overlap + output[TSPKeys.CHUNK_OVERLAP] = profile_manager.chunk_overlap output[TSPKeys.LLM] = llm output[TSPKeys.TYPE] = prompt.enforce_type output[TSPKeys.NAME] = prompt.prompt_key - output[TSPKeys.RETRIEVAL_STRATEGY] = prompt.profile_manager.retrieval_strategy - output[TSPKeys.SIMILARITY_TOP_K] = prompt.profile_manager.similarity_top_k - output[TSPKeys.SECTION] = prompt.profile_manager.section + output[TSPKeys.RETRIEVAL_STRATEGY] = profile_manager.retrieval_strategy + output[TSPKeys.SIMILARITY_TOP_K] = profile_manager.similarity_top_k + output[TSPKeys.SECTION] = profile_manager.section output[TSPKeys.X2TEXT_ADAPTER] = x2text # Eval settings for the prompt output[TSPKeys.EVAL_SETTINGS] = {} @@ -715,10 +751,11 @@ def dynamic_indexer( file_path: str, org_id: str, document_id: str, + user_id: str, is_summary: bool = False, reindex: bool = False, run_id: str = None, - ) -> str: + ) -> Any: """Used to index a file based on the passed arguments. This is useful when a file needs to be indexed dynamically as the @@ -732,6 +769,7 @@ def dynamic_indexer( org_id (str): ID of the organization is_summary (bool, optional): Flag to ensure if extracted contents need to be persisted. Defaults to False. + user_id (str): The ID of the user who uploaded the document Returns: str: Index key for the combination of arguments @@ -750,9 +788,42 @@ def dynamic_indexer( profile_manager.chunk_size = 0 try: + usage_kwargs = {"run_id": run_id} util = PromptIdeBaseTool(log_level=LogLevel.INFO, org_id=org_id) tool_index = Index(tool=util) + doc_id_key = tool_index.generate_file_id( + tool_id=tool_id, + vector_db=vector_db, + embedding=embedding_model, + x2text=x2text_adapter, + chunk_size=str(profile_manager.chunk_size), + chunk_overlap=str(profile_manager.chunk_overlap), + file_path=file_path, + file_hash=None, + ) + if not reindex: + indexed_doc_id = DocumentIndexingService.get_indexed_document_id( + org_id=org_id, user_id=user_id, doc_id_key=doc_id_key + ) + if indexed_doc_id: + return { + "status": IndexingStatus.COMPLETED_STATUS.value, + "output": indexed_doc_id, + } + # Polling if document is already being indexed + if DocumentIndexingService.is_document_indexing( + org_id=org_id, user_id=user_id, doc_id_key=doc_id_key + ): + return { + "status": IndexingStatus.PENDING_STATUS.value, + "output": IndexingStatus.DOCUMENT_BEING_INDEXED.value, + } + + # Set the document as being indexed + DocumentIndexingService.set_document_indexing( + org_id=org_id, user_id=user_id, doc_id_key=doc_id_key + ) doc_id: str = tool_index.index( tool_id=tool_id, embedding_instance_id=embedding_model, @@ -772,7 +843,10 @@ def dynamic_indexer( profile_manager=profile_manager, doc_id=doc_id, ) - return doc_id + DocumentIndexingService.mark_document_indexed( + org_id=org_id, user_id=user_id, doc_id_key=doc_id_key, doc_id=doc_id + ) + return {"status": IndexingStatus.COMPLETED_STATUS.value, "output": doc_id} except (IndexingError, IndexingAPIError, SdkError) as e: doc_name = os.path.split(file_path)[1] PromptStudioHelper._publish_log( @@ -791,6 +865,7 @@ def _fetch_single_pass_response( file_path: str, prompts: list[ToolStudioPrompt], org_id: str, + user_id: str, document_id: str, run_id: str = None, ) -> Any: @@ -819,7 +894,7 @@ def _fetch_single_pass_response( if not default_profile: raise DefaultProfileError() - PromptStudioHelper.dynamic_indexer( + index_result = PromptStudioHelper.dynamic_indexer( profile_manager=default_profile, file_path=file_path, tool_id=tool_id, @@ -827,7 +902,13 @@ def _fetch_single_pass_response( is_summary=tool.summarize_as_source, document_id=document_id, run_id=run_id, + user_id=user_id, ) + if index_result.get("status") == IndexingStatus.PENDING_STATUS.value: + return { + "status": IndexingStatus.PENDING_STATUS.value, + "message": IndexingStatus.DOCUMENT_BEING_INDEXED.value, + } vector_db = str(default_profile.vector_store.id) embedding_model = str(default_profile.embedding_model.id) diff --git a/backend/prompt_studio/prompt_studio_core/views.py b/backend/prompt_studio/prompt_studio_core/views.py index 9093efd42..8db0a3ef5 100644 --- a/backend/prompt_studio/prompt_studio_core/views.py +++ b/backend/prompt_studio/prompt_studio_core/views.py @@ -21,6 +21,9 @@ ToolStudioKeys, ToolStudioPromptKeys, ) +from prompt_studio.prompt_studio_core.document_indexing_service import ( + DocumentIndexingService, +) from prompt_studio.prompt_studio_core.exceptions import ( IndexingAPIError, ToolDeleteError, @@ -30,6 +33,7 @@ from prompt_studio.prompt_studio_document_manager.prompt_studio_document_helper import ( # noqa: E501 PromptStudioDocumentHelper, ) +from prompt_studio.prompt_studio_index_manager.models import IndexManager from prompt_studio.prompt_studio_registry.prompt_studio_registry_helper import ( PromptStudioRegistryHelper, ) @@ -264,6 +268,7 @@ def fetch_response(self, request: HttpRequest, pk: Any = None) -> Response: document_id: str = request.data.get(ToolStudioPromptKeys.DOCUMENT_ID) id: str = request.data.get(ToolStudioPromptKeys.ID) run_id: str = request.data.get(ToolStudioPromptKeys.RUN_ID) + profile_manager: str = request.data.get(ToolStudioPromptKeys.PROFILE_MANAGER_ID) if not run_id: # Generate a run_id run_id = CommonUtils.generate_uuid() @@ -275,6 +280,7 @@ def fetch_response(self, request: HttpRequest, pk: Any = None) -> Response: user_id=custom_tool.created_by.user_id, document_id=document_id, run_id=run_id, + profile_manager_id=profile_manager, ) return Response(response, status=status.HTTP_200_OK) @@ -446,17 +452,26 @@ def delete_for_ide(self, request: HttpRequest, pk: uuid) -> Response: document_id: str = serializer.validated_data.get( ToolStudioPromptKeys.DOCUMENT_ID ) + org_id = UserSessionUtils.get_organization_id(request) + user_id = custom_tool.created_by.user_id document: DocumentManager = DocumentManager.objects.get(pk=document_id) file_name: str = document.document_name file_path = FileManagerHelper.handle_sub_directory_for_tenants( - UserSessionUtils.get_organization_id(request), + org_id=org_id, is_create=False, - user_id=custom_tool.created_by.user_id, + user_id=user_id, tool_id=str(custom_tool.tool_id), ) path = file_path file_system = LocalStorageFS(settings={"path": path}) try: + # Delete indexed flags in redis + index_managers = IndexManager.objects.filter(document_manager=document_id) + for index_manager in index_managers: + raw_index_id = index_manager.raw_index_id + DocumentIndexingService.remove_document_indexing( + org_id=org_id, user_id=user_id, doc_id_key=raw_index_id + ) # Delete the document record document.delete() # Delete the files diff --git a/backend/prompt_studio/prompt_studio_output_manager/migrations/0013_promptstudiooutputmanager_context.py b/backend/prompt_studio/prompt_studio_output_manager/migrations/0013_promptstudiooutputmanager_context.py new file mode 100644 index 000000000..9d72dbd4d --- /dev/null +++ b/backend/prompt_studio/prompt_studio_output_manager/migrations/0013_promptstudiooutputmanager_context.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.1 on 2024-06-27 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("prompt_studio_output_manager", "0012_promptstudiooutputmanager_run_id"), + ] + + operations = [ + migrations.AddField( + model_name="promptstudiooutputmanager", + name="context", + field=models.CharField( + blank=True, db_comment="Field to store chucks used", null=True + ), + ), + ] diff --git a/backend/prompt_studio/prompt_studio_output_manager/migrations/0014_alter_promptstudiooutputmanager_context.py b/backend/prompt_studio/prompt_studio_output_manager/migrations/0014_alter_promptstudiooutputmanager_context.py new file mode 100644 index 000000000..9d7844eaa --- /dev/null +++ b/backend/prompt_studio/prompt_studio_output_manager/migrations/0014_alter_promptstudiooutputmanager_context.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.1 on 2024-06-30 17:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("prompt_studio_output_manager", "0013_promptstudiooutputmanager_context"), + ] + + operations = [ + migrations.AlterField( + model_name="promptstudiooutputmanager", + name="context", + field=models.TextField( + blank=True, db_comment="Field to store chunks used", null=True + ), + ), + ] diff --git a/backend/prompt_studio/prompt_studio_output_manager/models.py b/backend/prompt_studio/prompt_studio_output_manager/models.py index 14febf634..e1f7f5b86 100644 --- a/backend/prompt_studio/prompt_studio_output_manager/models.py +++ b/backend/prompt_studio/prompt_studio_output_manager/models.py @@ -21,6 +21,9 @@ class PromptStudioOutputManager(BaseModel): output = models.CharField( db_comment="Field to store output", editable=True, null=True, blank=True ) + context = models.TextField( + db_comment="Field to store chunks used", editable=True, null=True, blank=True + ) eval_metrics = models.JSONField( db_column="eval_metrics", null=False, diff --git a/backend/prompt_studio/prompt_studio_output_manager/output_manager_helper.py b/backend/prompt_studio/prompt_studio_output_manager/output_manager_helper.py index 6f942e3b7..b88a25602 100644 --- a/backend/prompt_studio/prompt_studio_output_manager/output_manager_helper.py +++ b/backend/prompt_studio/prompt_studio_output_manager/output_manager_helper.py @@ -1,10 +1,14 @@ import json import logging -from typing import Any +from typing import Any, Optional from prompt_studio.prompt_profile_manager.models import ProfileManager -from prompt_studio.prompt_studio.exceptions import AnswerFetchError from prompt_studio.prompt_studio.models import ToolStudioPrompt +from prompt_studio.prompt_studio_core.exceptions import ( + AnswerFetchError, + DefaultProfileError, +) +from prompt_studio.prompt_studio_core.models import CustomTool from prompt_studio.prompt_studio_document_manager.models import DocumentManager from prompt_studio.prompt_studio_output_manager.constants import ( PromptStudioOutputManagerKeys as PSOMKeys, @@ -20,42 +24,32 @@ def handle_prompt_output_update( run_id: str, prompts: list[ToolStudioPrompt], outputs: Any, + context: Any, document_id: str, is_single_pass_extract: bool, + profile_manager_id: Optional[str] = None, ) -> None: """Handles updating prompt outputs in the database. Args: + run_id (str): ID of the run. prompts (list[ToolStudioPrompt]): List of prompts to update. outputs (Any): Outputs corresponding to the prompts. document_id (str): ID of the document. + profile_manager_id (Optional[str]): UUID of the profile manager. is_single_pass_extract (bool): Flag indicating if single pass extract is active. """ - # Check if prompts list is empty - if not prompts: - return # Return early if prompts list is empty - tool = prompts[0].tool_id - document_manager = DocumentManager.objects.get(pk=document_id) - default_profile = ProfileManager.get_default_llm_profile(tool=tool) - # Iterate through each prompt in the list - for prompt in prompts: - if prompt.prompt_type == PSOMKeys.NOTES: - continue - if is_single_pass_extract: - profile_manager = default_profile - else: - profile_manager = prompt.profile_manager - output = json.dumps(outputs.get(prompt.prompt_key)) - eval_metrics = outputs.get(f"{prompt.prompt_key}__evaluation", []) - - # Attempt to update an existing output manager, - # for the given criteria, - # or create a new one if it doesn't exist + def update_or_create_prompt_output( + prompt: ToolStudioPrompt, + profile_manager: ProfileManager, + output: str, + eval_metrics: list[Any], + tool: CustomTool, + context: str, + ): try: - # Create or get the existing record for this document, prompt and - # profile combo _, success = PromptStudioOutputManager.objects.get_or_create( document_manager=document_manager, tool_id=tool, @@ -65,6 +59,7 @@ def handle_prompt_output_update( defaults={ "output": output, "eval_metrics": eval_metrics, + "context": context, }, ) @@ -79,11 +74,12 @@ def handle_prompt_output_update( f"profile {profile_manager.profile_id}" ) - args: dict[str, str] = dict() - args["run_id"] = run_id - args["output"] = output - args["eval_metrics"] = eval_metrics - # Update the record with the run id and other params + args: dict[str, str] = { + "run_id": run_id, + "output": output, + "eval_metrics": eval_metrics, + "context": context, + } PromptStudioOutputManager.objects.filter( document_manager=document_manager, tool_id=tool, @@ -94,3 +90,57 @@ def handle_prompt_output_update( except Exception as e: raise AnswerFetchError(f"Error updating prompt output {e}") from e + + if not prompts: + return # Return early if prompts list is empty + + tool = prompts[0].tool_id + default_profile = OutputManagerHelper.get_default_profile( + profile_manager_id, tool + ) + document_manager = DocumentManager.objects.get(pk=document_id) + + for prompt in prompts: + if prompt.prompt_type == PSOMKeys.NOTES or not prompt.active: + continue + + if not is_single_pass_extract: + context = json.dumps(context.get(prompt.prompt_key)) + + output = json.dumps(outputs.get(prompt.prompt_key)) + profile_manager = default_profile + eval_metrics = outputs.get(f"{prompt.prompt_key}__evaluation", []) + + update_or_create_prompt_output( + prompt=prompt, + profile_manager=profile_manager, + output=output, + eval_metrics=eval_metrics, + tool=tool, + context=context, + ) + + @staticmethod + def get_default_profile( + profile_manager_id: Optional[str], tool: CustomTool + ) -> ProfileManager: + if profile_manager_id: + return OutputManagerHelper.fetch_profile_manager(profile_manager_id) + else: + return OutputManagerHelper.fetch_default_llm_profile(tool) + + @staticmethod + def fetch_profile_manager(profile_manager_id: str) -> ProfileManager: + try: + return ProfileManager.objects.get(profile_id=profile_manager_id) + except ProfileManager.DoesNotExist: + raise DefaultProfileError( + f"ProfileManager with ID {profile_manager_id} does not exist." + ) + + @staticmethod + def fetch_default_llm_profile(tool: CustomTool) -> ProfileManager: + try: + return ProfileManager.get_default_llm_profile(tool=tool) + except DefaultProfileError: + raise DefaultProfileError("Default ProfileManager does not exist.") diff --git a/backend/sample.env b/backend/sample.env index abbc1757c..f42a32068 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -139,3 +139,6 @@ LOGS_BATCH_LIMIT=30 # Celery Configuration CELERY_BROKER_URL = "redis://unstract-redis:6379" + +# Indexing flag to prevent re-index +INDEXING_FLAG_TTL=1800 diff --git a/backend/usage/constants.py b/backend/usage/constants.py index d28074e1e..8da54da05 100644 --- a/backend/usage/constants.py +++ b/backend/usage/constants.py @@ -4,3 +4,4 @@ class UsageKeys: PROMPT_TOKENS = "prompt_tokens" COMPLETION_TOKENS = "completion_tokens" TOTAL_TOKENS = "total_tokens" + COST_IN_DOLLARS = "cost_in_dollars" diff --git a/backend/usage/helper.py b/backend/usage/helper.py index b91fae556..0bfab7556 100644 --- a/backend/usage/helper.py +++ b/backend/usage/helper.py @@ -36,6 +36,7 @@ def get_aggregated_token_count(run_id: str) -> dict: prompt_tokens=Sum(UsageKeys.PROMPT_TOKENS), completion_tokens=Sum(UsageKeys.COMPLETION_TOKENS), total_tokens=Sum(UsageKeys.TOTAL_TOKENS), + cost_in_dollars=Sum(UsageKeys.COST_IN_DOLLARS), ) logger.info(f"Token counts aggregated successfully for run_id: {run_id}") @@ -50,6 +51,7 @@ def get_aggregated_token_count(run_id: str) -> dict: UsageKeys.COMPLETION_TOKENS ), UsageKeys.TOTAL_TOKENS: usage_summary.get(UsageKeys.TOTAL_TOKENS), + UsageKeys.COST_IN_DOLLARS: usage_summary.get(UsageKeys.COST_IN_DOLLARS), } return result except Usage.DoesNotExist: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aee9ec808..018771100 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,6 +25,7 @@ "cronstrue": "^2.48.0", "emoji-picker-react": "^4.8.0", "emoji-regex": "^10.3.0", + "framer-motion": "^11.2.10", "handlebars": "^4.7.8", "http-proxy-middleware": "^2.0.6", "js-cookie": "^3.0.5", @@ -9383,6 +9384,30 @@ "url": "https://www.patreon.com/infusion" } }, + "node_modules/framer-motion": { + "version": "11.2.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.10.tgz", + "integrity": "sha512-/gr3PLZUVFCc86a9MqCUboVrALscrdluzTb3yew+2/qKBU8CX6nzs918/SRBRCqaPbx0TZP10CB6yFgK2C5cYQ==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -27235,6 +27260,14 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" }, + "framer-motion": { + "version": "11.2.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.10.tgz", + "integrity": "sha512-/gr3PLZUVFCc86a9MqCUboVrALscrdluzTb3yew+2/qKBU8CX6nzs918/SRBRCqaPbx0TZP10CB6yFgK2C5cYQ==", + "requires": { + "tslib": "^2.4.0" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index a01f53738..674ef6689 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "cronstrue": "^2.48.0", "emoji-picker-react": "^4.8.0", "emoji-regex": "^10.3.0", + "framer-motion": "^11.2.10", "handlebars": "^4.7.8", "http-proxy-middleware": "^2.0.6", "js-cookie": "^3.0.5", diff --git a/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx b/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx index 4b0cb5281..980a80916 100644 --- a/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx +++ b/frontend/src/components/custom-tools/combined-output/CombinedOutput.jsx @@ -7,6 +7,7 @@ import PropTypes from "prop-types"; import { displayPromptResult, + getLLMModelNamesForProfiles, promptType, } from "../../../helpers/GetStaticData"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; @@ -31,18 +32,25 @@ try { function CombinedOutput({ docId, setFilledFields }) { const [combinedOutput, setCombinedOutput] = useState({}); const [isOutputLoading, setIsOutputLoading] = useState(false); + const [adapterData, setAdapterData] = useState([]); + const [activeKey, setActiveKey] = useState("0"); const { details, defaultLlmProfile, singlePassExtractMode, isSinglePassExtractLoading, + llmProfiles, isSimplePromptStudio, } = useCustomToolStore(); const { sessionDetails } = useSessionStore(); const { setAlertDetails } = useAlertStore(); const axiosPrivate = useAxiosPrivate(); const handleException = useExceptionHandler(); + const [selectedProfile, setSelectedProfile] = useState(defaultLlmProfile); + useEffect(() => { + getAdapterInfo(); + }, []); useEffect(() => { if (!docId || isSinglePassExtractLoading) { return; @@ -62,7 +70,7 @@ function CombinedOutput({ docId, setFilledFields }) { } output[item?.prompt_key] = ""; - let profileManager = item?.profile_manager; + let profileManager = selectedProfile || item?.profile_manager; if (singlePassExtractMode) { profileManager = defaultLlmProfile; } @@ -100,12 +108,25 @@ function CombinedOutput({ docId, setFilledFields }) { .finally(() => { setIsOutputLoading(false); }); - }, [docId, singlePassExtractMode, isSinglePassExtractLoading]); + }, [ + docId, + singlePassExtractMode, + isSinglePassExtractLoading, + selectedProfile, + ]); const handleOutputApiRequest = async () => { - let url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt-output/?tool_id=${details?.tool_id}&document_manager=${docId}&is_single_pass_extract=${singlePassExtractMode}`; + let url; if (isSimplePromptStudio) { url = promptOutputApiSps(details?.tool_id, null, docId); + } else { + url = `/api/v1/unstract/${ + sessionDetails?.orgId + }/prompt-studio/prompt-output/?tool_id=${ + details?.tool_id + }&document_manager=${docId}&is_single_pass_extract=${singlePassExtractMode}&profile_manager=${ + selectedProfile || defaultLlmProfile + }`; } const requestOptions = { method: "GET", @@ -122,15 +143,43 @@ function CombinedOutput({ docId, setFilledFields }) { }); }; + const getAdapterInfo = () => { + axiosPrivate + .get( + `/api/v1/unstract/${sessionDetails?.orgId}/adapter/?adapter_type=LLM` + ) + .then((res) => { + const adapterList = res?.data; + setAdapterData(getLLMModelNamesForProfiles(llmProfiles, adapterList)); + }); + }; + if (isOutputLoading) { return ; } + const handleTabChange = (key) => { + if (key === "0") { + setSelectedProfile(defaultLlmProfile); + } else { + setSelectedProfile(adapterData[key - 1]?.profile_id); + } + setActiveKey(key); + }; + if (isSimplePromptStudio && TableView) { return ; } - return ; + return ( + + ); } CombinedOutput.propTypes = { diff --git a/frontend/src/components/custom-tools/combined-output/JsonView.jsx b/frontend/src/components/custom-tools/combined-output/JsonView.jsx index 4ad2fa7a1..61bfc9f46 100644 --- a/frontend/src/components/custom-tools/combined-output/JsonView.jsx +++ b/frontend/src/components/custom-tools/combined-output/JsonView.jsx @@ -1,8 +1,18 @@ import PropTypes from "prop-types"; import Prism from "prismjs"; import { useEffect } from "react"; +import { ProfileInfoBar } from "../profile-info-bar/ProfileInfoBar"; +import TabPane from "antd/es/tabs/TabPane"; +import { Tabs } from "antd"; -function JsonView({ combinedOutput }) { +function JsonView({ + combinedOutput, + handleTabChange, + adapterData, + activeKey, + selectedProfile, + llmProfiles, +}) { useEffect(() => { Prism.highlightAll(); }, [combinedOutput]); @@ -10,9 +20,19 @@ function JsonView({ combinedOutput }) { return (
+ }> + Default} key={"0"}> + {adapterData.map((adapter, index) => ( + {adapter.llm_model}} + key={(index + 1)?.toString()} + /> + ))} +
+
{combinedOutput && (
@@ -29,6 +49,11 @@ function JsonView({ combinedOutput }) {
 
 JsonView.propTypes = {
   combinedOutput: PropTypes.object.isRequired,
+  handleTabChange: PropTypes.func,
+  adapterData: PropTypes.array,
+  selectedProfile: PropTypes.string,
+  llmProfiles: PropTypes.array,
+  activeKey: PropTypes.string,
 };
 
 export { JsonView };
diff --git a/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx b/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx
index cc4d6d6f8..23de585e2 100644
--- a/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx
+++ b/frontend/src/components/custom-tools/output-for-doc-modal/OutputForDocModal.jsx
@@ -1,4 +1,4 @@
-import { Button, Modal, Table, Typography } from "antd";
+import { Button, Modal, Table, Tabs, Typography } from "antd";
 import PropTypes from "prop-types";
 import { useEffect, useState } from "react";
 import {
@@ -12,12 +12,17 @@ import { useCustomToolStore } from "../../../store/custom-tool-store";
 import { useSessionStore } from "../../../store/session-store";
 import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate";
 import "./OutputForDocModal.css";
-import { displayPromptResult } from "../../../helpers/GetStaticData";
+import {
+  displayPromptResult,
+  getLLMModelNamesForProfiles,
+} from "../../../helpers/GetStaticData";
 import { SpinnerLoader } from "../../widgets/spinner-loader/SpinnerLoader";
 import { useAlertStore } from "../../../store/alert-store";
 import { useExceptionHandler } from "../../../hooks/useExceptionHandler";
 import { TokenUsage } from "../token-usage/TokenUsage";
 import { useTokenUsageStore } from "../../../store/token-usage-store";
+import TabPane from "antd/es/tabs/TabPane";
+import { ProfileInfoBar } from "../profile-info-bar/ProfileInfoBar";
 
 const columns = [
   {
@@ -57,6 +62,7 @@ function OutputForDocModal({
 }) {
   const [promptOutputs, setPromptOutputs] = useState([]);
   const [rows, setRows] = useState([]);
+  const [adapterData, setAdapterData] = useState([]);
   const [isLoading, setIsLoading] = useState(false);
   const {
     details,
@@ -66,6 +72,7 @@ function OutputForDocModal({
     disableLlmOrDocChange,
     singlePassExtractMode,
     isSinglePassExtractLoading,
+    llmProfiles,
   } = useCustomToolStore();
   const { sessionDetails } = useSessionStore();
   const axiosPrivate = useAxiosPrivate();
@@ -73,12 +80,14 @@ function OutputForDocModal({
   const { setAlertDetails } = useAlertStore();
   const { handleException } = useExceptionHandler();
   const { tokenUsage } = useTokenUsageStore();
+  const [selectedProfile, setSelectedProfile] = useState(defaultLlmProfile);
 
   useEffect(() => {
     if (!open) {
       return;
     }
     handleGetOutputForDocs();
+    getAdapterInfo();
   }, [open, singlePassExtractMode, isSinglePassExtractLoading]);
 
   useEffect(() => {
@@ -89,6 +98,12 @@ function OutputForDocModal({
     handleRowsGeneration(promptOutputs);
   }, [promptOutputs, tokenUsage]);
 
+  useEffect(() => {
+    if (selectedProfile) {
+      handleGetOutputForDocs(selectedProfile);
+    }
+  }, [selectedProfile]);
+
   const moveSelectedDocToTop = () => {
     // Create a copy of the list of documents
     const docs = [...listOfDocs];
@@ -147,8 +162,16 @@ function OutputForDocModal({
     });
   };
 
-  const handleGetOutputForDocs = () => {
-    let profile = profileManagerId;
+  const getAdapterInfo = () => {
+    axiosPrivate
+      .get(`/api/v1/unstract/${sessionDetails.orgId}/adapter/?adapter_type=LLM`)
+      .then((res) => {
+        const adapterList = res.data;
+        setAdapterData(getLLMModelNamesForProfiles(llmProfiles, adapterList));
+      });
+  };
+
+  const handleGetOutputForDocs = (profile = profileManagerId) => {
     if (singlePassExtractMode) {
       profile = defaultLlmProfile;
     }
@@ -206,10 +229,14 @@ function OutputForDocModal({
       }
 
       const result = {
-        key: item,
+        key: item?.document_id,
         document: item?.document_name,
         token_count: (
-          
+          
         ),
         value: (
           <>
@@ -239,6 +266,14 @@ function OutputForDocModal({
     setRows(rowsData);
   };
 
+  const handleTabChange = (key) => {
+    if (key === "0") {
+      setSelectedProfile(profileManagerId);
+    } else {
+      setSelectedProfile(adapterData[key - 1]?.profile_id);
+    }
+  };
+
   return (
     
         
+
+ + Default} key={"0"}> + {adapterData?.map((adapter, index) => ( + {adapter?.llm_model}} + key={(index + 1)?.toString()} + > + ))} + {" "} + +
+ ), + spinning: isLoading, + }} />
diff --git a/frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.css b/frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.css new file mode 100644 index 000000000..8346c26d7 --- /dev/null +++ b/frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.css @@ -0,0 +1,3 @@ +.profile-info-bar { + margin-bottom: 10px; +} diff --git a/frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.jsx b/frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.jsx new file mode 100644 index 000000000..8f52a68a4 --- /dev/null +++ b/frontend/src/components/custom-tools/profile-info-bar/ProfileInfoBar.jsx @@ -0,0 +1,58 @@ +import { Col, Row, Tag } from "antd"; +import PropTypes from "prop-types"; +import "./ProfileInfoBar.css"; + +const ProfileInfoBar = ({ profiles, profileId }) => { + const profile = profiles?.find((p) => p?.profile_id === profileId); + + if (!profile) { + return

Profile not found

; + } + + return ( + + + + Profile Name: {profile?.profile_name} + + + + + Chunk Size: {profile?.chunk_size} + + + + + Vector Store: {profile?.vector_store} + + + + + Embedding Model: {profile?.embedding_model} + + + + + LLM: {profile?.llm} + + + + + X2Text: {profile?.x2text} + + + + + Reindex: {profile?.reindex ? "Yes" : "No"} + + + + ); +}; + +ProfileInfoBar.propTypes = { + profiles: PropTypes.array, + profileId: PropTypes.string, +}; + +export { ProfileInfoBar }; diff --git a/frontend/src/components/custom-tools/prompt-card/Header.jsx b/frontend/src/components/custom-tools/prompt-card/Header.jsx index 35ac9d5aa..954b5706d 100644 --- a/frontend/src/components/custom-tools/prompt-card/Header.jsx +++ b/frontend/src/components/custom-tools/prompt-card/Header.jsx @@ -3,10 +3,12 @@ import { DeleteOutlined, EditOutlined, LoadingOutlined, + PlayCircleFilled, PlayCircleOutlined, SyncOutlined, } from "@ant-design/icons"; -import { Button, Col, Row, Tag, Tooltip } from "antd"; +import { useState } from "react"; +import { Button, Checkbox, Col, Divider, Row, Tag, Tooltip } from "antd"; import PropTypes from "prop-types"; import { promptStudioUpdateStatus } from "../../../helpers/GetStaticData"; @@ -31,6 +33,7 @@ function Header({ enableEdit, expandCard, setExpandCard, + enabledProfiles, }) { const { selectedDoc, @@ -40,9 +43,21 @@ function Header({ indexDocs, } = useCustomToolStore(); - const handleRunBtnClick = () => { + const [isDisablePrompt, setIsDisablePrompt] = useState(promptDetails?.active); + + const handleRunBtnClick = (profileManager = null, coverAllDoc = true) => { setExpandCard(true); - handleRun(); + handleRun(profileManager, coverAllDoc, enabledProfiles, true); + }; + + const handleDisablePrompt = (event) => { + const check = event?.target?.checked; + setIsDisablePrompt(check); + handleChange(check, promptDetails?.prompt_id, "active", true, true).catch( + () => { + setIsDisablePrompt(!check); + } + ); }; return ( @@ -122,24 +137,51 @@ function Header({ {!singlePassExtractMode && ( - - - + <> + + + + + + + )} + + handleDelete(promptDetails?.prompt_id)} content="The prompt will be permanently deleted." @@ -150,9 +192,9 @@ function Header({ type="text" className="prompt-card-action-button" disabled={ - disableLlmOrDocChange.includes(promptDetails?.prompt_id) || + disableLlmOrDocChange?.includes(promptDetails?.prompt_id) || isSinglePassExtractLoading || - indexDocs.includes(selectedDoc?.document_id) + indexDocs?.includes(selectedDoc?.document_id) } > @@ -180,6 +222,7 @@ Header.propTypes = { enableEdit: PropTypes.func.isRequired, expandCard: PropTypes.bool.isRequired, setExpandCard: PropTypes.func.isRequired, + enabledProfiles: PropTypes.array.isRequired, }; export { Header }; diff --git a/frontend/src/components/custom-tools/prompt-card/OutputForIndex.jsx b/frontend/src/components/custom-tools/prompt-card/OutputForIndex.jsx new file mode 100644 index 000000000..1f29cc8a1 --- /dev/null +++ b/frontend/src/components/custom-tools/prompt-card/OutputForIndex.jsx @@ -0,0 +1,50 @@ +import PropTypes from "prop-types"; +import { Modal } from "antd"; +import "./PromptCard.css"; +import { uniqueId } from "lodash"; + +function OutputForIndex({ chunkData, setIsIndexOpen, isIndexOpen }) { + const handleClose = () => { + setIsIndexOpen(false); + }; + + const lines = chunkData?.split("\\n"); // Split text into lines and remove any empty lines + + const renderContent = (chunk) => { + if (!chunk) { + return

No chunks founds

; + } + return ( + <> + {chunk?.map((line) => ( +
+ {line} +
+
+ ))} + + ); + }; + + return ( + +
{renderContent(lines)}
+
+ ); +} + +OutputForIndex.propTypes = { + chunkData: PropTypes.string, + isIndexOpen: PropTypes.bool.isRequired, + setIsIndexOpen: PropTypes.func.isRequired, +}; + +export { OutputForIndex }; diff --git a/frontend/src/components/custom-tools/prompt-card/PromptCard.css b/frontend/src/components/custom-tools/prompt-card/PromptCard.css index a57d12fd8..d190564f2 100644 --- a/frontend/src/components/custom-tools/prompt-card/PromptCard.css +++ b/frontend/src/components/custom-tools/prompt-card/PromptCard.css @@ -2,6 +2,7 @@ .prompt-card { border: 1px solid #d9d9d9; + border-radius: 0; } .prompt-card .ant-card-body { @@ -20,10 +21,6 @@ background-color: #eceff3; } -.prompt-card-rad { - border-radius: 8px 8px 0px 0px; -} - .prompt-card-head-info-icon { color: #575859; } @@ -61,8 +58,11 @@ background-color: #f5f7f9; } -.prompt-card-comp-layout-border { - border-radius: 0px 0px 10px 10px; +.prompt-card-llm-layout { + width: 100%; + padding: 8px 12px; + background-color: #f5f7f9; + row-gap: 2; } .prompt-card-actions-dropdowns { @@ -76,7 +76,10 @@ .prompt-card-result { padding-top: 12px; background-color: #fff8e6; - border-radius: 0px 0px 8px 8px; + display: flex; + justify-content: space-between; + align-items: center; + width: -webkit-fill-available; } .prompt-card-result .ant-typography { @@ -84,7 +87,8 @@ } .prompt-card-res { - white-space: pre-wrap; + min-width: 0; + flex-basis: 60%; } .prompt-card-select-type { @@ -116,3 +120,86 @@ .prompt-card-collapse .ant-collapse-content-box { padding: 0px !important; } + +.llm-info { + display: flex; + align-items: center; +} + +.prompt-card-llm-title { + margin: 0 0 0 10px !important; +} + +.prompt-card-llm-icon { + display: flex; + justify-content: center; +} + +.prompt-cost-item { + font-size: 12px; + margin-right: 10px; +} + +.prompt-info { + display: flex; + justify-content: space-between; +} + +.llm-info-container > * { + margin-left: 10px; +} + +.prompt-card-llm-container { + border-right: 1px solid #0000000f; + width: fill-available !important; +} + +.prompt-profile-run-expanded { + flex-direction: column; + align-items: flex-start; +} + +.prompt-card-llm { + flex: 1; + min-width: 250px; +} + +.collapsed-output { + max-height: 20px; /* Adjust height as necessary */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.expanded-output { + max-height: 250px; /* Adjust height as necessary */ + overflow-y: auto; +} + +.prompt-profile-run { + align-self: flex-end; + margin-left: 10px; +} + +.index-output-tab { + overflow: scroll; + height: 60vh; +} + +.header-delete-divider { + margin: auto 2px auto 10px; + border: 1px solid rgba(5, 5, 5, 0.1); + height: 20px; +} + +.ant-tag-checkable.checked { + background-color: #f6ffed !important; + border-color: #b7eb8f !important; + color: #52c41a !important; +} + +.ant-tag-checkable.unchecked { + background-color: #00000005 !important; + border-color: #00000026 !important; + color: #000 !important; +} diff --git a/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx b/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx index a6efe92b1..6a67d9939 100644 --- a/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx +++ b/frontend/src/components/custom-tools/prompt-card/PromptCard.jsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { defaultTokenUsage, generateUUID, + pollForCompletion, } from "../../../helpers/GetStaticData"; import { useAxiosPrivate } from "../../../hooks/useAxiosPrivate"; import { useExceptionHandler } from "../../../hooks/useExceptionHandler"; @@ -42,22 +43,19 @@ function PromptCard({ updatePlaceHolder, }) { const [enforceTypeList, setEnforceTypeList] = useState([]); - const [page, setPage] = useState(0); - const [isRunLoading, setIsRunLoading] = useState(false); + const [isRunLoading, setIsRunLoading] = useState({}); const [promptKey, setPromptKey] = useState(""); const [promptText, setPromptText] = useState(""); const [selectedLlmProfileId, setSelectedLlmProfileId] = useState(null); const [openEval, setOpenEval] = useState(false); - const [result, setResult] = useState({ - promptOutputId: null, - output: "", - }); - const [coverage, setCoverage] = useState(0); + const [result, setResult] = useState([]); + const [coverage, setCoverage] = useState({}); const [coverageTotal, setCoverageTotal] = useState(0); const [isCoverageLoading, setIsCoverageLoading] = useState(false); const [openOutputForDoc, setOpenOutputForDoc] = useState(false); const [progressMsg, setProgressMsg] = useState({}); const [docOutputs, setDocOutputs] = useState({}); + const [timers, setTimers] = useState({}); const { getDropdownItems, llmProfiles, @@ -113,17 +111,25 @@ function PromptCard({ }, [messages]); useEffect(() => { - setSelectedLlmProfileId(promptDetails?.profile_manager || null); + setSelectedLlmProfileId( + promptDetails?.profile_manager || llmProfiles[0]?.profile_id + ); }, [promptDetails]); useEffect(() => { resetInfoMsgs(); + handleGetOutput(); + handleGetCoverage(); if (isSinglePassExtractLoading) { return; } - - handleGetOutput(); - handleGetCoverage(); + if (selectedLlmProfileId !== promptDetails?.profile_id) { + handleChange( + selectedLlmProfileId, + promptDetails?.prompt_id, + "profile_manager" + ); + } }, [ selectedLlmProfileId, selectedDoc, @@ -154,20 +160,6 @@ function PromptCard({ updateCustomTool({ disableLlmOrDocChange: listOfIds }); }, [isCoverageLoading]); - useEffect(() => { - if (page < 1) { - return; - } - const llmProfile = llmProfiles[page - 1]; - if (llmProfile?.profile_id !== promptDetails?.profile_id) { - handleChange( - llmProfile?.profile_id, - promptDetails?.prompt_id, - "profile_manager" - ); - } - }, [page]); - useEffect(() => { if (isCoverageLoading && coverageTotal === listOfDocs?.length) { setIsCoverageLoading(false); @@ -180,42 +172,26 @@ function PromptCard({ }; useEffect(() => { - const isProfilePresent = llmProfiles.some( - (profile) => profile.profile_id === selectedLlmProfileId + const isProfilePresent = llmProfiles?.some( + (profile) => profile?.profile_id === selectedLlmProfileId ); // If selectedLlmProfileId is not present, set it to null if (!isProfilePresent) { setSelectedLlmProfileId(null); } - - const llmProfileId = promptDetails?.profile_manager; - if (!llmProfileId) { - setPage(0); - return; - } - const index = llmProfiles.findIndex( - (item) => item?.profile_id === llmProfileId - ); - setPage(index + 1); }, [llmProfiles]); - const handlePageLeft = () => { - if (page <= 1) { - return; - } - - const newPage = page - 1; - setPage(newPage); + // Function to update loading state for a specific document and profile + const handleIsRunLoading = (docId, profileId, isLoading) => { + setIsRunLoading((prevLoadingProfiles) => ({ + ...prevLoadingProfiles, + [`${docId}_${profileId}`]: isLoading, + })); }; - const handlePageRight = () => { - if (page >= llmProfiles?.length) { - return; - } - - const newPage = page + 1; - setPage(newPage); + const handleSelectDefaultLLM = (llmProfileId) => { + setSelectedLlmProfileId(llmProfileId); }; const handleTypeChange = (value) => { @@ -238,7 +214,12 @@ function PromptCard({ }; // Generate the result for the currently selected document - const handleRun = () => { + const handleRun = ( + profileManagerId, + coverAllDoc = true, + selectedLlmProfiles = [], + runAllLLM = false + ) => { try { setPostHogCustomEvent("ps_prompt_run", { info: "Click on 'Run Prompt' button (Multi Pass)", @@ -247,39 +228,60 @@ function PromptCard({ // If an error occurs while setting custom posthog event, ignore it and continue } - if (!promptDetails?.profile_manager?.length && !isSimplePromptStudio) { - setAlertDetails({ - type: "error", - content: "LLM Profile is not selected", - }); - return; - } + const validateInputs = ( + profileManagerId, + selectedLlmProfiles, + coverAllDoc + ) => { + if ( + !profileManagerId && + !promptDetails?.profile_manager?.length && + !(!coverAllDoc && selectedLlmProfiles?.length > 0) && + !isSimplePromptStudio + ) { + setAlertDetails({ + type: "error", + content: "LLM Profile is not selected", + }); + return true; + } - if (!selectedDoc) { - setAlertDetails({ - type: "error", - content: "Document not selected", - }); - return; - } + if (!selectedDoc) { + setAlertDetails({ + type: "error", + content: "Document not selected", + }); + return true; + } - if (!promptKey) { - setAlertDetails({ - type: "error", - content: "Prompt key cannot be empty", - }); - return; - } + if (!promptKey) { + setAlertDetails({ + type: "error", + content: "Prompt key cannot be empty", + }); + return true; + } - if (!promptText) { - setAlertDetails({ - type: "error", - content: "Prompt cannot be empty", - }); + if (!promptText) { + setAlertDetails({ + type: "error", + content: "Prompt cannot be empty", + }); + return true; + } + + return false; + }; + + if (validateInputs(profileManagerId, selectedLlmProfiles, coverAllDoc)) { return; } - setIsRunLoading(true); + handleIsRunLoading( + selectedDoc?.document_id, + profileManagerId || selectedLlmProfileId, + true + ); setIsCoverageLoading(true); setCoverage(0); setCoverageTotal(0); @@ -297,8 +299,9 @@ function PromptCard({ details?.summarize_llm_profile ) { // Summary needs to be indexed before running the prompt - setIsRunLoading(false); - handleStepsAfterRunCompletion(); + handleIsRunLoading(selectedDoc?.document_id, selectedLlmProfileId, false); + setCoverageTotal(1); + handleCoverage(selectedLlmProfileId); setAlertDetails({ type: "error", content: `Summary needs to be indexed before running the prompt - ${selectedDoc?.document_name}.`, @@ -307,39 +310,93 @@ function PromptCard({ } handleDocOutputs(docId, true, null); - handleRunApiRequest(docId) - .then((res) => { - const data = res?.data?.output; - const value = data[promptDetails?.prompt_key]; - if (value || value === 0) { - setCoverage((prev) => prev + 1); - } - handleDocOutputs(docId, false, value); - handleGetOutput(); - }) - .catch((err) => { - setIsRunLoading(false); - handleDocOutputs(docId, false, null); - setAlertDetails( - handleException(err, `Failed to generate output for ${docId}`) + if (runAllLLM) { + let selectedProfiles = llmProfiles; + if (!coverAllDoc && selectedLlmProfiles?.length > 0) { + selectedProfiles = llmProfiles.filter((profile) => + selectedLlmProfiles.includes(profile?.profile_id) ); - }) - .finally(() => { - if (isSimplePromptStudio) { + } + for (const profile of selectedProfiles) { + setIsCoverageLoading(true); + + handleIsRunLoading(selectedDoc?.document_id, profile?.profile_id, true); + handleRunApiRequest(docId, profile?.profile_id) + .then((res) => { + const data = res?.data?.output; + const value = data[promptDetails?.prompt_key]; + if (value || value === 0) { + setCoverage((prev) => prev + 1); + } + handleDocOutputs(docId, false, value); + handleGetOutput(profile?.profile_id); + updateDocCoverage( + coverage, + promptDetails?.prompt_id, + profile?.profile_id, + docId + ); + }) + .catch((err) => { + handleIsRunLoading( + selectedDoc?.document_id, + profile?.profile_id, + false + ); + handleDocOutputs(docId, false, null); + setAlertDetails( + handleException(err, `Failed to generate output for ${docId}`) + ); + }) + .finally(() => { + setIsCoverageLoading(false); + }); + runCoverageForAllDoc(coverAllDoc, profile.profile_id); + } + } else { + handleRunApiRequest(docId, profileManagerId) + .then((res) => { + const data = res?.data?.output; + const value = data[promptDetails?.prompt_key]; + if (value || value === 0) { + updateDocCoverage( + coverage, + promptDetails?.prompt_id, + profileManagerId, + docId + ); + } + handleDocOutputs(docId, false, value); + handleGetOutput(); + setCoverageTotal(1); + }) + .catch((err) => { + handleIsRunLoading( + selectedDoc?.document_id, + selectedLlmProfileId, + false + ); + handleDocOutputs(docId, false, null); + setAlertDetails( + handleException(err, `Failed to generate output for ${docId}`) + ); + }) + .finally(() => { setIsCoverageLoading(false); - } else { - handleStepsAfterRunCompletion(); - } - }); + handleIsRunLoading(selectedDoc?.document_id, profileManagerId, false); + }); + runCoverageForAllDoc(coverAllDoc, profileManagerId); + } }; - const handleStepsAfterRunCompletion = () => { - setCoverageTotal(1); - handleCoverage(); + const runCoverageForAllDoc = (coverAllDoc, profileManagerId) => { + if (coverAllDoc) { + handleCoverage(profileManagerId); + } }; // Get the coverage for all the documents except the one that's currently selected - const handleCoverage = () => { + const handleCoverage = (profileManagerId) => { const listOfDocsToProcess = [...listOfDocs].filter( (item) => item?.document_id !== selectedDoc?.document_id ); @@ -372,13 +429,19 @@ function PromptCard({ return; } + setIsCoverageLoading(true); handleDocOutputs(docId, true, null); - handleRunApiRequest(docId) + handleRunApiRequest(docId, profileManagerId) .then((res) => { const data = res?.data?.output; const outputValue = data[promptDetails?.prompt_key]; if (outputValue || outputValue === 0) { - setCoverage((prev) => prev + 1); + updateDocCoverage( + coverage, + promptDetails?.prompt_id, + profileManagerId, + docId + ); } handleDocOutputs(docId, false, outputValue); }) @@ -390,102 +453,164 @@ function PromptCard({ }) .finally(() => { totalCoverageValue++; + if (listOfDocsToProcess?.length >= totalCoverageValue) { + setIsCoverageLoading(false); + return; + } setCoverageTotal(totalCoverageValue); }); }); }; - const handleRunApiRequest = async (docId) => { + const updateDocCoverage = (coverage, promptId, profileManagerId, docId) => { + const key = `${promptId}_${profileManagerId}`; + const counts = { ...coverage }; + // If the key exists in the counts object, increment the count + if (counts[key]) { + if (!counts[key]?.docs_covered?.includes(docId)) { + counts[key]?.docs_covered?.push(docId); + } + } else { + // Otherwise, add the key to the counts object with an initial count of 1 + counts[key] = { + prompt_id: promptId, + profile_manager: profileManagerId, + docs_covered: [docId], + }; + } + setCoverage(counts); + }; + + const handleRunApiRequest = async (docId, profileManagerId) => { const promptId = promptDetails?.prompt_id; const runId = generateUUID(); + const maxWaitTime = 30 * 1000; // 30 seconds + const pollingInterval = 5000; // 5 seconds + const tokenUsagepollingInterval = 5000; const body = { document_id: docId, id: promptId, }; - let intervalId; - let tokenUsageId; - let url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/fetch_response/${details?.tool_id}`; - if (!isSimplePromptStudio) { - body["run_id"] = runId; - // Update the token usage state with default token usage for a specific document ID - tokenUsageId = promptId + "__" + docId; - setTokenUsage(tokenUsageId, defaultTokenUsage); - - // Set up an interval to fetch token usage data at regular intervals - intervalId = setInterval( - () => getTokenUsage(runId, tokenUsageId), - 5000 // Fetch token usage data every 5000 milliseconds (5 seconds) - ); - } else { - body["sps_id"] = details?.tool_id; - url = promptRunApiSps; - } - - const requestOptions = { - method: "POST", - url, - headers: { - "X-CSRFToken": sessionDetails?.csrfToken, - "Content-Type": "application/json", - }, - data: body, - }; - - return axiosPrivate(requestOptions) - .then((res) => res) - .catch((err) => { - throw err; - }) - .finally(() => { - if (!isSimplePromptStudio) { - clearInterval(intervalId); - getTokenUsage(runId, tokenUsageId); + if (profileManagerId) { + body.profile_manager = profileManagerId; + let intervalId; + let tokenUsageId; + let url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/fetch_response/${details?.tool_id}`; + if (!isSimplePromptStudio) { + body["run_id"] = runId; + // Update the token usage state with default token usage for a specific document ID + tokenUsageId = promptId + "__" + docId + "__" + profileManagerId; + setTokenUsage(tokenUsageId, defaultTokenUsage); + + // Set up an interval to fetch token usage data at regular intervals + if ( + profileManagerId === selectedLlmProfileId && + docId === selectedDoc?.document_id + ) { + intervalId = setInterval( + () => getTokenUsage(runId, tokenUsageId), + tokenUsagepollingInterval // Fetch token usage data every 5000 milliseconds (5 seconds) + ); } - }); + setTimers((prev) => ({ + ...prev, + [tokenUsageId]: 0, + })); + } else { + body["sps_id"] = details?.tool_id; + url = promptRunApiSps; + } + const timerIntervalId = startTimer(tokenUsageId); + + const requestOptions = { + method: "POST", + url, + headers: { + "X-CSRFToken": sessionDetails?.csrfToken, + "Content-Type": "application/json", + }, + data: body, + }; + + const makeApiRequest = (requestOptions) => { + return axiosPrivate(requestOptions); + }; + const startTime = Date.now(); + return pollForCompletion( + startTime, + requestOptions, + maxWaitTime, + pollingInterval, + makeApiRequest + ) + .then((response) => { + return response; + }) + .catch((err) => { + throw err; + }) + .finally(() => { + if (!isSimplePromptStudio) { + clearInterval(intervalId); + getTokenUsage(runId, tokenUsageId); + stopTimer(tokenUsageId, timerIntervalId); + } + }); + } }; - const handleGetOutput = () => { - setIsRunLoading(true); - if ( - !selectedDoc || - (!singlePassExtractMode && !selectedLlmProfileId && !isSimplePromptStudio) - ) { - setResult({ - promptOutputId: null, - output: "", - }); - setIsRunLoading(false); + const handleGetOutput = (profileManager = undefined) => { + if (!selectedDoc) { + setResult([]); + return; + } + + if (!singlePassExtractMode && !selectedLlmProfileId) { + setResult([]); return; } + handleIsRunLoading( + selectedDoc?.document_id, + profileManager || selectedLlmProfileId, + true + ); + handleOutputApiRequest(true) .then((res) => { const data = res?.data; if (!data || data?.length === 0) { - setResult({ - promptOutputId: null, - output: "", - }); + setResult([]); return; } - const outputResult = data[0]; - setResult({ - promptOutputId: outputResult?.prompt_output_id, - output: outputResult?.output, - evalMetrics: getEvalMetrics( - promptDetails?.evaluate, - outputResult?.eval_metrics || [] - ), + const outputResults = data.map((outputResult) => { + return { + runId: outputResult?.run_id, + promptOutputId: outputResult?.prompt_output_id, + profileManager: outputResult?.profile_manager, + context: outputResult?.context, + output: outputResult?.output, + totalCost: outputResult?.token_usage?.cost_in_dollars, + evalMetrics: getEvalMetrics( + promptDetails?.evaluate, + outputResult?.eval_metrics || [] + ), + }; }); + setResult(outputResults); }) .catch((err) => { setAlertDetails(handleException(err, "Failed to generate the result")); }) .finally(() => { - setIsRunLoading(false); + handleIsRunLoading( + selectedDoc?.document_id, + profileManager || selectedLlmProfileId, + false + ); }); }; @@ -494,7 +619,7 @@ function PromptCard({ (singlePassExtractMode && !defaultLlmProfile) || (!singlePassExtractMode && !selectedLlmProfileId) ) { - setCoverage(0); + setCoverage({}); return; } @@ -510,6 +635,7 @@ function PromptCard({ const handleOutputApiRequest = async (isOutput) => { let url; + let profileManager = selectedLlmProfileId; if (isSimplePromptStudio) { url = promptOutputApiSps( details?.tool_id, @@ -517,16 +643,17 @@ function PromptCard({ null ); } else { - let profileManager = selectedLlmProfileId; if (singlePassExtractMode) { profileManager = defaultLlmProfile; } - url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt-output/?tool_id=${details?.tool_id}&prompt_id=${promptDetails?.prompt_id}&profile_manager=${profileManager}&is_single_pass_extract=${singlePassExtractMode}`; + url = `/api/v1/unstract/${sessionDetails?.orgId}/prompt-studio/prompt-output/?tool_id=${details?.tool_id}&prompt_id=${promptDetails?.prompt_id}&is_single_pass_extract=${singlePassExtractMode}`; } - if (isOutput) { url += `&document_manager=${selectedDoc?.document_id}`; } + if (singlePassExtractMode) { + url += `&profile_manager=${profileManager}`; + } const requestOptions = { method: "GET", @@ -542,14 +669,14 @@ function PromptCard({ if (singlePassExtractMode) { const tokenUsageId = `single_pass__${selectedDoc?.document_id}`; - const usage = data.find((item) => item?.run_id !== undefined); + const usage = data?.find((item) => item?.run_id !== undefined); if (!tokenUsage[tokenUsageId] && usage) { setTokenUsage(tokenUsageId, usage?.token_usage); } } else { - data.forEach((item) => { - const tokenUsageId = `${item?.prompt_id}__${item?.document_manager}`; + data?.forEach((item) => { + const tokenUsageId = `${item?.prompt_id}__${item?.document_manager}__${item?.profile_manager}`; if (tokenUsage[tokenUsageId] === undefined) { setTokenUsage(tokenUsageId, item?.token_usage); @@ -564,14 +691,35 @@ function PromptCard({ }; const handleGetCoverageData = (data) => { - const coverageValue = data.reduce((acc, item) => { - if (item?.output || item?.output === 0) { - return acc + 1; - } else { - return acc; - } - }, 0); - setCoverage(coverageValue); + data?.forEach((item) => { + updateDocCoverage( + coverage, + item?.prompt_id, + item?.profile_manager, + item?.document_manager + ); + }); + }; + + const startTimer = (profileId) => { + setTimers((prev) => ({ + ...prev, + [profileId]: (prev[profileId] || 0) + 1, + })); + return setInterval(() => { + setTimers((prev) => ({ + ...prev, + [profileId]: (prev[profileId] || 0) + 1, + })); + }, 1000); + }; + + const stopTimer = (profileId, intervalId) => { + clearInterval(intervalId); + setTimers((prev) => ({ + ...prev, + [profileId]: prev[profileId] || 0, + })); }; return ( @@ -589,8 +737,6 @@ function PromptCard({ progressMsg={progressMsg} handleRun={handleRun} handleChange={handleChange} - handlePageLeft={handlePageLeft} - handlePageRight={handlePageRight} handleTypeChange={handleTypeChange} handleDelete={handleDelete} updateStatus={updateStatus} @@ -599,7 +745,8 @@ function PromptCard({ setOpenEval={setOpenEval} setOpenOutputForDoc={setOpenOutputForDoc} selectedLlmProfileId={selectedLlmProfileId} - page={page} + handleSelectDefaultLLM={handleSelectDefaultLLM} + timers={timers} /> {EvalModal && !singlePassExtractMode && ( profile.profile_id) + ); + const [expandedProfiles, setExpandedProfiles] = useState([]); // New state for expanded profiles + const [isIndexOpen, setIsIndexOpen] = useState(false); + const privateAxios = useAxiosPrivate(); + const { sessionDetails } = useSessionStore(); + const { width: windowWidth } = useWindowDimensions(); + const handleException = useExceptionHandler(); + const { setAlertDetails } = useAlertStore(); + const componentWidth = windowWidth * 0.4; - useEffect(() => { - setExpandCard(true); - }, [isSinglePassExtractLoading]); + const divRef = useRef(null); const enableEdit = (event) => { event.stopPropagation(); @@ -73,7 +106,149 @@ function PromptCardItems({ setIsEditingTitle(true); setIsEditingPrompt(true); }; + const getModelOrAdapterId = (profile, adapters) => { + const result = { conf: {} }; + const keys = ["vector_store", "embedding_model", "llm", "x2text"]; + + keys.forEach((key) => { + const adapterName = profile[key]; + const adapter = adapters?.find( + (adapter) => adapter?.adapter_name === adapterName + ); + if (adapter) { + result.conf[key] = adapter?.model || adapter?.adapter_id?.split("|")[0]; + if (adapter?.adapter_type === "LLM") result.icon = adapter?.icon; + } + }); + return result; + }; + const getAdapterInfo = async () => { + privateAxios + .get(`/api/v1/unstract/${sessionDetails?.orgId}/adapter/`) + .then((res) => { + const adapterData = res?.data; + + // Update llmProfiles with additional fields + const updatedProfiles = llmProfiles?.map((profile) => { + return { ...getModelOrAdapterId(profile, adapterData), ...profile }; + }); + setLlmProfileDetails( + updatedProfiles + .map((profile) => ({ + ...profile, + isDefault: profile?.profile_id === selectedLlmProfileId, + isEnabled: enabledProfiles.includes(profile?.profile_id), + })) + .sort((a, b) => { + if (a?.isDefault) return -1; // Default profile comes first + if (b?.isDefault) return 1; + if (a?.isEnabled && !b?.isEnabled) return -1; // Enabled profiles come before disabled + if (!a?.isEnabled && b?.isEnabled) return 1; + return 0; + }) + ); + }) + .catch((err) => { + setAlertDetails(handleException(err)); + }); + }; + + const tooltipContent = (adapterConf) => ( +
+ {Object.entries(adapterConf)?.map(([key, value]) => ( +
+ {key}: {value} +
+ ))} +
+ ); + + const handleExpandClick = (profile) => { + const profileId = profile?.profile_id; + setExpandedProfiles((prevState) => + prevState.includes(profileId) + ? prevState.filter((id) => id !== profileId) + : [...prevState, profileId] + ); + }; + + const handleTagChange = (checked, profileId) => { + setEnabledProfiles((prevState) => + checked + ? [...prevState, profileId] + : prevState.filter((id) => id !== profileId) + ); + }; + + const getColSpan = () => (componentWidth < 1200 ? 24 : 6); + + const renderSinglePassResult = () => { + const [firstResult] = result || []; + if ( + promptDetails.active && + (firstResult?.output || firstResult?.output === 0) + ) { + return ( + <> + +
+ {isSinglePassExtractLoading ? ( + } /> + ) : ( + +
+ {displayPromptResult(firstResult.output, true)} +
+
+ )} +
+ + + +
+
+ + ); + } + return <>; + }; + + useEffect(() => { + setExpandCard(true); + }, [isSinglePassExtractLoading]); + + useEffect(() => { + if (singlePassExtractMode) { + setExpandedProfiles([]); + } + }, [singlePassExtractMode]); + + useEffect(() => { + getAdapterInfo(); + }, [llmProfiles, selectedLlmProfileId, enabledProfiles]); return (
@@ -94,6 +269,7 @@ function PromptCardItems({ enableEdit={enableEdit} expandCard={expandCard} setExpandCard={setExpandCard} + enabledProfiles={enabledProfiles} />
@@ -110,7 +286,7 @@ function PromptCardItems({ text={promptText} setText={setPromptText} promptId={promptDetails?.prompt_id} - defaultText={promptDetails.prompt} + defaultText={promptDetails?.prompt} handleChange={handleChange} isTextarea={true} placeHolder={updatePlaceHolder} @@ -120,7 +296,6 @@ function PromptCardItems({ {!isSimplePromptStudio && ( <> - )} @@ -150,22 +325,16 @@ function PromptCardItems({ )} - Coverage: {coverage} of {listOfDocs?.length || 0}{" "} - docs + Coverage:{" "} + {coverage[ + `${promptDetails?.prompt_id}_${selectedLlmProfileId}` + ]?.docs_covered?.length || 0}{" "} + of {listOfDocs?.length || 0} docs - {!singlePassExtractMode && ( - - )}