Feat: Add a switch to control the display of structured output to the agent form. #10427 (#11344)

### What problem does this PR solve?

Feat: Add a switch to control the display of structured output to the
agent form. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-18 18:58:36 +08:00
committed by GitHub
parent d1716d865a
commit 4942a23290
13 changed files with 133 additions and 75 deletions

View File

@ -6,6 +6,7 @@ import {
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Button } from '@/components/ui/button';
import {
Form,
@ -15,6 +16,7 @@ import {
FormLabel,
} from '@/components/ui/form';
import { Input, NumberInput } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { LlmModelType } from '@/constants/knowledge';
@ -26,9 +28,9 @@ import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
AgentExceptionMethod,
AgentStructuredOutputField,
NodeHandleId,
VariableType,
initialAgentValues,
} from '../../constant';
import { INextOperatorForm } from '../../interface';
import useGraphStore from '../../store';
@ -71,18 +73,20 @@ const FormSchema = z.object({
exception_default_value: z.string().optional(),
...LargeModelFilterFormSchema,
cite: z.boolean().optional(),
showStructuredOutput: z.boolean().optional(),
[AgentStructuredOutputField]: z.record(z.any()),
});
export type AgentFormSchemaType = z.infer<typeof FormSchema>;
const outputList = buildOutputList(initialAgentValues.outputs);
function AgentForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const { edges, deleteEdgesBySourceAndSourceHandle } = useGraphStore(
(state) => state,
);
const outputList = buildOutputList(node?.data.form.outputs);
const defaultValues = useValues(node);
const { extraOptions } = useBuildPromptExtraPromptOptions(edges, node?.id);
@ -112,13 +116,18 @@ function AgentForm({ node }: INextOperatorForm) {
name: 'exception_method',
});
const showStructuredOutput = useWatch({
control: form.control,
name: 'showStructuredOutput',
});
const {
initialStructuredOutput,
showStructuredOutputDialog,
structuredOutputDialogVisible,
hideStructuredOutputDialog,
handleStructuredOutputDialogOk,
} = useShowStructuredOutputDialog(node?.id);
} = useShowStructuredOutputDialog(form);
useEffect(() => {
if (exceptionMethod !== AgentExceptionMethod.Goto) {
@ -275,18 +284,42 @@ function AgentForm({ node }: INextOperatorForm) {
)}
</section>
</Collapse>
<Output list={outputList}></Output>
<section className="space-y-2">
<div className="flex justify-between items-center">
{t('flow.structuredOutput.structuredOutput')}
<Button variant={'outline'} onClick={showStructuredOutputDialog}>
{t('flow.structuredOutput.configuration')}
</Button>
</div>
<StructuredOutputPanel
value={initialStructuredOutput}
></StructuredOutputPanel>
</section>
<RAGFlowFormItem name={AgentStructuredOutputField} className="hidden">
<Input></Input>
</RAGFlowFormItem>
<Output list={outputList}>
<RAGFlowFormItem name="showStructuredOutput">
{(field) => (
<div className="flex items-center space-x-2">
<Label htmlFor="airplane-mode">
{t('flow.structuredOutput.structuredOutput')}
</Label>
<Switch
id="airplane-mode"
checked={field.value}
onCheckedChange={field.onChange}
/>
</div>
)}
</RAGFlowFormItem>
</Output>
{showStructuredOutput && (
<section className="space-y-2">
<div className="flex justify-between items-center">
{t('flow.structuredOutput.structuredOutput')}
<Button
variant={'outline'}
onClick={showStructuredOutputDialog}
>
{t('flow.structuredOutput.configuration')}
</Button>
</div>
<StructuredOutputPanel
value={initialStructuredOutput}
></StructuredOutputPanel>
</section>
)}
</FormWrapper>
</Form>
{structuredOutputDialogVisible && (

View File

@ -1,27 +1,25 @@
import { JSONSchema } from '@/components/jsonjoy-builder';
import { AgentStructuredOutputField } from '@/constants/agent';
import { useSetModalState } from '@/hooks/common-hooks';
import { useCallback } from 'react';
import useGraphStore from '../../store';
import { UseFormReturn } from 'react-hook-form';
export function useShowStructuredOutputDialog(nodeId?: string) {
export function useShowStructuredOutputDialog(form: UseFormReturn<any>) {
const {
visible: structuredOutputDialogVisible,
showModal: showStructuredOutputDialog,
hideModal: hideStructuredOutputDialog,
} = useSetModalState();
const { updateNodeForm, getNode } = useGraphStore((state) => state);
const initialStructuredOutput = getNode(nodeId)?.data.form.outputs.structured;
const initialStructuredOutput = form.getValues(AgentStructuredOutputField);
const handleStructuredOutputDialogOk = useCallback(
(values: JSONSchema) => {
// Sync data to canvas
if (nodeId) {
updateNodeForm(nodeId, values, ['outputs', 'structured']);
}
form.setValue(AgentStructuredOutputField, values);
hideStructuredOutputDialog();
},
[hideStructuredOutputDialog, nodeId, updateNodeForm],
[form, hideStructuredOutputDialog],
);
return {

View File

@ -1,6 +1,7 @@
import { omit } from 'lodash';
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { PromptRole } from '../../constant';
import { AgentStructuredOutputField, PromptRole } from '../../constant';
import useGraphStore from '../../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
@ -16,6 +17,20 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
prompts: [{ role: PromptRole.User, content: values.prompts }],
};
if (values.showStructuredOutput) {
nextValues = {
...nextValues,
outputs: {
...values.outputs,
[AgentStructuredOutputField]: values[AgentStructuredOutputField],
},
};
} else {
nextValues = {
...nextValues,
outputs: omit(values.outputs, [AgentStructuredOutputField]),
};
}
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);

View File

@ -1,6 +1,7 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { t } from 'i18next';
import { PropsWithChildren } from 'react';
import { z } from 'zod';
export type OutputType = {
@ -11,7 +12,7 @@ export type OutputType = {
type OutputProps = {
list: Array<OutputType>;
isFormRequired?: boolean;
};
} & PropsWithChildren;
export function transferOutputs(outputs: Record<string, any>) {
return Object.entries(outputs).map(([key, value]) => ({
@ -24,10 +25,16 @@ export const OutputSchema = {
outputs: z.record(z.any()),
};
export function Output({ list, isFormRequired = false }: OutputProps) {
export function Output({
list,
isFormRequired = false,
children,
}: OutputProps) {
return (
<section className="space-y-2">
<div className="text-sm">{t('flow.output')}</div>
<div className="text-sm flex items-center justify-between">
{t('flow.output')} <span>{children}</span>
</div>
<ul>
{list.map((x, idx) => (
<li

View File

@ -4,6 +4,7 @@ import {
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { cn } from '@/lib/utils';
import { getStructuredDatatype } from '@/utils/canvas-util';
import { get, isEmpty, isPlainObject } from 'lodash';
import { ChevronRight } from 'lucide-react';
import { PropsWithChildren, ReactNode, useCallback } from 'react';
@ -62,22 +63,28 @@ export function StructuredOutputSecondaryMenu({
value: option.value + `.${key}`,
};
const dataType = get(value, 'type');
const { dataType, compositeDataType } =
getStructuredDatatype(value);
if (
isEmpty(types) ||
(!isEmpty(types) &&
(types?.some((x) => x === dataType) ||
(types?.some((x) => x === compositeDataType) ||
hasSpecificTypeChild(value ?? {}, types)))
) {
return (
<li key={key} className="pl-1">
<div
onClick={handleSubMenuClick(nextOption, dataType)}
onClick={handleSubMenuClick(
nextOption,
compositeDataType,
)}
className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between"
>
{key}
<span className="text-text-secondary">{dataType}</span>
<span className="text-text-secondary">
{compositeDataType}
</span>
</div>
{[JsonSchemaDataType.Object, JsonSchemaDataType.Array].some(
(x) => x === dataType,
@ -122,7 +129,7 @@ export function StructuredOutputSecondaryMenu({
side="left"
align="start"
className={cn(
'min-w-[140px] border border-border rounded-md shadow-lg p-0',
'min-w-72 border border-border rounded-md shadow-lg p-0',
)}
>
<section className="p-2">

View File

@ -81,7 +81,8 @@ function MessageForm({ node }: INextOperatorForm) {
)}
{...field}
onValueChange={field.onChange}
placeholder={t('flow.messagePlaceholder')}
placeholder={t('common.selectPlaceholder')}
allowClear
></RAGFlowSelect>
</FormControl>
</FormItem>

View File

@ -1,7 +1,7 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { ExportFileType, initialMessageValues } from '../../constant';
import { initialMessageValues } from '../../constant';
import { convertToObjectArray } from '../../utils';
export function useValues(node?: RAGFlowNodeType) {
@ -15,7 +15,6 @@ export function useValues(node?: RAGFlowNodeType) {
return {
...formData,
content: convertToObjectArray(formData.content),
output_format: formData.output_format || ExportFileType.PDF,
};
}, [node]);

View File

@ -1,5 +1,4 @@
import { FormContainer } from '@/components/form-container';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { BlockButton, Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
@ -16,16 +15,15 @@ import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-ope
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { toLower } from 'lodash';
import { X } from 'lucide-react';
import { memo, useCallback, useMemo } from 'react';
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { SwitchLogicOperatorOptions, VariableType } from '../../constant';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
import { SwitchLogicOperatorOptions } from '../../constant';
import { IOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { QueryVariable } from '../components/query-variable';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
@ -47,19 +45,6 @@ function ConditionCards({
}: ConditionCardsProps) {
const form = useFormContext();
const nextOptions = useBuildQueryVariableOptions();
const finalOptions = useMemo(() => {
return nextOptions.map((x) => {
return {
...x,
options: x.options.filter(
(y) => !toLower(y.type).includes(VariableType.Array),
),
};
});
}, [nextOptions]);
const switchOperatorOptions = useBuildSwitchOperatorOptions();
const name = `${parentName}.${ItemKey}`;
@ -101,11 +86,11 @@ function ConditionCards({
render={({ field }) => (
<FormItem className="flex-1 min-w-0">
<FormControl>
<SelectWithSearch
<QueryVariable
pureQuery
{...field}
options={finalOptions}
triggerClassName="text-accent-primary bg-transparent border-none truncate"
></SelectWithSearch>
hideLabel
></QueryVariable>
</FormControl>
<FormMessage />
</FormItem>