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)
This commit is contained in:
balibabu
2025-09-05 15:48:57 +08:00
committed by GitHub
parent 6ff7cfe005
commit 79ca25ec7e
11 changed files with 108 additions and 18 deletions

View File

@ -51,6 +51,7 @@ export const enum AgentApiAction {
FetchAgentAvatar = 'fetchAgentAvatar', FetchAgentAvatar = 'fetchAgentAvatar',
FetchExternalAgentInputs = 'fetchExternalAgentInputs', FetchExternalAgentInputs = 'fetchExternalAgentInputs',
SetAgentSetting = 'setAgentSetting', SetAgentSetting = 'setAgentSetting',
FetchPrompt = 'fetchPrompt',
} }
export const EmptyDsl = { export const EmptyDsl = {
@ -637,3 +638,24 @@ export const useSetAgentSetting = () => {
return { data, loading, setAgentSetting: mutateAsync }; return { data, loading, setAgentSetting: mutateAsync };
}; };
export const useFetchPrompt = () => {
const {
data,
isFetching: loading,
refetch,
} = useQuery<Record<string, string>>({
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 };
};

View File

@ -1518,6 +1518,7 @@ This delimiter is used to split the input text into several text pieces echo of
sqlStatement: 'SQL Statement', sqlStatement: 'SQL Statement',
sqlStatementTip: sqlStatementTip:
'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.', 'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.',
frameworkPrompts: 'Framework Prompts',
}, },
llmTools: { llmTools: {
bad_calculator: { bad_calculator: {

View File

@ -1433,6 +1433,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
sqlStatement: 'SQL 语句', sqlStatement: 'SQL 语句',
sqlStatementTip: sqlStatementTip:
'在此处编写您的 SQL 查询。您可以使用变量、原始 SQL或使用变量语法混合使用两者。', '在此处编写您的 SQL 查询。您可以使用变量、原始 SQL或使用变量语法混合使用两者。',
frameworkPrompts: '框架提示词',
}, },
footer: { footer: {
profile: 'All rights reserved @ React', profile: 'All rights reserved @ React',

View File

@ -39,6 +39,7 @@ import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor'; import { PromptEditor } from '../components/prompt-editor';
import { QueryVariable } from '../components/query-variable'; import { QueryVariable } from '../components/query-variable';
import { AgentTools, Agents } from './agent-tools'; import { AgentTools, Agents } from './agent-tools';
import { useBuildPromptExtraPromptOptions } from './use-build-prompt-options';
import { useValues } from './use-values'; import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change'; import { useWatchFormChange } from './use-watch-change';
@ -85,6 +86,9 @@ function AgentForm({ node }: INextOperatorForm) {
const defaultValues = useValues(node); const defaultValues = useValues(node);
const { extraOptions } = useBuildPromptExtraPromptOptions();
console.log('🚀 ~ AgentForm ~ prompts:', extraOptions);
const ExceptionMethodOptions = Object.values(AgentExceptionMethod).map( const ExceptionMethodOptions = Object.values(AgentExceptionMethod).map(
(x) => ({ (x) => ({
label: t(`flow.${x}`), label: t(`flow.${x}`),
@ -150,6 +154,7 @@ function AgentForm({ node }: INextOperatorForm) {
{...field} {...field}
placeholder={t('flow.messagePlaceholder')} placeholder={t('flow.messagePlaceholder')}
showToolbar={false} showToolbar={false}
extraOptions={extraOptions}
></PromptEditor> ></PromptEditor>
</FormControl> </FormControl>
</FormItem> </FormItem>

View File

@ -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}
</${capitalTag}>`;
}
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 };
}

View File

@ -29,7 +29,9 @@ import { PasteHandlerPlugin } from './paste-handler-plugin';
import theme from './theme'; import theme from './theme';
import { VariableNode } from './variable-node'; import { VariableNode } from './variable-node';
import { VariableOnChangePlugin } from './variable-on-change-plugin'; 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 // Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will // or throw them as needed. If you don't throw them, Lexical will
@ -52,7 +54,8 @@ type IProps = {
value?: string; value?: string;
onChange?: (value?: string) => void; onChange?: (value?: string) => void;
placeholder?: ReactNode; placeholder?: ReactNode;
} & PromptContentProps; } & PromptContentProps &
Pick<VariablePickerMenuPluginProps, 'extraOptions'>;
function PromptContent({ function PromptContent({
showToolbar = true, showToolbar = true,
@ -122,6 +125,7 @@ export function PromptEditor({
placeholder, placeholder,
showToolbar, showToolbar,
multiLine = true, multiLine = true,
extraOptions,
}: IProps) { }: IProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const initialConfig: InitialConfigType = { const initialConfig: InitialConfigType = {
@ -170,7 +174,10 @@ export function PromptEditor({
} }
ErrorBoundary={LexicalErrorBoundary} ErrorBoundary={LexicalErrorBoundary}
/> />
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin> <VariablePickerMenuPlugin
value={value}
extraOptions={extraOptions}
></VariablePickerMenuPlugin>
<PasteHandlerPlugin /> <PasteHandlerPlugin />
<VariableOnChangePlugin <VariableOnChangePlugin
onChange={onValueChange} onChange={onValueChange}

View File

@ -1,7 +1,5 @@
import { BeginId } from '@/pages/flow/constant';
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical'; import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
const prefix = BeginId + '@';
export class VariableNode extends DecoratorNode<ReactNode> { export class VariableNode extends DecoratorNode<ReactNode> {
__value: string; __value: string;

View File

@ -3,7 +3,7 @@ import { EditorState, LexicalEditor } from 'lexical';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { ProgrammaticTag } from './constant'; import { ProgrammaticTag } from './constant';
interface IProps { interface VariableOnChangePluginProps {
onChange: ( onChange: (
editorState: EditorState, editorState: EditorState,
editor?: LexicalEditor, editor?: LexicalEditor,
@ -11,7 +11,9 @@ interface IProps {
) => void; ) => void;
} }
export function VariableOnChangePlugin({ onChange }: IProps) { export function VariableOnChangePlugin({
onChange,
}: VariableOnChangePluginProps) {
// Access the editor through the LexicalComposerContext // Access the editor through the LexicalComposerContext
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
// Wrap our listener in useEffect to handle the teardown and avoid stale references. // Wrap our listener in useEffect to handle the teardown and avoid stale references.

View File

@ -32,6 +32,7 @@ import * as ReactDOM from 'react-dom';
import { $createVariableNode } from './variable-node'; import { $createVariableNode } from './variable-node';
import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query'; import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query';
import { PromptIdentity } from '../../agent-form/use-build-prompt-options';
import { ProgrammaticTag } from './constant'; import { ProgrammaticTag } from './constant';
import './index.css'; import './index.css';
class VariableInnerOption extends MenuOption { 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({ export default function VariablePickerMenuPlugin({
value, value,
}: { extraOptions,
value?: string; }: VariablePickerMenuPluginProps): JSX.Element {
}): JSX.Element {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
@ -122,10 +130,10 @@ export default function VariablePickerMenuPlugin({
const [queryString, setQueryString] = React.useState<string | null>(''); const [queryString, setQueryString] = React.useState<string | null>('');
const options = useBuildQueryVariableOptions(); let options = useBuildQueryVariableOptions();
const buildNextOptions = useCallback(() => { const buildNextOptions = useCallback(() => {
let filteredOptions = options; let filteredOptions = [...options, ...(extraOptions ?? [])];
if (queryString) { if (queryString) {
const lowerQuery = queryString.toLowerCase(); const lowerQuery = queryString.toLowerCase();
filteredOptions = options filteredOptions = options
@ -140,7 +148,7 @@ export default function VariablePickerMenuPlugin({
.filter((x) => x.options.length > 0); .filter((x) => x.options.length > 0);
} }
const nextOptions: VariableOption[] = filteredOptions.map( const finalOptions: VariableOption[] = filteredOptions.map(
(x) => (x) =>
new VariableOption( new VariableOption(
x.label, x.label,
@ -150,8 +158,8 @@ export default function VariablePickerMenuPlugin({
}), }),
), ),
); );
return nextOptions; return finalOptions;
}, [options, queryString]); }, [extraOptions, options, queryString]);
const findItemByValue = useCallback( const findItemByValue = useCallback(
(value: string) => { (value: string) => {
@ -173,7 +181,7 @@ export default function VariablePickerMenuPlugin({
const onSelectOption = useCallback( const onSelectOption = useCallback(
( (
selectedOption: VariableOption | VariableInnerOption, selectedOption: VariableInnerOption,
nodeToRemove: TextNode | null, nodeToRemove: TextNode | null,
closeMenu: () => void, closeMenu: () => void,
) => { ) => {
@ -193,7 +201,11 @@ export default function VariablePickerMenuPlugin({
selectedOption.parentLabel as string | ReactNode, selectedOption.parentLabel as string | ReactNode,
selectedOption.icon as ReactNode, selectedOption.icon as ReactNode,
); );
selection.insertNodes([variableNode]); if (selectedOption.parentLabel === PromptIdentity) {
selection.insertText(selectedOption.value);
} else {
selection.insertNodes([variableNode]);
}
closeMenu(); closeMenu();
}); });
@ -269,7 +281,13 @@ export default function VariablePickerMenuPlugin({
return ( return (
<LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption> <LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption>
onQueryChange={setQueryString} onQueryChange={setQueryString}
onSelectOption={onSelectOption} onSelectOption={(option, textNodeContainingQuery, closeMenu) =>
onSelectOption(
option as VariableInnerOption, // Only the second level menu can be selected
textNodeContainingQuery,
closeMenu,
)
}
triggerFn={checkForTriggerMatch} triggerFn={checkForTriggerMatch}
options={buildNextOptions()} options={buildNextOptions()}
menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => { menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => {

View File

@ -25,6 +25,7 @@ const {
fetchAgentAvatar, fetchAgentAvatar,
fetchAgentLogs, fetchAgentLogs,
fetchExternalAgentInputs, fetchExternalAgentInputs,
prompt,
} = api; } = api;
const methods = { const methods = {
@ -112,6 +113,10 @@ const methods = {
url: fetchExternalAgentInputs, url: fetchExternalAgentInputs,
method: 'get', method: 'get',
}, },
fetchPrompt: {
url: prompt,
method: 'get',
},
} as const; } as const;
const agentService = registerNextServer<keyof typeof methods>(methods); const agentService = registerNextServer<keyof typeof methods>(methods);

View File

@ -164,6 +164,7 @@ export default {
`${api_host}/canvas/${canvasId}/sessions`, `${api_host}/canvas/${canvasId}/sessions`,
fetchExternalAgentInputs: (canvasId: string) => fetchExternalAgentInputs: (canvasId: string) =>
`${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`, `${ExternalApi}${api_host}/agentbots/${canvasId}/inputs`,
prompt: `${api_host}/canvas/prompts`,
// mcp server // mcp server
listMcpServer: `${api_host}/mcp_server/list`, listMcpServer: `${api_host}/mcp_server/list`,