From 39fdb1a1a259c9f18949b119c7be12bb2d5b117a Mon Sep 17 00:00:00 2001 From: "opensearch-trigger-bot[bot]" <98922864+opensearch-trigger-bot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 08:17:27 -0800 Subject: [PATCH] Support inserting template vars at cursor position; improve RAG preset (#526) (#527) * Improve RAG preset prompt defaults * Change prompt vars to insert at editor cursor position * Fix spacing in configure prompt modal * Improve default empty vals for input output map transforms * handle npe; add check to block showing resources for custom workflow types * add check to handle empty or missing ui_metadata * fix UT --------- (cherry picked from commit b613ce381719e263e0d7744a0f4fcddbdda33e2b) Signed-off-by: Tyler Ohlsen Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- common/constants.ts | 33 +++++-- common/interfaces.ts | 9 +- .../modals/configure_template_modal.tsx | 90 ++++++++++------- .../new_workflow/quick_configure_inputs.tsx | 97 ++++++++++++++++--- .../new_workflow/quick_configure_modal.tsx | 22 +++-- .../workflow_list/workflow_list.test.tsx | 2 +- .../workflows/workflow_list/workflow_list.tsx | 45 +++++++-- 7 files changed, 213 insertions(+), 85 deletions(-) diff --git a/common/constants.ts b/common/constants.ts index d6ee0097..1088940b 100644 --- a/common/constants.ts +++ b/common/constants.ts @@ -426,6 +426,12 @@ export const QUERY_PRESETS = [ }, ] as QueryPreset[]; +/** + * DEFAULT TEMPLATE VAR NAMES + */ +export const DEFAULT_PROMPT_RESULTS_FIELD = 'results'; +export const DEFAULT_PROMPT_QUESTION_FIELD = 'question'; + /** * PROMPT PRESETS */ @@ -434,19 +440,25 @@ export const SUMMARIZE_DOCS_PROMPT = You are given a list of document results. You will \ analyze the data and generate a human-readable summary of the results. If you don't \ know the answer, just say I don't know.\ -\n\n Results: \ +\n\n Results: ${parameters." + + DEFAULT_PROMPT_RESULTS_FIELD + + '.toString()} \ \n\n Human: Please summarize the results.\ -\n\n Assistant:"; +\n\n Assistant:'; export const QA_WITH_DOCUMENTS_PROMPT = "Human: You are a professional data analyst. \ You are given a list of document results, along with a question. You will \ analyze the results and generate a human-readable response to the question, \ based on the results. If you don't know the answer, just say I don't know.\ -\n\n Results: \ -\n\n Question: \ +\n\n Results: ${parameters." + + DEFAULT_PROMPT_RESULTS_FIELD + + '.toString()} \ +\n\n Question: ${parameters.' + + DEFAULT_PROMPT_QUESTION_FIELD + + '.toString()} \ \n\n Human: Please answer the question using the provided results.\ -\n\n Assistant:"; +\n\n Assistant:'; export const PROMPT_PRESETS = [ { @@ -507,11 +519,18 @@ export const EMPTY_MAP_ENTRY = { key: '', value: '' } as MapEntry; export const EMPTY_INPUT_MAP_ENTRY = { key: '', value: { - transformType: '' as TRANSFORM_TYPE, + transformType: TRANSFORM_TYPE.FIELD, value: '', }, } as InputMapEntry; -export const EMPTY_OUTPUT_MAP_ENTRY = EMPTY_INPUT_MAP_ENTRY; + +export const EMPTY_OUTPUT_MAP_ENTRY = { + ...EMPTY_INPUT_MAP_ENTRY, + value: { + ...EMPTY_INPUT_MAP_ENTRY.value, + transformType: NO_TRANSFORMATION as TRANSFORM_TYPE, + }, +}; export const MODEL_OUTPUT_SCHEMA_NESTED_PATH = 'output.properties.inference_results.items.properties.output.items.properties.dataAsMap.properties'; export const MODEL_OUTPUT_SCHEMA_FULL_PATH = 'output.properties'; diff --git a/common/interfaces.ts b/common/interfaces.ts index 39cad171..cdc02fd3 100644 --- a/common/interfaces.ts +++ b/common/interfaces.ts @@ -365,7 +365,7 @@ export type WorkflowTemplate = { // https://github.com/opensearch-project/flow-framework/issues/526 version?: any; workflows?: TemplateFlows; - use_case?: USE_CASE; + use_case?: string; // UI state and any ReactFlow state may not exist if a workflow is created via API/backend-only. ui_metadata?: UIState; }; @@ -386,12 +386,6 @@ export type Workflow = WorkflowTemplate & { resourcesCreated?: WorkflowResource[]; }; -export enum USE_CASE { - SEMANTIC_SEARCH = 'SEMANTIC_SEARCH', - NEURAL_SPARSE_SEARCH = 'NEURAL_SPARSE_SEARCH', - HYBRID_SEARCH = 'HYBRID_SEARCH', -} - /** ********** ML PLUGIN TYPES/INTERFACES ********** */ @@ -554,6 +548,7 @@ export type QuickConfigureFields = { textField?: string; imageField?: string; labelField?: string; + promptField?: string; embeddingLength?: number; llmResponseField?: string; }; diff --git a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/configure_template_modal.tsx b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/configure_template_modal.tsx index 8cfc280f..ce3bb2cc 100644 --- a/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/configure_template_modal.tsx +++ b/public/pages/workflow_detail/workflow_inputs/processor_inputs/ml_processor_inputs/modals/configure_template_modal.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'; import { useFormikContext, getIn, Formik } from 'formik'; -import { isEmpty } from 'lodash'; +import { get, isEmpty } from 'lodash'; import * as yup from 'yup'; import { EuiCodeEditor, @@ -23,7 +23,6 @@ import { EuiSmallButtonEmpty, EuiSmallButtonIcon, EuiSpacer, - EuiCopy, EuiIconTip, } from '@elastic/eui'; import { @@ -80,6 +79,10 @@ const VALUE_FLEX_RATIO = 6; // the max number of input docs we use to display & test transforms with (search response hits) const MAX_INPUT_DOCS = 10; +// the prompt editor element ID. Used when fetching the element to perform functions on the +// underlying ace editor (inserting template variables at the cursor position) +const PROMPT_EDITOR_ID = 'promptEditor'; + /** * A modal to configure a prompt template. Can manually configure, include placeholder values * using other model inputs, and/or select from a presets library. Used for configuring model @@ -298,12 +301,12 @@ export function ConfigureTemplateModal(props: ConfigureTemplateModalProps) { - + Prompt - + - + @@ -443,38 +446,51 @@ export function ConfigureTemplateModal(props: ConfigureTemplateModalProps) { /> - { + const promptEditorParentElement = document + .getElementById(PROMPT_EDITOR_ID) + ?.getElementsByClassName( + 'ace_editor' + ); + const promptEditor = get( + promptEditorParentElement, + '0.env.editor' + ); + const promptEditorSession = + promptEditor?.session; + const cursorPosition = promptEditor?.getCursorPosition(); + const valueToInsert = getPlaceholderString( + getIn( + formikProps.values, + `nestedVars.${idx}.name` + ) + ); + if ( + promptEditorSession !== undefined && + cursorPosition !== undefined && + valueToInsert !== undefined && + !isEmpty(valueToInsert) + ) { + promptEditorSession.insert( + cursorPosition, + valueToInsert + ); + } else { + console.error( + 'Value could not be inserted' + ); + } + }} > - {(copy) => ( - - )} - + Insert + - + Prompt preview - + ([]); + // Selected model interface state + const [selectedModelInterface, setSelectedModelInterface] = useState< + ModelInterface | undefined + >(undefined); + // Hook to update available deployed models useEffect(() => { if (models) { @@ -89,6 +97,7 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { case WORKFLOW_TYPE.RAG: { defaultFieldValues = { textField: DEFAULT_TEXT_FIELD, + promptField: '', llmResponseField: DEFAULT_LLM_RESPONSE_FIELD, }; break; @@ -116,6 +125,7 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { const selectedModel = deployedModels.find( (model) => model.id === fieldValues.modelId ); + setSelectedModelInterface(selectedModel?.interface); if (selectedModel?.connectorId !== undefined) { const connector = connectors[selectedModel.connectorId]; if (connector !== undefined) { @@ -150,6 +160,19 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { } }, [fieldValues.modelId, deployedModels, connectors]); + // When the model interface is defined, set a default prompt field, if applicable. + useEffect(() => { + if ( + props.workflowType === WORKFLOW_TYPE.RAG && + selectedModelInterface !== undefined + ) { + setFieldValues({ + ...fieldValues, + promptField: get(parseModelInputs(selectedModelInterface), '0.label'), + }); + } + }, [selectedModelInterface]); + return ( <> {props.workflowType !== WORKFLOW_TYPE.CUSTOM ? ( @@ -325,23 +348,65 @@ export function QuickConfigureInputs(props: QuickConfigureInputsProps) { )} {props.workflowType === WORKFLOW_TYPE.RAG && ( - - + { - setFieldValues({ - ...fieldValues, - llmResponseField: e.target.value, - }); - }} - /> - + label={'Prompt field'} + isInvalid={false} + helpText={'The model input field representing the prompt'} + > + + ({ + value: option.label, + inputDisplay: ( + <> + {option.label} + + ), + dropdownDisplay: ( + <> + {option.label} + + {option.type} + + + ), + disabled: false, + } as EuiSuperSelectOption) + )} + valueOfSelected={fieldValues?.promptField || ''} + onChange={(option: string) => { + setFieldValues({ + ...fieldValues, + promptField: option, + }); + }} + isInvalid={false} + /> + + + + { + setFieldValues({ + ...fieldValues, + llmResponseField: e.target.value, + }); + }} + /> + + )} diff --git a/public/pages/workflows/new_workflow/quick_configure_modal.tsx b/public/pages/workflows/new_workflow/quick_configure_modal.tsx index daa9e58e..8a706a79 100644 --- a/public/pages/workflows/new_workflow/quick_configure_modal.tsx +++ b/public/pages/workflows/new_workflow/quick_configure_modal.tsx @@ -20,6 +20,7 @@ import { EuiCompressedFormRow, } from '@elastic/eui'; import { + DEFAULT_PROMPT_RESULTS_FIELD, EMPTY_INPUT_MAP_ENTRY, EMPTY_OUTPUT_MAP_ENTRY, IMAGE_FIELD_PATTERN, @@ -33,6 +34,7 @@ import { OutputMapFormValue, PROCESSOR_TYPE, QuickConfigureFields, + SUMMARIZE_DOCS_PROMPT, TEXT_FIELD_PATTERN, TRANSFORM_TYPE, VECTOR, @@ -228,7 +230,7 @@ function injectQuickConfigureFields( workflow.ui_metadata.config, quickConfigureFields ); - workflow.ui_metadata.config = updateSearchResponseProcessors( + workflow.ui_metadata.config = updateRAGSearchResponseProcessors( workflow.ui_metadata.config, quickConfigureFields, modelInterface @@ -416,8 +418,8 @@ function updateSearchRequestProcessors( return config; } -// prefill response processor configs, if applicable -function updateSearchResponseProcessors( +// prefill response processor configs for RAG use cases +function updateRAGSearchResponseProcessors( config: WorkflowConfig, fields: QuickConfigureFields, modelInterface: ModelInterface | undefined @@ -431,13 +433,19 @@ function updateSearchResponseProcessors( } if (field.id === 'input_map') { const inputMap = generateInputMapFromModelInputs(modelInterface); - if (fields.textField) { + if (fields.promptField && fields.textField) { if (inputMap.length > 0) { inputMap[0] = { ...inputMap[0], value: { - transformType: TRANSFORM_TYPE.FIELD, - value: fields.textField, + transformType: TRANSFORM_TYPE.TEMPLATE, + value: SUMMARIZE_DOCS_PROMPT, + nestedVars: [ + { + name: DEFAULT_PROMPT_RESULTS_FIELD, + transform: fields.textField, + }, + ], }, }; } else { @@ -445,7 +453,7 @@ function updateSearchResponseProcessors( key: '', value: { transformType: TRANSFORM_TYPE.FIELD, - value: fields.textField, + value: '', }, }); } diff --git a/public/pages/workflows/workflow_list/workflow_list.test.tsx b/public/pages/workflows/workflow_list/workflow_list.test.tsx index a7f2d1b2..966e4316 100644 --- a/public/pages/workflows/workflow_list/workflow_list.test.tsx +++ b/public/pages/workflows/workflow_list/workflow_list.test.tsx @@ -162,7 +162,7 @@ describe('WorkflowList', () => { const viewResourcesButtons = getAllByLabelText('View resources'); userEvent.click(viewResourcesButtons[0]); await waitFor(() => { - expect(getByText('No existing resources found')).toBeInTheDocument(); + expect(getByText(/Active resources/)).toBeInTheDocument(); }); }); diff --git a/public/pages/workflows/workflow_list/workflow_list.tsx b/public/pages/workflows/workflow_list/workflow_list.tsx index 62bd7de3..5c2e6833 100644 --- a/public/pages/workflows/workflow_list/workflow_list.tsx +++ b/public/pages/workflows/workflow_list/workflow_list.tsx @@ -5,7 +5,7 @@ import React, { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { debounce } from 'lodash'; +import { debounce, isEmpty } from 'lodash'; import { EuiInMemoryTable, Direction, @@ -18,6 +18,7 @@ import { EuiFlyoutHeader, EuiText, EuiFlyoutBody, + EuiEmptyPrompt, } from '@elastic/eui'; import { AppState } from '../../../store'; import { @@ -93,7 +94,7 @@ export function WorkflowList(props: WorkflowListProps) { useEffect(() => { setFilteredWorkflows( fetchFilteredWorkflows( - Object.values(workflows), + Object.values(workflows || {}), selectedTypes, searchQuery ) @@ -147,7 +148,24 @@ export function WorkflowList(props: WorkflowListProps) { - + {selectedWorkflow?.ui_metadata === undefined || + isEmpty(selectedWorkflow?.ui_metadata) || + selectedWorkflow?.ui_metadata?.type === WORKFLOW_TYPE.CUSTOM ? ( + Invalid workflow type} + titleSize="s" + body={ + <> + + Displaying resources from custom workflows is not + currently supported. + + + } + /> + ) : ( + + )} )} @@ -199,14 +217,21 @@ function fetchFilteredWorkflows( typeFilters: EuiFilterSelectItem[], searchQuery: string ): Workflow[] { + // A common use case for API users is to create workflows to register agents for + // things like chatbots. We specifically filter those out on the UI to prevent confusion. + const allWorkflowsExceptRegisterAgent = allWorkflows.filter( + (workflow) => workflow.use_case !== 'REGISTER_AGENT' + ); // If missing/invalid ui metadata, add defaults - const allWorkflowsWithDefaults = allWorkflows.map((workflow) => ({ - ...workflow, - ui_metadata: { - ...workflow.ui_metadata, - type: workflow.ui_metadata?.type || WORKFLOW_TYPE.UNKNOWN, - } as UIState, - })); + const allWorkflowsWithDefaults = allWorkflowsExceptRegisterAgent.map( + (workflow) => ({ + ...workflow, + ui_metadata: { + ...workflow.ui_metadata, + type: workflow.ui_metadata?.type || WORKFLOW_TYPE.UNKNOWN, + } as UIState, + }) + ); // @ts-ignore const typeFilterStrings = typeFilters.map((filter) => filter.name); const filteredWorkflows = allWorkflowsWithDefaults.filter((workflow) =>