Skip to content

Commit 4942a23

Browse files
authored
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)
1 parent d1716d8 commit 4942a23

File tree

13 files changed

+133
-75
lines changed

13 files changed

+133
-75
lines changed

web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ export function AccordionOperators({
8181
Operator.DataOperations,
8282
Operator.VariableAssigner,
8383
Operator.ListOperations,
84-
Operator.VariableAssigner,
8584
Operator.VariableAggregator,
8685
]}
8786
isCustomDropdown={isCustomDropdown}

web/src/pages/agent/constant/index.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -463,21 +463,14 @@ export const initialAgentValues = {
463463
tools: [],
464464
mcp: [],
465465
cite: true,
466+
showStructuredOutput: false,
467+
[AgentStructuredOutputField]: {},
466468
outputs: {
467-
// structured_output: {
468-
// topic: {
469-
// type: 'string',
470-
// description:
471-
// 'default:general. The category of the search.news is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. general is for broader, more general-purpose searches that may include a wide range of sources.',
472-
// enum: ['general', 'news'],
473-
// default: 'general',
474-
// },
475-
// },
476469
content: {
477470
type: 'string',
478471
value: '',
479472
},
480-
[AgentStructuredOutputField]: {},
473+
// [AgentStructuredOutputField]: {},
481474
},
482475
};
483476

web/src/pages/agent/form/agent-form/index.tsx

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
77
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
88
import { SelectWithSearch } from '@/components/originui/select-with-search';
9+
import { RAGFlowFormItem } from '@/components/ragflow-form';
910
import { Button } from '@/components/ui/button';
1011
import {
1112
Form,
@@ -15,6 +16,7 @@ import {
1516
FormLabel,
1617
} from '@/components/ui/form';
1718
import { Input, NumberInput } from '@/components/ui/input';
19+
import { Label } from '@/components/ui/label';
1820
import { Separator } from '@/components/ui/separator';
1921
import { Switch } from '@/components/ui/switch';
2022
import { LlmModelType } from '@/constants/knowledge';
@@ -26,9 +28,9 @@ import { useTranslation } from 'react-i18next';
2628
import { z } from 'zod';
2729
import {
2830
AgentExceptionMethod,
31+
AgentStructuredOutputField,
2932
NodeHandleId,
3033
VariableType,
31-
initialAgentValues,
3234
} from '../../constant';
3335
import { INextOperatorForm } from '../../interface';
3436
import useGraphStore from '../../store';
@@ -71,18 +73,20 @@ const FormSchema = z.object({
7173
exception_default_value: z.string().optional(),
7274
...LargeModelFilterFormSchema,
7375
cite: z.boolean().optional(),
76+
showStructuredOutput: z.boolean().optional(),
77+
[AgentStructuredOutputField]: z.record(z.any()),
7478
});
7579

7680
export type AgentFormSchemaType = z.infer<typeof FormSchema>;
7781

78-
const outputList = buildOutputList(initialAgentValues.outputs);
79-
8082
function AgentForm({ node }: INextOperatorForm) {
8183
const { t } = useTranslation();
8284
const { edges, deleteEdgesBySourceAndSourceHandle } = useGraphStore(
8385
(state) => state,
8486
);
8587

88+
const outputList = buildOutputList(node?.data.form.outputs);
89+
8690
const defaultValues = useValues(node);
8791

8892
const { extraOptions } = useBuildPromptExtraPromptOptions(edges, node?.id);
@@ -112,13 +116,18 @@ function AgentForm({ node }: INextOperatorForm) {
112116
name: 'exception_method',
113117
});
114118

119+
const showStructuredOutput = useWatch({
120+
control: form.control,
121+
name: 'showStructuredOutput',
122+
});
123+
115124
const {
116125
initialStructuredOutput,
117126
showStructuredOutputDialog,
118127
structuredOutputDialogVisible,
119128
hideStructuredOutputDialog,
120129
handleStructuredOutputDialogOk,
121-
} = useShowStructuredOutputDialog(node?.id);
130+
} = useShowStructuredOutputDialog(form);
122131

