mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### What problem does this PR solve? Feat: Customize the output variable name of the loop operator #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -646,6 +646,7 @@ export const initialEmailValues = {
|
||||
|
||||
export const initialIterationValues = {
|
||||
items_ref: '',
|
||||
outputs: {},
|
||||
};
|
||||
export const initialIterationStartValues = {
|
||||
outputs: {
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
export type OutputType = {
|
||||
title: string;
|
||||
type: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type OutputProps = {
|
||||
list: Array<OutputType>;
|
||||
};
|
||||
|
||||
export function transferOutputs(outputs: Record<string, any>) {
|
||||
return Object.entries(outputs).map(([key, value]) => ({
|
||||
title: key,
|
||||
type: value?.type,
|
||||
}));
|
||||
}
|
||||
|
||||
export function Output({ list }: OutputProps) {
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
|
||||
@ -14,19 +14,18 @@ import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
||||
|
||||
type QueryVariableProps = { name?: string; type?: VariableType };
|
||||
|
||||
export function QueryVariable({
|
||||
name = 'query',
|
||||
type = VariableType.String,
|
||||
}: QueryVariableProps) {
|
||||
export function QueryVariable({ name = 'query', type }: QueryVariableProps) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
const nextOptions = useBuildQueryVariableOptions();
|
||||
|
||||
const finalOptions = useMemo(() => {
|
||||
return nextOptions.map((x) => {
|
||||
return { ...x, options: x.options.filter((y) => y.type === type) };
|
||||
});
|
||||
return type
|
||||
? nextOptions.map((x) => {
|
||||
return { ...x, options: x.options.filter((y) => y.type === type) };
|
||||
})
|
||||
: nextOptions;
|
||||
}, [nextOptions, type]);
|
||||
|
||||
return (
|
||||
|
||||
97
web/src/pages/agent/form/iteration-form/dynamic-output.tsx
Normal file
97
web/src/pages/agent/form/iteration-form/dynamic-output.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import { FormContainer } from '@/components/form-container';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { BlockButton, Button } from '@/components/ui/button';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { X } from 'lucide-react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useBuildSubNodeOutputOptions } from './use-build-options';
|
||||
|
||||
interface IProps {
|
||||
node?: RAGFlowNodeType;
|
||||
}
|
||||
|
||||
export function DynamicOutputForm({ node }: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const options = useBuildSubNodeOutputOptions(node?.id);
|
||||
const name = 'outputs';
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{fields.map((field, index) => {
|
||||
const typeField = `${name}.${index}.name`;
|
||||
return (
|
||||
<div key={field.id} className="flex items-center gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={typeField}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t('common.pleaseInput')}
|
||||
></Input>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator className="w-3 text-text-sub-title" />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`${name}.${index}.ref`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-2/5">
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
options={options}
|
||||
{...field}
|
||||
></SelectWithSearch>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||
<X className="text-text-sub-title-invert " />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<BlockButton onClick={() => append({ name: '', ref: undefined })}>
|
||||
Add
|
||||
</BlockButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function VariableTitle({ title }: { title: ReactNode }) {
|
||||
return <div className="font-medium text-text-title pb-2">{title}</div>;
|
||||
}
|
||||
|
||||
export function DynamicOutput({ node }: IProps) {
|
||||
return (
|
||||
<FormContainer>
|
||||
<VariableTitle title={'Output'}></VariableTitle>
|
||||
<DynamicOutputForm node={node}></DynamicOutputForm>
|
||||
</FormContainer>
|
||||
);
|
||||
}
|
||||
@ -2,36 +2,23 @@ import { FormContainer } from '@/components/form-container';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMemo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { initialRetrievalValues, VariableType } from '../../constant';
|
||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
||||
import { VariableType } from '../../constant';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
import { Output } from '../components/output';
|
||||
import { QueryVariable } from '../components/query-variable';
|
||||
import { DynamicOutput } from './dynamic-output';
|
||||
import { OutputArray } from './interface';
|
||||
import { useValues } from './use-values';
|
||||
import { useWatchFormChange } from './use-watch-form-change';
|
||||
|
||||
const FormSchema = z.object({
|
||||
query: z.string().optional(),
|
||||
similarity_threshold: z.coerce.number(),
|
||||
keywords_similarity_weight: z.coerce.number(),
|
||||
top_n: z.coerce.number(),
|
||||
top_k: z.coerce.number(),
|
||||
kb_ids: z.array(z.string()),
|
||||
rerank_id: z.string(),
|
||||
empty_response: z.string(),
|
||||
outputs: z.array(z.object({ name: z.string(), value: z.any() })).optional(),
|
||||
});
|
||||
|
||||
const IterationForm = ({ node }: INextOperatorForm) => {
|
||||
const outputList = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: 'formalized_content',
|
||||
type: initialRetrievalValues.outputs.formalized_content.type,
|
||||
},
|
||||
];
|
||||
}, []);
|
||||
|
||||
const defaultValues = useValues(node);
|
||||
|
||||
const form = useForm({
|
||||
@ -39,6 +26,15 @@ const IterationForm = ({ node }: INextOperatorForm) => {
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
|
||||
const outputs: OutputArray = useWatch({
|
||||
control: form?.control,
|
||||
name: 'outputs',
|
||||
});
|
||||
|
||||
const outputList = useMemo(() => {
|
||||
return outputs.map((x) => ({ title: x.name, type: x?.type }));
|
||||
}, [outputs]);
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
@ -55,6 +51,7 @@ const IterationForm = ({ node }: INextOperatorForm) => {
|
||||
type={VariableType.Array}
|
||||
></QueryVariable>
|
||||
</FormContainer>
|
||||
<DynamicOutput node={node}></DynamicOutput>
|
||||
<Output list={outputList}></Output>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
2
web/src/pages/agent/form/iteration-form/interface.ts
Normal file
2
web/src/pages/agent/form/iteration-form/interface.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type OutputArray = Array<{ name: string; ref: string; type?: string }>;
|
||||
export type OutputObject = Record<string, { ref: string }>;
|
||||
31
web/src/pages/agent/form/iteration-form/use-build-options.ts
Normal file
31
web/src/pages/agent/form/iteration-form/use-build-options.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { Operator } from '../../constant';
|
||||
import { buildOutputOptions } from '../../hooks/use-get-begin-query';
|
||||
import useGraphStore from '../../store';
|
||||
|
||||
export function useBuildSubNodeOutputOptions(nodeId?: string) {
|
||||
const { nodes } = useGraphStore((state) => state);
|
||||
|
||||
const nodeOutputOptions = useMemo(() => {
|
||||
if (!nodeId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const subNodeWithOutputList = nodes.filter(
|
||||
(x) =>
|
||||
x.parentId === nodeId &&
|
||||
x.data.label !== Operator.IterationStart &&
|
||||
!isEmpty(x.data?.form?.outputs),
|
||||
);
|
||||
|
||||
return subNodeWithOutputList.map((x) => ({
|
||||
label: x.data.name,
|
||||
value: x.id,
|
||||
title: x.data.name,
|
||||
options: buildOutputOptions(x.data.form.outputs, x.id),
|
||||
}));
|
||||
}, [nodeId, nodes]);
|
||||
|
||||
return nodeOutputOptions;
|
||||
}
|
||||
@ -2,24 +2,25 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { initialIterationValues } from '../../constant';
|
||||
import { OutputObject } from './interface';
|
||||
|
||||
function convertToArray(outputObject: OutputObject) {
|
||||
return Object.entries(outputObject).map(([key, value]) => ({
|
||||
name: key,
|
||||
ref: value.ref,
|
||||
}));
|
||||
}
|
||||
|
||||
export function useValues(node?: RAGFlowNodeType) {
|
||||
const defaultValues = useMemo(
|
||||
() => ({
|
||||
...initialIterationValues,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const values = useMemo(() => {
|
||||
const formData = node?.data?.form;
|
||||
|
||||
if (isEmpty(formData)) {
|
||||
return defaultValues;
|
||||
return { ...initialIterationValues, outputs: [] };
|
||||
}
|
||||
|
||||
return formData;
|
||||
}, [defaultValues, node?.data?.form]);
|
||||
return { ...formData, outputs: convertToArray(formData.outputs) };
|
||||
}, [node?.data?.form]);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { useEffect } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
import useGraphStore from '../../store';
|
||||
import { OutputArray, OutputObject } from './interface';
|
||||
|
||||
function transferToObject(list: OutputArray) {
|
||||
return list.reduce<OutputObject>((pre, cur) => {
|
||||
pre[cur.name] = { ref: cur.ref };
|
||||
return pre;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function useWatchFormChange(id?: string, form?: UseFormReturn) {
|
||||
let values = useWatch({ control: form?.control });
|
||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||
|
||||
useEffect(() => {
|
||||
// Manually triggered form updates are synchronized to the canvas
|
||||
if (id && form?.formState.isDirty) {
|
||||
values = form?.getValues();
|
||||
let nextValues: any = {
|
||||
...values,
|
||||
outputs: transferToObject(values.outputs),
|
||||
};
|
||||
|
||||
updateNodeForm(id, nextValues);
|
||||
}
|
||||
}, [form?.formState.isDirty, id, updateNodeForm, values]);
|
||||
}
|
||||
@ -58,7 +58,7 @@ function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
|
||||
}, []);
|
||||
}
|
||||
|
||||
function buildOutputOptions(
|
||||
export function buildOutputOptions(
|
||||
outputs: Record<string, any> = {},
|
||||
nodeId?: string,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user