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',
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<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',
sqlStatementTip:
'Write your SQL query here. You can use variables, raw SQL, or mix both using variable syntax.',
frameworkPrompts: 'Framework Prompts',
},
llmTools: {
bad_calculator: {

View File

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

View File

@ -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}
></PromptEditor>
</FormControl>
</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 { 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<VariablePickerMenuPluginProps, 'extraOptions'>;
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}
/>
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin>
<VariablePickerMenuPlugin
value={value}
extraOptions={extraOptions}
></VariablePickerMenuPlugin>
<PasteHandlerPlugin />
<VariableOnChangePlugin
onChange={onValueChange}

View File

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

View File

@ -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.

View File

@ -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<string | null>('');
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 (
<LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption>
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 }) => {

View File

@ -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<keyof typeof methods>(methods);

View File

@ -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`,