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 = {
|
export const initialIterationValues = {
|
||||||
items_ref: '',
|
items_ref: '',
|
||||||
|
outputs: {},
|
||||||
};
|
};
|
||||||
export const initialIterationStartValues = {
|
export const initialIterationStartValues = {
|
||||||
outputs: {
|
outputs: {
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
export type OutputType = {
|
export type OutputType = {
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OutputProps = {
|
type OutputProps = {
|
||||||
list: Array<OutputType>;
|
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) {
|
export function Output({ list }: OutputProps) {
|
||||||
return (
|
return (
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
|
|||||||
@ -14,19 +14,18 @@ import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
|
|||||||
|
|
||||||
type QueryVariableProps = { name?: string; type?: VariableType };
|
type QueryVariableProps = { name?: string; type?: VariableType };
|
||||||
|
|
||||||
export function QueryVariable({
|
export function QueryVariable({ name = 'query', type }: QueryVariableProps) {
|
||||||
name = 'query',
|
|
||||||
type = VariableType.String,
|
|
||||||
}: QueryVariableProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const form = useFormContext();
|
const form = useFormContext();
|
||||||
|
|
||||||
const nextOptions = useBuildQueryVariableOptions();
|
const nextOptions = useBuildQueryVariableOptions();
|
||||||
|
|
||||||
const finalOptions = useMemo(() => {
|
const finalOptions = useMemo(() => {
|
||||||
return nextOptions.map((x) => {
|
return type
|
||||||
return { ...x, options: x.options.filter((y) => y.type === type) };
|
? nextOptions.map((x) => {
|
||||||
});
|
return { ...x, options: x.options.filter((y) => y.type === type) };
|
||||||
|
})
|
||||||
|
: nextOptions;
|
||||||
}, [nextOptions, type]);
|
}, [nextOptions, type]);
|
||||||
|
|
||||||
return (
|
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 { Form } from '@/components/ui/form';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { initialRetrievalValues, VariableType } from '../../constant';
|
import { VariableType } from '../../constant';
|
||||||
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
|
|
||||||
import { INextOperatorForm } from '../../interface';
|
import { INextOperatorForm } from '../../interface';
|
||||||
import { Output } from '../components/output';
|
import { Output } from '../components/output';
|
||||||
import { QueryVariable } from '../components/query-variable';
|
import { QueryVariable } from '../components/query-variable';
|
||||||
|
import { DynamicOutput } from './dynamic-output';
|
||||||
|
import { OutputArray } from './interface';
|
||||||
import { useValues } from './use-values';
|
import { useValues } from './use-values';
|
||||||
|
import { useWatchFormChange } from './use-watch-form-change';
|
||||||
|
|
||||||
const FormSchema = z.object({
|
const FormSchema = z.object({
|
||||||
query: z.string().optional(),
|
query: z.string().optional(),
|
||||||
similarity_threshold: z.coerce.number(),
|
outputs: z.array(z.object({ name: z.string(), value: z.any() })).optional(),
|
||||||
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(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const IterationForm = ({ node }: INextOperatorForm) => {
|
const IterationForm = ({ node }: INextOperatorForm) => {
|
||||||
const outputList = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: 'formalized_content',
|
|
||||||
type: initialRetrievalValues.outputs.formalized_content.type,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const defaultValues = useValues(node);
|
const defaultValues = useValues(node);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
@ -39,6 +26,15 @@ const IterationForm = ({ node }: INextOperatorForm) => {
|
|||||||
resolver: zodResolver(FormSchema),
|
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);
|
useWatchFormChange(node?.id, form);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -55,6 +51,7 @@ const IterationForm = ({ node }: INextOperatorForm) => {
|
|||||||
type={VariableType.Array}
|
type={VariableType.Array}
|
||||||
></QueryVariable>
|
></QueryVariable>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
|
<DynamicOutput node={node}></DynamicOutput>
|
||||||
<Output list={outputList}></Output>
|
<Output list={outputList}></Output>
|
||||||
</form>
|
</form>
|
||||||
</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 { isEmpty } from 'lodash';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { initialIterationValues } from '../../constant';
|
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) {
|
export function useValues(node?: RAGFlowNodeType) {
|
||||||
const defaultValues = useMemo(
|
|
||||||
() => ({
|
|
||||||
...initialIterationValues,
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const values = useMemo(() => {
|
const values = useMemo(() => {
|
||||||
const formData = node?.data?.form;
|
const formData = node?.data?.form;
|
||||||
|
|
||||||
if (isEmpty(formData)) {
|
if (isEmpty(formData)) {
|
||||||
return defaultValues;
|
return { ...initialIterationValues, outputs: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
return formData;
|
return { ...formData, outputs: convertToArray(formData.outputs) };
|
||||||
}, [defaultValues, node?.data?.form]);
|
}, [node?.data?.form]);
|
||||||
|
|
||||||
return values;
|
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> = {},
|
outputs: Record<string, any> = {},
|
||||||
nodeId?: string,
|
nodeId?: string,
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user