mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-11-10 13:24:05 +08:00
fix: correct agent tool, schema, and save handling (#22089)
This commit is contained in:
1 parent
3f8dbf259b
commit
cf0551433d
9 files changed
+147
-31
No files matched your search
+37
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import ArrayEditor from './array-editor';
|
||||
|
||||
jest.mock('../type-editor', () => ({
|
||||
__esModule: true,
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
describe('ArrayEditor', () => {
|
||||
it('writes the latest uniqueItems switch value to the schema', async () => {
|
||||
const onChange = jest.fn();
|
||||
|
||||
render(
|
||||
<ArrayEditor
|
||||
schema={{ type: 'array', items: { type: 'string' } } as any}
|
||||
validationNode={undefined}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('switch'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ uniqueItems: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,13 +43,13 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
|
||||
);
|
||||
|
||||
// Handle validation settings change
|
||||
const handleValidationChange = () => {
|
||||
const handleValidationChange = (nextUniqueItems = uniqueItems) => {
|
||||
const validationProps: ObjectJSONSchema = {
|
||||
type: 'array',
|
||||
...(isBooleanSchema(schema) ? {} : schema),
|
||||
minItems: minItems,
|
||||
maxItems: maxItems,
|
||||
uniqueItems: uniqueItems || undefined,
|
||||
uniqueItems: nextUniqueItems || undefined,
|
||||
};
|
||||
|
||||
// Keep the items schema
|
||||
@@ -68,6 +68,10 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
|
||||
onChange(propsToKeep as ObjectJSONSchema);
|
||||
};
|
||||
|
||||
const handleValidationBlur = () => {
|
||||
handleValidationChange();
|
||||
};
|
||||
|
||||
// Handle item schema changes
|
||||
const handleItemSchemaChange = (updatedItemSchema: ObjectJSONSchema) => {
|
||||
const updatedSchema: ObjectJSONSchema = {
|
||||
@@ -125,7 +129,7 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
|
||||
setMinItems(value);
|
||||
// Don't update immediately to avoid too many rerenders
|
||||
}}
|
||||
onBlur={handleValidationChange}
|
||||
onBlur={handleValidationBlur}
|
||||
placeholder={t.arrayMinimumPlaceholder}
|
||||
className={cn('h-8', !!minMaxError && 'border-destructive')}
|
||||
/>
|
||||
@@ -150,7 +154,7 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
|
||||
setMaxItems(value);
|
||||
// Don't update immediately to avoid too many rerenders
|
||||
}}
|
||||
onBlur={handleValidationChange}
|
||||
onBlur={handleValidationBlur}
|
||||
placeholder={t.arrayMaximumPlaceholder}
|
||||
className={cn('h-8', !!minMaxError && 'border-destructive')}
|
||||
/>
|
||||
@@ -170,7 +174,7 @@ const ArrayEditor: React.FC<TypeEditorProps> = ({
|
||||
checked={uniqueItems}
|
||||
onCheckedChange={(checked) => {
|
||||
setUniqueItems(checked);
|
||||
setTimeout(handleValidationChange, 0);
|
||||
handleValidationChange(checked);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor={uniqueItemsId} className="cursor-pointer">
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useHandleForm = () => {
|
||||
globalVariables: param,
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
if (res?.code === 0) {
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -54,7 +54,7 @@ export const GlobalParamSheet = (props: IGlobalParamModalProps) => {
|
||||
const res = await saveGraph(undefined, {
|
||||
globalVariables: param,
|
||||
});
|
||||
if (res.code === 0) {
|
||||
if (res?.code === 0) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { Operator } from '../constant';
|
||||
import { useInitializeOperatorParams } from './use-add-node';
|
||||
import { useAgentToolInitialValues } from './use-agent-tool-initial-values';
|
||||
import { getQueritAgentInitialValues } from './querit-agent-initial-values';
|
||||
|
||||
jest.mock('./use-add-node', () => ({
|
||||
useInitializeOperatorParams: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getQueritAgentInitialValues', () => {
|
||||
it('creates Querit Agent params without runtime query or outputs', () => {
|
||||
expect(
|
||||
@@ -30,3 +38,27 @@ describe('getQueritAgentInitialValues', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useAgentToolInitialValues', () => {
|
||||
it('does not reuse canvas defaults for a Tavily Search agent tool', () => {
|
||||
jest.mocked(useInitializeOperatorParams).mockReturnValue({
|
||||
initialFormValuesMap: {
|
||||
[Operator.TavilySearch]: {
|
||||
api_key: 'canvas-api-key',
|
||||
search_depth: 'advanced',
|
||||
max_results: 10,
|
||||
},
|
||||
[Operator.TavilyExtract]: {
|
||||
api_key: 'canvas-api-key',
|
||||
extract_depth: 'advanced',
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useAgentToolInitialValues());
|
||||
|
||||
expect(
|
||||
result.current.initializeAgentToolValues(Operator.TavilySearch),
|
||||
).toEqual({ api_key: '' });
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,8 @@ export function useAgentToolInitialValues() {
|
||||
...omit(initialValues, 'query'),
|
||||
description: '',
|
||||
};
|
||||
case (Operator.TavilySearch, Operator.TavilyExtract):
|
||||
case Operator.TavilySearch:
|
||||
case Operator.TavilyExtract:
|
||||
return {
|
||||
api_key: '',
|
||||
};
|
||||
|
||||
@@ -144,6 +144,35 @@ describe('Run preflight', () => {
|
||||
expect(message.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports DSL build exceptions and resolves the save promise', async () => {
|
||||
mockBuildDslData.mockImplementationOnce(() => {
|
||||
throw new Error('Unable to build agent DSL');
|
||||
});
|
||||
const { result } = renderHook(() => useSaveGraph());
|
||||
let saveResult: unknown;
|
||||
|
||||
await act(async () => {
|
||||
saveResult = await result.current.saveGraph();
|
||||
});
|
||||
|
||||
expect(saveResult).toBeUndefined();
|
||||
expect(message.error).toHaveBeenCalledWith('Unable to build agent DSL');
|
||||
expect(mockSaveAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports rejected agent save requests and resolves the save promise', async () => {
|
||||
mockSaveAgent.mockRejectedValueOnce(new Error('Unable to save agent'));
|
||||
const { result } = renderHook(() => useSaveGraph());
|
||||
let saveResult: unknown;
|
||||
|
||||
await act(async () => {
|
||||
saveResult = await result.current.saveGraph();
|
||||
});
|
||||
|
||||
expect(saveResult).toBeUndefined();
|
||||
expect(message.error).toHaveBeenCalledWith('Unable to save agent');
|
||||
});
|
||||
|
||||
it('validates explicit nextNodes before opening chat', async () => {
|
||||
useGraphStore.setState({
|
||||
nodes: [node('Message', { content: ['hello'] })],
|
||||
|
||||
@@ -46,27 +46,40 @@ export const useSaveGraph = (
|
||||
return;
|
||||
}
|
||||
|
||||
const params: Record<string, any> = {
|
||||
id,
|
||||
title: data.title,
|
||||
dsl: buildDslData(currentNodes, otherParam),
|
||||
};
|
||||
try {
|
||||
const params: Record<string, any> = {
|
||||
id,
|
||||
title: data.title,
|
||||
dsl: buildDslData(currentNodes, otherParam),
|
||||
};
|
||||
|
||||
if (release) {
|
||||
params.release = 'true';
|
||||
}
|
||||
// Both the debounced auto-save and the explicit Save button pass
|
||||
// autosave:true so the backend refreshes only the live draft and never
|
||||
// mints a new version. Only publishing (release) writes a version row,
|
||||
// optionally carrying an alias into user_canvas_version.
|
||||
if (autosave) {
|
||||
params.autosave = true;
|
||||
}
|
||||
if (alias) {
|
||||
params.alias = alias;
|
||||
}
|
||||
if (release) {
|
||||
params.release = 'true';
|
||||
}
|
||||
// Both the debounced auto-save and the explicit Save button pass
|
||||
// autosave:true so the backend refreshes only the live draft and never
|
||||
// mints a new version. Only publishing (release) writes a version row,
|
||||
// optionally carrying an alias into user_canvas_version.
|
||||
if (autosave) {
|
||||
params.autosave = true;
|
||||
}
|
||||
if (alias) {
|
||||
params.alias = alias;
|
||||
}
|
||||
|
||||
return saveAgent(params);
|
||||
const result = await saveAgent(params);
|
||||
if (result?.code !== undefined && result.code !== 0) {
|
||||
message.error(result.message || i18n.t('message.requestError'));
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
message.error(
|
||||
error instanceof Error && error.message
|
||||
? error.message
|
||||
: i18n.t('message.requestError'),
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[id, data.title, buildDslData, saveAgent],
|
||||
);
|
||||
|
||||
@@ -120,12 +120,12 @@ export default function Agent() {
|
||||
const { saveGraph, loading } = useSaveGraph();
|
||||
// Manual Save is a draft save, same as auto-save: it refreshes the live
|
||||
// canvas without minting a user_canvas_version row. Publishing does that.
|
||||
const handleSave = useCallback(() => {
|
||||
saveGraph(undefined, undefined, undefined, true);
|
||||
const handleSave = useCallback(async () => {
|
||||
await saveGraph(undefined, undefined, undefined, true);
|
||||
}, [saveGraph]);
|
||||
const handlePublish = useCallback(
|
||||
(alias?: string) => {
|
||||
saveGraph(undefined, undefined, true, undefined, alias);
|
||||
async (alias?: string) => {
|
||||
await saveGraph(undefined, undefined, true, undefined, alias);
|
||||
},
|
||||
[saveGraph],
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user