mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 12:32:30 +08:00
### 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:
@ -81,7 +81,6 @@ export function AccordionOperators({
|
||||
Operator.DataOperations,
|
||||
Operator.VariableAssigner,
|
||||
Operator.ListOperations,
|
||||
Operator.VariableAssigner,
|
||||
Operator.VariableAggregator,
|
||||
]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
|
||||
@ -463,21 +463,14 @@ export const initialAgentValues = {
|
||||
tools: [],
|
||||
mcp: [],
|
||||
cite: true,
|
||||
showStructuredOutput: false,
|
||||
[AgentStructuredOutputField]: {},
|
||||
outputs: {
|
||||
// structured_output: {
|
||||
// topic: {
|
||||
// type: 'string',
|
||||
// description:
|
||||
// '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.',
|
||||
// enum: ['general', 'news'],
|
||||
// default: 'general',
|
||||
// },
|
||||
// },
|
||||
content: {
|
||||
type: 'string',
|
||||
value: '',
|
||||
},
|
||||
[AgentStructuredOutputField]: {},
|
||||
// [AgentStructuredOutputField]: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -81,7 +81,8 @@ function MessageForm({ node }: INextOperatorForm) {
|
||||
)}
|
||||
{...field}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t('flow.messagePlaceholder')}
|
||||
placeholder={t('common.selectPlaceholder')}
|
||||
allowClear
|
||||
></RAGFlowSelect>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { getStructuredDatatype } from '@/utils/canvas-util';
|
||||
import { get, isPlainObject } from 'lodash';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import {
|
||||
@ -106,10 +107,10 @@ export function useFindAgentStructuredOutputTypeByValue() {
|
||||
if (isPlainObject(values) && properties) {
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
const nextPath = path ? `${path}.${key}` : key;
|
||||
const dataType = get(value, 'type');
|
||||
const { dataType, compositeDataType } = getStructuredDatatype(value);
|
||||
|
||||
if (nextPath === target) {
|
||||
return dataType;
|
||||
return compositeDataType;
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { JSONSchema } from '@/components/jsonjoy-builder';
|
||||
import { getStructuredDatatype } from '@/utils/canvas-util';
|
||||
import { get, isPlainObject, toLower } from 'lodash';
|
||||
import { JsonSchemaDataType } from '../constant';
|
||||
|
||||
function predicate(types: string[], type: string) {
|
||||
return types.some((x) => toLower(x) === toLower(type));
|
||||
function predicate(types: string[], value: unknown) {
|
||||
return types.some(
|
||||
(x) =>
|
||||
toLower(x) === toLower(getStructuredDatatype(value).compositeDataType),
|
||||
);
|
||||
}
|
||||
|
||||
export function hasSpecificTypeChild(
|
||||
@ -12,7 +16,7 @@ export function hasSpecificTypeChild(
|
||||
) {
|
||||
if (Array.isArray(data)) {
|
||||
for (const value of data) {
|
||||
if (isPlainObject(value) && predicate(types, value.type)) {
|
||||
if (isPlainObject(value) && predicate(types, value)) {
|
||||
return true;
|
||||
}
|
||||
if (hasSpecificTypeChild(value, types)) {
|
||||
@ -23,7 +27,11 @@ export function hasSpecificTypeChild(
|
||||
|
||||
if (isPlainObject(data)) {
|
||||
for (const value of Object.values(data)) {
|
||||
if (isPlainObject(value) && predicate(types, value.type)) {
|
||||
if (
|
||||
isPlainObject(value) &&
|
||||
predicate(types, value) &&
|
||||
get(data, 'type') !== JsonSchemaDataType.Array
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
import { BaseNode } from '@/interfaces/database/agent';
|
||||
|
||||
import { Edge } from '@xyflow/react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { get, isEmpty } from 'lodash';
|
||||
import { ComponentType, ReactNode } from 'react';
|
||||
|
||||
export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
|
||||
@ -87,3 +87,15 @@ export function buildNodeOutputOptions({
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getStructuredDatatype(value: Record<string, any> | unknown) {
|
||||
const dataType = get(value, 'type');
|
||||
const arrayItemsType = get(value, 'items.type', JsonSchemaDataType.String);
|
||||
|
||||
const compositeDataType =
|
||||
dataType === JsonSchemaDataType.Array
|
||||
? `${dataType}<${arrayItemsType}>`
|
||||
: dataType;
|
||||
|
||||
return { dataType, compositeDataType };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user