From 79ca25ec7ef093369d17ea60011d600bb826b450 Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 5 Sep 2025 15:48:57 +0800 Subject: [PATCH] Feat: Allow users to select prompt word templates in agent operators. #9935 (#9936) ### What problem does this PR solve? Feat: Allow users to select prompt word templates in agent operators. #9935 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/hooks/use-agent-request.ts | 22 ++++++++++ web/src/locales/en.ts | 1 + web/src/locales/zh.ts | 1 + web/src/pages/agent/form/agent-form/index.tsx | 5 +++ .../agent-form/use-build-prompt-options.ts | 30 ++++++++++++++ .../form/components/prompt-editor/index.tsx | 13 ++++-- .../prompt-editor/variable-node.tsx | 2 - .../variable-on-change-plugin.tsx | 6 ++- .../prompt-editor/variable-picker-plugin.tsx | 40 ++++++++++++++----- web/src/services/agent-service.ts | 5 +++ web/src/utils/api.ts | 1 + 11 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 web/src/pages/agent/form/agent-form/use-build-prompt-options.ts diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index a40ef0f16..6a3f53eca 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -51,6 +51,7 @@ export const enum AgentApiAction { FetchAgentAvatar = 'fetchAgentAvatar', FetchExternalAgentInputs = 'fetchExternalAgentInputs', SetAgentSetting = 'setAgentSetting', + FetchPrompt = 'fetchPrompt', } export const EmptyDsl = { @@ -637,3 +638,24 @@ export const useSetAgentSetting = () => { return { data, loading, setAgentSetting: mutateAsync }; }; + +export const useFetchPrompt = () => { + const { + data, + isFetching: loading, + refetch, + } = useQuery>({ + queryKey: [AgentApiAction.FetchPrompt], + refetchOnReconnect: false, + refetchOnMount: false, + refetchOnWindowFocus: false, + gcTime: 0, + queryFn: async () => { + const { data } = await agentService.fetchPrompt(); + + return data?.data ?? {}; + }, + }); + + return { data, loading, refetch }; +}; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 2edf2404c..b94d1c9c5 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1518,6 +1518,7 @@ This delimiter is used to split the input text into several text pieces echo of sqlStatement: 'SQL Statement', sqlStatementTip: 'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.', + frameworkPrompts: 'Framework Prompts', }, llmTools: { bad_calculator: { diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 973a9ac9a..bfae19a6b 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1433,6 +1433,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 sqlStatement: 'SQL 语句', sqlStatementTip: '在此处编写您的 SQL 查询。您可以使用变量、原始 SQL,或使用变量语法混合使用两者。', + frameworkPrompts: '框架提示词', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index 9ca0fb69c..2e0eb5fd2 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -39,6 +39,7 @@ import { Output } from '../components/output'; import { PromptEditor } from '../components/prompt-editor'; import { QueryVariable } from '../components/query-variable'; import { AgentTools, Agents } from './agent-tools'; +import { useBuildPromptExtraPromptOptions } from './use-build-prompt-options'; import { useValues } from './use-values'; import { useWatchFormChange } from './use-watch-change'; @@ -85,6 +86,9 @@ function AgentForm({ node }: INextOperatorForm) { const defaultValues = useValues(node); + const { extraOptions } = useBuildPromptExtraPromptOptions(); + console.log('🚀 ~ AgentForm ~ prompts:', extraOptions); + const ExceptionMethodOptions = Object.values(AgentExceptionMethod).map( (x) => ({ label: t(`flow.${x}`), @@ -150,6 +154,7 @@ function AgentForm({ node }: INextOperatorForm) { {...field} placeholder={t('flow.messagePlaceholder')} showToolbar={false} + extraOptions={extraOptions} > diff --git a/web/src/pages/agent/form/agent-form/use-build-prompt-options.ts b/web/src/pages/agent/form/agent-form/use-build-prompt-options.ts new file mode 100644 index 000000000..9c34ce77c --- /dev/null +++ b/web/src/pages/agent/form/agent-form/use-build-prompt-options.ts @@ -0,0 +1,30 @@ +import { useFetchPrompt } from '@/hooks/use-agent-request'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const PromptIdentity = 'RAGFlow-Prompt'; + +function wrapPromptWithTag(text: string, tag: string) { + const capitalTag = tag.toUpperCase(); + return `<${capitalTag}> + ${text} +`; +} + +export function useBuildPromptExtraPromptOptions() { + const { data: prompts } = useFetchPrompt(); + const { t } = useTranslation(); + + const options = useMemo(() => { + return Object.entries(prompts || {}).map(([key, value]) => ({ + label: key, + value: wrapPromptWithTag(value, key), + })); + }, [prompts]); + + const extraOptions = [ + { label: PromptIdentity, title: t('flow.frameworkPrompts'), options }, + ]; + + return { extraOptions }; +} diff --git a/web/src/pages/agent/form/components/prompt-editor/index.tsx b/web/src/pages/agent/form/components/prompt-editor/index.tsx index caf6914b4..ffab9b44e 100644 --- a/web/src/pages/agent/form/components/prompt-editor/index.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/index.tsx @@ -29,7 +29,9 @@ import { PasteHandlerPlugin } from './paste-handler-plugin'; import theme from './theme'; import { VariableNode } from './variable-node'; import { VariableOnChangePlugin } from './variable-on-change-plugin'; -import VariablePickerMenuPlugin from './variable-picker-plugin'; +import VariablePickerMenuPlugin, { + VariablePickerMenuPluginProps, +} from './variable-picker-plugin'; // Catch any errors that occur during Lexical updates and log them // or throw them as needed. If you don't throw them, Lexical will @@ -52,7 +54,8 @@ type IProps = { value?: string; onChange?: (value?: string) => void; placeholder?: ReactNode; -} & PromptContentProps; +} & PromptContentProps & + Pick; function PromptContent({ showToolbar = true, @@ -122,6 +125,7 @@ export function PromptEditor({ placeholder, showToolbar, multiLine = true, + extraOptions, }: IProps) { const { t } = useTranslation(); const initialConfig: InitialConfigType = { @@ -170,7 +174,10 @@ export function PromptEditor({ } ErrorBoundary={LexicalErrorBoundary} /> - + { __value: string; diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx index 86fa66db4..002face8d 100644 --- a/web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/variable-on-change-plugin.tsx @@ -3,7 +3,7 @@ import { EditorState, LexicalEditor } from 'lexical'; import { useEffect } from 'react'; import { ProgrammaticTag } from './constant'; -interface IProps { +interface VariableOnChangePluginProps { onChange: ( editorState: EditorState, editor?: LexicalEditor, @@ -11,7 +11,9 @@ interface IProps { ) => void; } -export function VariableOnChangePlugin({ onChange }: IProps) { +export function VariableOnChangePlugin({ + onChange, +}: VariableOnChangePluginProps) { // Access the editor through the LexicalComposerContext const [editor] = useLexicalComposerContext(); // Wrap our listener in useEffect to handle the teardown and avoid stale references. diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx index f429981c7..7488d991a 100644 --- a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx @@ -32,6 +32,7 @@ import * as ReactDOM from 'react-dom'; import { $createVariableNode } from './variable-node'; import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query'; +import { PromptIdentity } from '../../agent-form/use-build-prompt-options'; import { ProgrammaticTag } from './constant'; import './index.css'; class VariableInnerOption extends MenuOption { @@ -108,11 +109,18 @@ function VariablePickerMenuItem({ ); } +export type VariablePickerMenuPluginProps = { + value?: string; + extraOptions?: Array<{ + label: string; + title: string; + options: Array<{ label: string; value: string; icon?: ReactNode }>; + }>; +}; export default function VariablePickerMenuPlugin({ value, -}: { - value?: string; -}): JSX.Element { + extraOptions, +}: VariablePickerMenuPluginProps): JSX.Element { const [editor] = useLexicalComposerContext(); const isFirstRender = useRef(true); @@ -122,10 +130,10 @@ export default function VariablePickerMenuPlugin({ const [queryString, setQueryString] = React.useState(''); - const options = useBuildQueryVariableOptions(); + let options = useBuildQueryVariableOptions(); const buildNextOptions = useCallback(() => { - let filteredOptions = options; + let filteredOptions = [...options, ...(extraOptions ?? [])]; if (queryString) { const lowerQuery = queryString.toLowerCase(); filteredOptions = options @@ -140,7 +148,7 @@ export default function VariablePickerMenuPlugin({ .filter((x) => x.options.length > 0); } - const nextOptions: VariableOption[] = filteredOptions.map( + const finalOptions: VariableOption[] = filteredOptions.map( (x) => new VariableOption( x.label, @@ -150,8 +158,8 @@ export default function VariablePickerMenuPlugin({ }), ), ); - return nextOptions; - }, [options, queryString]); + return finalOptions; + }, [extraOptions, options, queryString]); const findItemByValue = useCallback( (value: string) => { @@ -173,7 +181,7 @@ export default function VariablePickerMenuPlugin({ const onSelectOption = useCallback( ( - selectedOption: VariableOption | VariableInnerOption, + selectedOption: VariableInnerOption, nodeToRemove: TextNode | null, closeMenu: () => void, ) => { @@ -193,7 +201,11 @@ export default function VariablePickerMenuPlugin({ selectedOption.parentLabel as string | ReactNode, selectedOption.icon as ReactNode, ); - selection.insertNodes([variableNode]); + if (selectedOption.parentLabel === PromptIdentity) { + selection.insertText(selectedOption.value); + } else { + selection.insertNodes([variableNode]); + } closeMenu(); }); @@ -269,7 +281,13 @@ export default function VariablePickerMenuPlugin({ return ( onQueryChange={setQueryString} - onSelectOption={onSelectOption} + onSelectOption={(option, textNodeContainingQuery, closeMenu) => + onSelectOption( + option as VariableInnerOption, // Only the second level menu can be selected + textNodeContainingQuery, + closeMenu, + ) + } triggerFn={checkForTriggerMatch} options={buildNextOptions()} menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => { diff --git a/web/src/services/agent-service.ts b/web/src/services/agent-service.ts index 978100b1a..6c0b3aceb 100644 --- a/web/src/services/agent-service.ts +++ b/web/src/services/agent-service.ts @@ -25,6 +25,7 @@ const { fetchAgentAvatar, fetchAgentLogs, fetchExternalAgentInputs, + prompt, } = api; const methods = { @@ -112,6 +113,10 @@ const methods = { url: fetchExternalAgentInputs, method: 'get', }, + fetchPrompt: { + url: prompt, + method: 'get', + }, } as const; const agentService = registerNextServer(methods); diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index cbb0588b3..cf294a67c 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -164,6 +164,7 @@ export default { `${api_host}/canvas/${canvasId}/sessions`, fetchExternalAgentInputs: (canvasId: string) => `${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`, + prompt: `${api_host}/canvas/prompts`, // mcp server listMcpServer: `${api_host}/mcp_server/list`,