123132
useEffect(() => {
124133
if (exceptionMethod !== AgentExceptionMethod.Goto) {
@@ -275,18 +284,42 @@ function AgentForm({ node }: INextOperatorForm) {
275284
)}
276285
</section>
277286
</Collapse>
278-
<Output list={outputList}></Output>
279-
<section className="space-y-2">
280-
<div className="flex justify-between items-center">
281-
{t('flow.structuredOutput.structuredOutput')}
282-
<Button variant={'outline'} onClick={showStructuredOutputDialog}>
283-
{t('flow.structuredOutput.configuration')}
284-
</Button>
285-
</div>
286-
<StructuredOutputPanel
287-
value={initialStructuredOutput}
288-
></StructuredOutputPanel>
289-
</section>
287+
<RAGFlowFormItem name={AgentStructuredOutputField} className="hidden">
288+
<Input></Input>
289+
</RAGFlowFormItem>
290+
<Output list={outputList}>
291+
<RAGFlowFormItem name="showStructuredOutput">
292+
{(field) => (
293+
<div className="flex items-center space-x-2">
294+
<Label htmlFor="airplane-mode">
295+
{t('flow.structuredOutput.structuredOutput')}
296+
</Label>
297+
<Switch
298+
id="airplane-mode"
299+
checked={field.value}
300+
onCheckedChange={field.onChange}
301+
/>
302+
</div>
303+
)}
304+
</RAGFlowFormItem>
305+
</Output>
306+
{showStructuredOutput && (
307+
<section className="space-y-2">
308+
<div className="flex justify-between items-center">
309+
{t('flow.structuredOutput.structuredOutput')}
310+
<Button
311+
variant={'outline'}
312+
onClick={showStructuredOutputDialog}
313+
>
314+
{t('flow.structuredOutput.configuration')}
315+
</Button>
316+
</div>
317+
318+
<StructuredOutputPanel
319+
value={initialStructuredOutput}
320+
></StructuredOutputPanel>
321+
</section>
322+
)}
290323
</FormWrapper>
291324
</Form>
292325
{structuredOutputDialogVisible && (

web/src/pages/agent/form/agent-form/use-show-structured-output-dialog.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
import { JSONSchema } from '@/components/jsonjoy-builder';
2+
import { AgentStructuredOutputField } from '@/constants/agent';
23
import { useSetModalState } from '@/hooks/common-hooks';
34
import { useCallback } from 'react';
4-
import useGraphStore from '../../store';
5+
import { UseFormReturn } from 'react-hook-form';
56

6-
export function useShowStructuredOutputDialog(nodeId?: string) {
7+
export function useShowStructuredOutputDialog(form: UseFormReturn<any>) {
78
const {
89
visible: structuredOutputDialogVisible,
910
showModal: showStructuredOutputDialog,
1011
hideModal: hideStructuredOutputDialog,
1112
} = useSetModalState();
12-
const { updateNodeForm, getNode } = useGraphStore((state) => state);
1313

14-
const initialStructuredOutput = getNode(nodeId)?.data.form.outputs.structured;
14+
const initialStructuredOutput = form.getValues(AgentStructuredOutputField);
1515

1616
const handleStructuredOutputDialogOk = useCallback(
1717
(values: JSONSchema) => {
1818
// Sync data to canvas
19-
if (nodeId) {
20-
updateNodeForm(nodeId, values, ['outputs', 'structured']);
21-
}
19+
form.setValue(AgentStructuredOutputField, values);
2220
hideStructuredOutputDialog();
2321
},
24-
[hideStructuredOutputDialog, nodeId, updateNodeForm],
22+
[form, hideStructuredOutputDialog],
2523
);
2624

2725
return {

web/src/pages/agent/form/agent-form/use-watch-change.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import { omit } from 'lodash';
12
import { useEffect } from 'react';
23
import { UseFormReturn, useWatch } from 'react-hook-form';
3-
import { PromptRole } from '../../constant';
4+
import { AgentStructuredOutputField, PromptRole } from '../../constant';
45
import useGraphStore from '../../store';
56

67
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
@@ -16,6 +17,20 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
1617
prompts: [{ role: PromptRole.User, content: values.prompts }],
1718
};
1819

20+
if (values.showStructuredOutput) {
21+
nextValues = {
22+
...nextValues,
23+
outputs: {
24+
...values.outputs,
25+
[AgentStructuredOutputField]: values[AgentStructuredOutputField],
26+
},
27+
};
28+
} else {
29+
nextValues = {
30+
...nextValues,
31+
outputs: omit(values.outputs, [AgentStructuredOutputField]),
32+
};
33+
}
1934
updateNodeForm(id, nextValues);
2035
}
2136
}, [form?.formState.isDirty, id, updateNodeForm, values]);

web/src/pages/agent/form/components/output.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RAGFlowFormItem } from '@/components/ragflow-form';
22
import { Input } from '@/components/ui/input';
33
import { t } from 'i18next';
4+
import { PropsWithChildren } from 'react';
45
import { z } from 'zod';
56

67
export type OutputType = {
@@ -11,7 +12,7 @@ export type OutputType = {
1112
type OutputProps = {
1213
list: Array<OutputType>;
1314
isFormRequired?: boolean;
14-
};
15+
} & PropsWithChildren;
1516

1617
export function transferOutputs(outputs: Record<string, any>) {
1718
return Object.entries(outputs).map(([key, value]) => ({
@@ -24,10 +25,16 @@ export const OutputSchema = {
2425
outputs: z.record(z.any()),
2526
};
2627

27-
export function Output({ list, isFormRequired = false }: OutputProps) {
28+
export function Output({
29+
list,
30+
isFormRequired = false,
31+
children,
32+
}: OutputProps) {
2833
return (
2934
<section className="space-y-2">
30-
<div className="text-sm">{t('flow.output')}</div>
35+
<div className="text-sm flex items-center justify-between">
36+
{t('flow.output')} <span>{children}</span>
37+
</div>
3138
<ul>
3239
{list.map((x, idx) => (
3340
<li

web/src/pages/agent/form/components/structured-output-secondary-menu.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
HoverCardTrigger,
55
} from '@/components/ui/hover-card';
66
import { cn } from '@/lib/utils';
7+
import { getStructuredDatatype } from '@/utils/canvas-util';
78
import { get, isEmpty, isPlainObject } from 'lodash';
89
import { ChevronRight } from 'lucide-react';
910
import { PropsWithChildren, ReactNode, useCallback } from 'react';
@@ -62,22 +63,28 @@ export function StructuredOutputSecondaryMenu({
6263
value: option.value + `.${key}`,
6364
};
6465

65-
const dataType = get(value, 'type');
66+
const { dataType, compositeDataType } =
67+
getStructuredDatatype(value);
6668

6769
if (
6870
isEmpty(types) ||
6971
(!isEmpty(types) &&
70-
(types?.some((x) => x === dataType) ||
72+
(types?.some((x) => x === compositeDataType) ||
7173
hasSpecificTypeChild(value ?? {}, types)))
7274
) {
7375
return (
7476
<li key={key} className="pl-1">
7577
<div
76-
onClick={handleSubMenuClick(nextOption, dataType)}
78+
onClick={handleSubMenuClick(
79+
nextOption,
80+
compositeDataType,
81+
)}
7782
className="hover:bg-bg-card p-1 text-text-primary rounded-sm flex justify-between"
7883
>
7984
{key}
80-
<span className="text-text-secondary">{dataType}</span>
85+
<span className="text-text-secondary">
86+
{compositeDataType}
87+
</span>
8188
</div>
8289
{[JsonSchemaDataType.Object, JsonSchemaDataType.Array].some(
8390
(x) => x === dataType,
@@ -122,7 +129,7 @@ export function StructuredOutputSecondaryMenu({
122129
side="left"
123130
align="start"
124131
className={cn(
125-
'min-w-[140px] border border-border rounded-md shadow-lg p-0',
132+
'min-w-72 border border-border rounded-md shadow-lg p-0',
126133
)}
127134
>
128135
<section className="p-2">

web/src/pages/agent/form/message-form/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ function MessageForm({ node }: INextOperatorForm) {
8181
)}
8282
{...field}
8383
onValueChange={field.onChange}
84-
placeholder={t('flow.messagePlaceholder')}
84+
placeholder={t('common.selectPlaceholder')}
85+
allowClear
8586
></RAGFlowSelect>
8687
</FormControl>
8788
</FormItem>

web/src/pages/agent/form/message-form/use-values.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { RAGFlowNodeType } from '@/interfaces/database/flow';
22
import { isEmpty } from 'lodash';
33
import { useMemo } from 'react';
4-
import { ExportFileType, initialMessageValues } from '../../constant';
4+
import { initialMessageValues } from '../../constant';
55
import { convertToObjectArray } from '../../utils';
66

77
export function useValues(node?: RAGFlowNodeType) {
@@ -15,7 +15,6 @@ export function useValues(node?: RAGFlowNodeType) {
1515
return {
1616
...formData,
1717
content: convertToObjectArray(formData.content),
18-
output_format: formData.output_format || ExportFileType.PDF,
1918
};
2019
}, [node]);
2120

web/src/pages/agent/form/switch-form/index.tsx

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { FormContainer } from '@/components/form-container';
2-
import { SelectWithSearch } from '@/components/originui/select-with-search';
32
import { BlockButton, Button } from '@/components/ui/button';
43
import { Card, CardContent } from '@/components/ui/card';
54
import {
@@ -16,16 +15,15 @@ import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-ope
1615
import { cn } from '@/lib/utils';
1716
import { zodResolver } from '@hookform/resolvers/zod';
1817
import { t } from 'i18next';
19-
import { toLower } from 'lodash';
2018
import { X } from 'lucide-react';
2119
import { memo, useCallback, useMemo } from 'react';
2220
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
2321
import { useTranslation } from 'react-i18next';
2422
import { z } from 'zod';
25-
import { SwitchLogicOperatorOptions, VariableType } from '../../constant';
26-
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
23+
import { SwitchLogicOperatorOptions } from '../../constant';
2724
import { IOperatorForm } from '../../interface';
2825
import { FormWrapper } from '../components/form-wrapper';
26+
import { QueryVariable } from '../components/query-variable';
2927
import { useValues } from './use-values';
3028
import { useWatchFormChange } from './use-watch-change';
3129

@@ -47,19 +45,6 @@ function ConditionCards({
4745
}: ConditionCardsProps) {
4846
const form = useFormContext();
4947

50-
const nextOptions = useBuildQueryVariableOptions();
51-
52-
const finalOptions = useMemo(() => {
53-
return nextOptions.map((x) => {
54-
return {
55-
...x,
56-
options: x.options.filter(
57-
(y) => !toLower(y.type).includes(VariableType.Array),
58-
),
59-
};
60-
});
61-
}, [nextOptions]);
62-
6348
const switchOperatorOptions = useBuildSwitchOperatorOptions();
6449

6550
const name = `${parentName}.${ItemKey}`;
@@ -101,11 +86,11 @@ function ConditionCards({
10186
render={({ field }) => (
10287
<FormItem className="flex-1 min-w-0">
10388
<FormControl>
104-
<SelectWithSearch
89+
<QueryVariable
90+
pureQuery
10591
{...field}
106-
options={finalOptions}
107-
triggerClassName="text-accent-primary bg-transparent border-none truncate"
108-
></SelectWithSearch>
92+
hideLabel
93+
></QueryVariable>
10994
</FormControl>
11095
<FormMessage />
11196
</FormItem>

0 commit comments

Comments
 (0)