Skip to content

Commit 7d9e4fc

Browse files
authored
[onechat] Use tool schema for resolving params in test flyout (#234064)
## Summary Status quo implementation was hardcoded to work with esql only We should use tool's `schema` rather than `configuration` that is specific to es|ql tools
1 parent 5c62e55 commit 7d9e4fc

File tree

1 file changed

+190
-119
lines changed
  • x-pack/platform/plugins/shared/onechat/public/application/components/tools/execute

1 file changed

+190
-119
lines changed

x-pack/platform/plugins/shared/onechat/public/application/components/tools/execute/test_tools.tsx

Lines changed: 190 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
EuiButton,
1010
EuiFieldText,
1111
EuiFieldNumber,
12+
EuiSwitch,
1213
EuiFlexGroup,
1314
EuiFlexItem,
1415
EuiFlyout,
@@ -19,12 +20,14 @@ import {
1920
EuiSpacer,
2021
EuiTitle,
2122
EuiCodeBlock,
23+
EuiLoadingSpinner,
2224
} from '@elastic/eui';
2325
import { css } from '@emotion/react';
2426
import React, { useState } from 'react';
25-
import { useForm, FormProvider, Controller } from 'react-hook-form';
26-
import type { ToolDefinition } from '@kbn/onechat-common';
27+
import { useForm, FormProvider, Controller, type Control } from 'react-hook-form';
28+
import type { ToolDefinitionWithSchema } from '@kbn/onechat-common';
2729
import { i18n } from '@kbn/i18n';
30+
import { formatOnechatErrorMessage } from '@kbn/onechat-browser';
2831
import { useExecuteTool } from '../../../hooks/tools/use_execute_tools';
2932
import type { ExecuteToolResponse } from '../../../../../common/http_api/tools';
3033
import { useTool } from '../../../hooks/tools/use_tools';
@@ -39,29 +42,132 @@ interface OnechatTestToolFlyout {
3942
interface ToolParameter {
4043
name: string;
4144
label: string;
45+
description: string;
4246
value: string;
4347
type: string;
4448
}
4549

46-
const getParameters = (tool: ToolDefinition | undefined): Array<ToolParameter> => {
47-
if (!tool) return [];
50+
enum ToolParameterType {
51+
TEXT = 'text',
52+
NUMERIC = 'numeric',
53+
BOOLEAN = 'boolean',
54+
}
55+
56+
const getComponentType = (schemaType: string): ToolParameterType => {
57+
switch (schemaType) {
58+
case 'boolean':
59+
return ToolParameterType.BOOLEAN;
60+
case 'integer':
61+
case 'long':
62+
case 'number':
63+
case 'double':
64+
case 'float':
65+
return ToolParameterType.NUMERIC;
66+
default:
67+
return ToolParameterType.TEXT;
68+
}
69+
};
70+
71+
const getParameters = (tool?: ToolDefinitionWithSchema): Array<ToolParameter> => {
72+
if (!tool || !tool.schema || !tool.schema.properties) return [];
73+
74+
const { properties } = tool.schema;
4875

4976
const fields: Array<ToolParameter> = [];
50-
if (tool.configuration && tool.configuration.params) {
51-
const params = tool.configuration.params as Record<string, any>;
52-
Object.entries(params).forEach(([paramName, paramConfig]) => {
53-
fields.push({
54-
name: paramName,
55-
label: paramName,
56-
value: '',
57-
type: paramConfig.type || 'text',
58-
});
77+
78+
Object.entries(properties).forEach(([paramName, paramSchema]) => {
79+
let type = 'string'; // default fallback
80+
81+
if (paramSchema && 'type' in paramSchema && paramSchema.type) {
82+
if (Array.isArray(paramSchema.type)) {
83+
type = paramSchema.type[0];
84+
} else if (typeof paramSchema.type === 'string') {
85+
type = paramSchema.type;
86+
}
87+
}
88+
89+
fields.push({
90+
name: paramName,
91+
label: paramSchema?.title || paramName,
92+
value: '',
93+
description: paramSchema?.description || '',
94+
type,
5995
});
60-
}
96+
});
6197

6298
return fields;
6399
};
64100

101+
const renderFormField = (
102+
field: ToolParameter,
103+
tool: ToolDefinitionWithSchema,
104+
control: Control<Record<string, any>>
105+
) => {
106+
const componentType = getComponentType(field.type);
107+
const isRequired = tool?.schema?.required?.includes(field.name);
108+
109+
const commonProps = {
110+
name: field.name,
111+
control,
112+
rules: {
113+
required: isRequired ? `${field.label} is required` : false,
114+
},
115+
};
116+
117+
switch (componentType) {
118+
case ToolParameterType.NUMERIC:
119+
return (
120+
<Controller
121+
{...commonProps}
122+
render={({ field: { onChange, value, name } }) => (
123+
<EuiFieldNumber
124+
name={name}
125+
value={value ?? ''}
126+
type="number"
127+
onChange={(e) => onChange(e.target.valueAsNumber || e.target.value)}
128+
placeholder={`Enter ${field.label.toLowerCase()}`}
129+
fullWidth
130+
/>
131+
)}
132+
/>
133+
);
134+
135+
case ToolParameterType.BOOLEAN:
136+
return (
137+
<Controller
138+
{...commonProps}
139+
defaultValue={false}
140+
rules={{ required: false }}
141+
render={({ field: { onChange, value, name } }) => (
142+
<EuiSwitch
143+
name={name}
144+
checked={Boolean(value)}
145+
onChange={(e) => onChange(e.target.checked)}
146+
label={field.label}
147+
/>
148+
)}
149+
/>
150+
);
151+
152+
case ToolParameterType.TEXT:
153+
default:
154+
return (
155+
<Controller
156+
{...commonProps}
157+
render={({ field: { onChange, value, name } }) => (
158+
<EuiFieldText
159+
name={name}
160+
value={value ?? ''}
161+
onChange={(e) => onChange(e.target.value)}
162+
placeholder={`Enter ${field.label.toLowerCase()}`}
163+
fullWidth
164+
/>
165+
)}
166+
/>
167+
);
168+
}
169+
};
170+
65171
export const OnechatTestFlyout: React.FC<OnechatTestToolFlyout> = ({ toolId, onClose }) => {
66172
const [response, setResponse] = useState<string>('{}');
67173

@@ -74,37 +180,26 @@ export const OnechatTestFlyout: React.FC<OnechatTestToolFlyout> = ({ toolId, onC
74180
formState: { errors },
75181
} = form;
76182

77-
const { tool } = useTool({ toolId });
183+
const { tool, isLoading } = useTool({ toolId });
78184

79185
const { executeTool, isLoading: isExecuting } = useExecuteTool({
80186
onSuccess: (data: ExecuteToolResponse) => {
81187
setResponse(JSON.stringify(data, null, 2));
82188
},
83189
onError: (error: Error) => {
84-
setResponse(JSON.stringify({ error: error.message }, null, 2));
190+
setResponse(JSON.stringify({ error: formatOnechatErrorMessage(error) }, null, 2));
85191
},
86192
});
87193

88194
const onSubmit = async (formData: Record<string, any>) => {
89-
const toolParams: Record<string, any> = {};
90-
getParameters(tool).forEach((field) => {
91-
if (field.name) {
92-
let value = formData[field.name];
93-
if (field.type === 'integer' || field.type === 'long') {
94-
value = parseInt(value, 10);
95-
} else if (field.type === 'double' || field.type === 'float') {
96-
value = parseFloat(value);
97-
}
98-
toolParams[field.name] = value;
99-
}
100-
});
101-
102195
await executeTool({
103196
toolId: tool!.id,
104-
toolParams,
197+
toolParams: formData,
105198
});
106199
};
107200

201+
if (!tool) return null;
202+
108203
return (
109204
<EuiFlyout onClose={onClose} aria-labelledby="flyoutTitle">
110205
<EuiFlyoutHeader hasBorder>
@@ -121,99 +216,75 @@ export const OnechatTestFlyout: React.FC<OnechatTestToolFlyout> = ({ toolId, onC
121216
</EuiFlexGroup>
122217
</EuiFlyoutHeader>
123218
<EuiFlyoutBody>
124-
<FormProvider {...form}>
125-
<EuiFlexGroup gutterSize="l" responsive={false}>
126-
<EuiFlexItem
127-
grow={false}
128-
css={css`
129-
min-width: 200px;
130-
max-width: 300px;
131-
`}
132-
>
133-
<EuiTitle size="s">
134-
<h5>
135-
{i18n.translate('xpack.onechat.tools.testTool.inputsTitle', {
136-
defaultMessage: 'Inputs',
137-
})}
138-
</h5>
139-
</EuiTitle>
140-
<EuiSpacer size="m" />
141-
<EuiForm component="form" onSubmit={handleSubmit(onSubmit)}>
142-
{getParameters(tool)?.map((field) => (
143-
<EuiFormRow
144-
key={field.name}
145-
label={field.label}
146-
isInvalid={!!errors[field.name]}
147-
error={errors[field.name]?.message as string}
148-
>
149-
{field.type === 'integer' ||
150-
field.type === 'long' ||
151-
field.type === 'double' ||
152-
field.type === 'float' ? (
153-
<Controller
154-
name={field.name}
155-
control={form.control}
156-
rules={{ required: `${field.label} is required` }}
157-
render={({ field: { onChange, value, name } }) => (
158-
<EuiFieldNumber
159-
name={name}
160-
value={value || ''}
161-
onChange={(e) => onChange(e.target.value)}
162-
placeholder={`Enter ${field.label.toLowerCase()}`}
163-
fullWidth
164-
/>
165-
)}
166-
/>
167-
) : (
168-
<Controller
169-
name={field.name}
170-
control={form.control}
171-
rules={{ required: `${field.label} is required` }}
172-
render={({ field: { onChange, value, name } }) => (
173-
<EuiFieldText
174-
name={name}
175-
value={value || ''}
176-
onChange={(e) => onChange(e.target.value)}
177-
placeholder={`Enter ${field.label.toLowerCase()}`}
178-
fullWidth
179-
/>
180-
)}
181-
/>
182-
)}
183-
</EuiFormRow>
184-
))}
185-
<EuiSpacer size="m" />
186-
<EuiButton type="submit" size="s" fill isLoading={isExecuting} disabled={!tool}>
187-
{i18n.translate('xpack.onechat.tools.testTool.executeButton', {
188-
defaultMessage: 'Submit',
189-
})}
190-
</EuiButton>
191-
</EuiForm>
219+
{isLoading ? (
220+
<EuiFlexGroup justifyContent="center" alignItems="center">
221+
<EuiFlexItem grow={false}>
222+
<EuiLoadingSpinner size="l" />
192223
</EuiFlexItem>
193-
<EuiFlexItem>
194-
<EuiTitle size="s">
195-
<h5>
196-
{i18n.translate('xpack.onechat.tools.testTool.responseTitle', {
197-
defaultMessage: 'Response',
198-
})}
199-
</h5>
200-
</EuiTitle>
201-
<EuiSpacer size="m" />
202-
<EuiCodeBlock
203-
language="json"
204-
fontSize="s"
205-
paddingSize="m"
206-
isCopyable={true}
224+
</EuiFlexGroup>
225+
) : (
226+
<FormProvider {...form}>
227+
<EuiFlexGroup gutterSize="l" responsive={false}>
228+
<EuiFlexItem
229+
grow={false}
207230
css={css`
208-
height: 75vh;
209-
overflow: auto;
231+
min-width: 200px;
232+
max-width: 300px;
210233
`}
211234
>
212-
{response}
213-
</EuiCodeBlock>
214-
</EuiFlexItem>
215-
</EuiFlexGroup>
216-
</FormProvider>
235+
<EuiTitle size="s">
236+
<h5>
237+
{i18n.translate('xpack.onechat.tools.testTool.inputsTitle', {
238+
defaultMessage: 'Inputs',
239+
})}
240+
</h5>
241+
</EuiTitle>
242+
<EuiSpacer size="m" />
243+
<EuiForm component="form" onSubmit={handleSubmit(onSubmit)}>
244+
{getParameters(tool).map((field) => (
245+
<EuiFormRow
246+
key={field.name}
247+
label={field.label}
248+
helpText={field.description}
249+
isInvalid={!!errors[field.name]}
250+
error={errors[field.name]?.message as string}
251+
>
252+
{renderFormField(field, tool, form.control)}
253+
</EuiFormRow>
254+
))}
255+
<EuiSpacer size="m" />
256+
<EuiButton type="submit" size="s" fill isLoading={isExecuting} disabled={!tool}>
257+
{i18n.translate('xpack.onechat.tools.testTool.executeButton', {
258+
defaultMessage: 'Submit',
259+
})}
260+
</EuiButton>
261+
</EuiForm>
262+
</EuiFlexItem>
263+
<EuiFlexItem>
264+
<EuiTitle size="s">
265+
<h5>
266+
{i18n.translate('xpack.onechat.tools.testTool.responseTitle', {
267+
defaultMessage: 'Response',
268+
})}
269+
</h5>
270+
</EuiTitle>
271+
<EuiSpacer size="m" />
272+
<EuiCodeBlock
273+
language="json"
274+
fontSize="s"
275+
paddingSize="m"
276+
isCopyable={true}
277+
css={css`
278+
height: 75vh;
279+
overflow: auto;
280+
`}
281+
>
282+
{response}
283+
</EuiCodeBlock>
284+
</EuiFlexItem>
285+
</EuiFlexGroup>
286+
</FormProvider>
287+
)}
217288
</EuiFlyoutBody>
218289
</EuiFlyout>
219290
);

0 commit comments

Comments
 (0)