Skip to content

Commit a880beb

Browse files
authored
Feat: Add a form for variable aggregation operators infiniflow#10427 (infiniflow#11095)
### What problem does this PR solve? Feat: Add a form for variable aggregation operators infiniflow#10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
1 parent 34283d4 commit a880beb

11 files changed

Lines changed: 289 additions & 22 deletions

File tree

web/src/pages/agent/canvas/node/retrieval-node.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import classNames from 'classnames';
77
import { get } from 'lodash';
88
import { memo } from 'react';
99
import { NodeHandleId } from '../../constant';
10-
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
10+
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
1111
import { CommonHandle, LeftEndHandle } from './handle';
1212
import styles from './index.less';
1313
import NodeHeader from './node-header';
@@ -23,7 +23,7 @@ function InnerRetrievalNode({
2323
const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
2424
const { list: knowledgeList } = useFetchKnowledgeList(true);
2525

26-
const getLabel = useGetVariableLabelByValue(id);
26+
const { getLabel } = useGetVariableLabelOrTypeByValue(id);
2727

2828
return (
2929
<ToolBar selected={selected} id={id} label={data.label}>

web/src/pages/agent/canvas/node/switch-node.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { LogicalOperatorIcon } from '@/hooks/logic-hooks/use-build-operator-opti
44
import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow';
55
import { NodeProps, Position } from '@xyflow/react';
66
import { memo, useCallback } from 'react';
7-
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
7+
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
88
import { CommonHandle, LeftEndHandle } from './handle';
99
import { RightHandleStyle } from './handle-icon';
1010
import NodeHeader from './node-header';
@@ -27,7 +27,7 @@ const ConditionBlock = ({
2727
nodeId,
2828
}: { condition: ISwitchCondition } & { nodeId: string }) => {
2929
const items = condition?.items ?? [];
30-
const getLabel = useGetVariableLabelByValue(nodeId);
30+
const { getLabel } = useGetVariableLabelOrTypeByValue(nodeId);
3131

3232
const renderOperatorIcon = useCallback((operator?: string) => {
3333
const item = SwitchOperatorOptions.find((x) => x.value === operator);

web/src/pages/agent/form-sheet/form-config-map.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import TokenizerForm from '../form/tokenizer-form';
3838
import ToolForm from '../form/tool-form';
3939
import TuShareForm from '../form/tushare-form';
4040
import UserFillUpForm from '../form/user-fill-up-form';
41+
import VariableAggregatorForm from '../form/variable-aggregator-form';
4142
import VariableAssignerForm from '../form/variable-assigner-form';
4243
import WenCaiForm from '../form/wencai-form';
4344
import WikipediaForm from '../form/wikipedia-form';
@@ -186,4 +187,8 @@ export const FormConfigMap = {
186187
[Operator.VariableAssigner]: {
187188
component: VariableAssignerForm,
188189
},
190+
191+
[Operator.VariableAggregator]: {
192+
component: VariableAggregatorForm,
193+
},
189194
};

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { useTranslation } from 'react-i18next';
2626
import { z } from 'zod';
2727
import {
2828
AgentExceptionMethod,
29-
JsonSchemaDataType,
3029
NodeHandleId,
3130
VariableType,
3231
initialAgentValues,
@@ -158,7 +157,6 @@ function AgentForm({ node }: INextOperatorForm) {
158157
placeholder={t('flow.messagePlaceholder')}
159158
showToolbar={true}
160159
extraOptions={extraOptions}
161-
types={[JsonSchemaDataType.String]}
162160
></PromptEditor>
163161
</FormControl>
164162
</FormItem>
@@ -176,7 +174,6 @@ function AgentForm({ node }: INextOperatorForm) {
176174
<PromptEditor
177175
{...field}
178176
showToolbar={true}
179-
types={[JsonSchemaDataType.String]}
180177
></PromptEditor>
181178
</section>
182179
</FormControl>

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,13 @@ export function StructuredOutputSecondaryMenu({
5353

5454
const renderAgentStructuredOutput = useCallback(
5555
(values: any, option: { label: ReactNode; value: string }) => {
56-
if (isPlainObject(values) && 'properties' in values) {
56+
const properties =
57+
get(values, 'properties') || get(values, 'items.properties');
58+
59+
if (isPlainObject(values) && properties) {
5760
return (
5861
<ul className="border-l">
59-
{Object.entries(values.properties).map(([key, value]) => {
62+
{Object.entries(properties).map(([key, value]) => {
6063
const nextOption = {
6164
label: option.label + `.${key}`,
6265
value: option.value + `.${key}`,
@@ -79,8 +82,9 @@ export function StructuredOutputSecondaryMenu({
7982
{key}
8083
<span className="text-text-secondary">{dataType}</span>
8184
</div>
82-
{dataType === JsonSchemaDataType.Object &&
83-
renderAgentStructuredOutput(value, nextOption)}
85+
{[JsonSchemaDataType.Object, JsonSchemaDataType.Array].some(
86+
(x) => x === dataType,
87+
) && renderAgentStructuredOutput(value, nextOption)}
8488
</li>
8589
);
8690
}

web/src/pages/agent/form/invoke-form/variable-table.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
} from '@/components/ui/tooltip';
3333
import { cn } from '@/lib/utils';
3434
import { useTranslation } from 'react-i18next';
35-
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
35+
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
3636
import { VariableFormSchemaType } from './schema';
3737

3838
interface IProps {
@@ -49,7 +49,7 @@ export function VariableTable({
4949
nodeId,
5050
}: IProps) {
5151
const { t } = useTranslation();
52-
const getLabel = useGetVariableLabelByValue(nodeId!);
52+
const { getLabel } = useGetVariableLabelOrTypeByValue(nodeId!);
5353

5454
const [sorting, setSorting] = React.useState<SortingState>([]);
5555
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { memo } from 'react';
1414
import { useFieldArray, useForm } from 'react-hook-form';
1515
import { useTranslation } from 'react-i18next';
1616
import { z } from 'zod';
17-
import { JsonSchemaDataType } from '../../constant';
1817
import { INextOperatorForm } from '../../interface';
1918
import { FormWrapper } from '../components/form-wrapper';
2019
import { PromptEditor } from '../components/prompt-editor';
@@ -63,11 +62,9 @@ function MessageForm({ node }: INextOperatorForm) {
6362
render={({ field }) => (
6463
<FormItem className="flex-1">
6564
<FormControl>
66-
{/* <Textarea {...field}> </Textarea> */}
6765
<PromptEditor
6866
{...field}
6967
placeholder={t('flow.messagePlaceholder')}
70-
types={[JsonSchemaDataType.String]}
7168
></PromptEditor>
7269
</FormControl>
7370
</FormItem>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { RAGFlowFormItem } from '@/components/ragflow-form';
2+
import { Button } from '@/components/ui/button';
3+
import { Input } from '@/components/ui/input';
4+
import { Plus, Trash2 } from 'lucide-react';
5+
import { useFieldArray, useFormContext } from 'react-hook-form';
6+
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
7+
import { QueryVariable } from '../components/query-variable';
8+
9+
type DynamicGroupVariableProps = {
10+
name: string;
11+
parentIndex: number;
12+
removeParent: (index: number) => void;
13+
};
14+
15+
export function DynamicGroupVariable({
16+
name,
17+
parentIndex,
18+
removeParent,
19+
}: DynamicGroupVariableProps) {
20+
const form = useFormContext();
21+
22+
const variableFieldName = `${name}.variables`;
23+
24+
const { getType } = useGetVariableLabelOrTypeByValue();
25+
26+
const { fields, remove, append } = useFieldArray({
27+
name: variableFieldName,
28+
control: form.control,
29+
});
30+
31+
const firstValue = form.getValues(`${variableFieldName}.0.value`);
32+
const firstType = getType(firstValue);
33+
34+
return (
35+
<section className="py-3 group space-y-3">
36+
<div className="flex items-center justify-between">
37+
<div className="flex items-center gap-3">
38+
<RAGFlowFormItem name={`${name}.group_name`} className="w-32">
39+
<Input></Input>
40+
</RAGFlowFormItem>
41+
42+
<Button
43+
variant={'ghost'}
44+
type="button"
45+
className="hidden group-hover:block"
46+
onClick={() => removeParent(parentIndex)}
47+
>
48+
<Trash2 />
49+
</Button>
50+
</div>
51+
<div className="flex gap-2 items-center">
52+
{firstType && (
53+
<span className="text-text-secondary border px-1 rounded-md">
54+
{firstType}
55+
</span>
56+
)}
57+
<Button
58+
variant={'ghost'}
59+
type="button"
60+
onClick={() => append({ value: '' })}
61+
>
62+
<Plus />
63+
</Button>
64+
</div>
65+
</div>
66+
67+
<section className="space-y-3">
68+
{fields.map((field, index) => (
69+
<div key={field.id} className="flex gap-2 items-center">
70+
<QueryVariable
71+
name={`${variableFieldName}.${index}.value`}
72+
className="flex-1 min-w-0"
73+
hideLabel
74+
types={firstType && fields.length > 1 ? [firstType] : []}
75+
></QueryVariable>
76+
<Button
77+
variant={'ghost'}
78+
type="button"
79+
onClick={() => remove(index)}
80+
>
81+
<Trash2 />
82+
</Button>
83+
</div>
84+
))}
85+
</section>
86+
</section>
87+
);
88+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { BlockButton } from '@/components/ui/button';
2+
import { Form } from '@/components/ui/form';
3+
import { Separator } from '@/components/ui/separator';
4+
import { zodResolver } from '@hookform/resolvers/zod';
5+
import { memo } from 'react';
6+
import { useFieldArray, useForm } from 'react-hook-form';
7+
import { useTranslation } from 'react-i18next';
8+
import { z } from 'zod';
9+
import { initialDataOperationsValues } from '../../constant';
10+
import { useFormValues } from '../../hooks/use-form-values';
11+
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
12+
import { INextOperatorForm } from '../../interface';
13+
import { buildOutputList } from '../../utils/build-output-list';
14+
import { FormWrapper } from '../components/form-wrapper';
15+
import { Output } from '../components/output';
16+
import { DynamicGroupVariable } from './dynamic-group-variable';
17+
18+
export const RetrievalPartialSchema = {
19+
groups: z.array(
20+
z.object({
21+
group_name: z.string(),
22+
variables: z.array(z.object({ value: z.string().optional() })),
23+
}),
24+
),
25+
operations: z.string(),
26+
};
27+
28+
export const FormSchema = z.object(RetrievalPartialSchema);
29+
30+
export type DataOperationsFormSchemaType = z.infer<typeof FormSchema>;
31+
32+
const outputList = buildOutputList(initialDataOperationsValues.outputs);
33+
34+
function VariableAggregatorForm({ node }: INextOperatorForm) {
35+
const { t } = useTranslation();
36+
37+
const defaultValues = useFormValues(initialDataOperationsValues, node);
38+
39+
const form = useForm<DataOperationsFormSchemaType>({
40+
defaultValues: defaultValues,
41+
mode: 'onChange',
42+
resolver: zodResolver(FormSchema),
43+
shouldUnregister: true,
44+
});
45+
46+
const { fields, remove, append } = useFieldArray({
47+
name: 'groups',
48+
control: form.control,
49+
});
50+
51+
useWatchFormChange(node?.id, form, true);
52+
53+
return (
54+
<Form {...form}>
55+
<FormWrapper>
56+
<section className="divide-y">
57+
{fields.map((field, idx) => (
58+
<DynamicGroupVariable
59+
key={field.id}
60+
name={`groups.${idx}`}
61+
parentIndex={idx}
62+
removeParent={remove}
63+
></DynamicGroupVariable>
64+
))}
65+
</section>
66+
<BlockButton
67+
onClick={() =>
68+
append({ group_name: `Group ${fields.length}`, variables: [] })
69+
}
70+
>
71+
{t('common.add')}
72+
</BlockButton>
73+
<Separator />
74+
75+
<Output list={outputList} isFormRequired></Output>
76+
</FormWrapper>
77+
</Form>
78+
);
79+
}
80+
81+
export default memo(VariableAggregatorForm);

web/src/pages/agent/hooks/use-build-structured-output.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { get } from 'lodash';
1+
import { get, isPlainObject } from 'lodash';
22
import { ReactNode, useCallback } from 'react';
3-
import { AgentStructuredOutputField, Operator } from '../constant';
3+
import {
4+
AgentStructuredOutputField,
5+
JsonSchemaDataType,
6+
Operator,
7+
} from '../constant';
48
import useGraphStore from '../store';
59

610
function getNodeId(value: string) {
@@ -82,3 +86,70 @@ export function useFindAgentStructuredOutputLabel() {
8286

8387
return findAgentStructuredOutputLabel;
8488
}
89+
90+
export function useFindAgentStructuredOutputTypeByValue() {
91+
const { getOperatorTypeFromId } = useGraphStore((state) => state);
92+
const filterStructuredOutput = useGetStructuredOutputByValue();
93+
94+
const findTypeByValue = useCallback(
95+
(
96+
values: unknown,
97+
target: string,
98+
path: string = '',
99+
): string | undefined => {
100+
const properties =
101+
get(values, 'properties') || get(values, 'items.properties');
102+
103+
if (isPlainObject(values) && properties) {
104+
for (const [key, value] of Object.entries(properties)) {
105+
const nextPath = path ? `${path}.${key}` : key;
106+
const dataType = get(value, 'type');
107+
108+
if (nextPath === target) {
109+
return dataType;
110+
}
111+
112+
if (
113+
[JsonSchemaDataType.Object, JsonSchemaDataType.Array].some(
114+
(x) => x === dataType,
115+
)
116+
) {
117+
const type = findTypeByValue(value, target, nextPath);
118+
if (type) {
119+
return type;
120+
}
121+
}
122+
}
123+
}
124+
},
125+
[],
126+
);
127+
128+
const findAgentStructuredOutputTypeByValue = useCallback(
129+
(value?: string) => {
130+
if (!value) {
131+
return;
132+
}
133+
const fields = value.split('@');
134+
const nodeId = fields.at(0);
135+
const jsonSchema = filterStructuredOutput(value);
136+
137+
if (
138+
getOperatorTypeFromId(nodeId) === Operator.Agent &&
139+
fields.at(1)?.startsWith(AgentStructuredOutputField)
140+
) {
141+
const jsonSchemaFields = fields
142+
.at(1)
143+
?.slice(AgentStructuredOutputField.length + 1);
144+
145+
if (jsonSchemaFields) {
146+
const type = findTypeByValue(jsonSchema, jsonSchemaFields);
147+
return type;
148+
}
149+
}
150+
},
151+
[filterStructuredOutput, findTypeByValue, getOperatorTypeFromId],
152+
);
153+
154+
return findAgentStructuredOutputTypeByValue;
155+
}

0 commit comments

Comments
 (0)