diff --git a/x-pack/platform/plugins/shared/onechat/common/http_api/tools.ts b/x-pack/platform/plugins/shared/onechat/common/http_api/tools.ts index 4f955f3afc5ac..ee02f39e33fef 100644 --- a/x-pack/platform/plugins/shared/onechat/common/http_api/tools.ts +++ b/x-pack/platform/plugins/shared/onechat/common/http_api/tools.ts @@ -10,6 +10,7 @@ import type { ToolDefinitionWithSchema, SerializedOnechatError, } from '@kbn/onechat-common'; +import type { ToolResult } from '@kbn/onechat-common/tools/tool_result'; export interface ListToolsResponse { results: ToolDefinitionWithSchema[]; @@ -50,3 +51,7 @@ export type BulkDeleteToolResult = BulkDeleteToolSuccessResult | BulkDeleteToolF export interface BulkDeleteToolResponse { results: BulkDeleteToolResult[]; } + +export interface ExecuteToolResponse { + result: ToolResult[]; +} diff --git a/x-pack/platform/plugins/shared/onechat/public/application/components/tools/esql/esql_tool.tsx b/x-pack/platform/plugins/shared/onechat/public/application/components/tools/esql/esql_tool.tsx index e303b82072990..273c79acfdb42 100644 --- a/x-pack/platform/plugins/shared/onechat/public/application/components/tools/esql/esql_tool.tsx +++ b/x-pack/platform/plugins/shared/onechat/public/application/components/tools/esql/esql_tool.tsx @@ -17,8 +17,9 @@ import { import { css } from '@emotion/react'; import type { EsqlToolDefinitionWithSchema } from '@kbn/onechat-common'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { FormProvider } from 'react-hook-form'; +import { i18n } from '@kbn/i18n'; import type { CreateToolPayload, CreateToolResponse, @@ -36,6 +37,7 @@ import { import { OnechatEsqlToolForm, OnechatEsqlToolFormMode } from './form/esql_tool_form'; import type { OnechatEsqlToolFormData } from './form/types/esql_tool_form_types'; import { useEsqlToolForm } from '../../../hooks/tools/use_esql_tool_form'; +import { OnechatTestFlyout } from '../execute/test_tools'; interface EsqlToolBaseProps { tool?: EsqlToolDefinitionWithSchema; @@ -65,8 +67,11 @@ export const EsqlTool: React.FC = ({ const { euiTheme } = useEuiTheme(); const { navigateToOnechatUrl } = useNavigation(); const form = useEsqlToolForm(); - const { reset, formState } = form; - const { errors } = formState; + const { reset, formState, watch } = form; + const { errors, isDirty } = formState; + const [showTestFlyout, setShowTestFlyout] = useState(false); + + const currentToolId = watch('name'); const handleClear = useCallback(() => { reset(); @@ -86,7 +91,10 @@ export const EsqlTool: React.FC = ({ useEffect(() => { if (tool) { - reset(transformEsqlToolToFormData(tool), { keepDefaultValues: true }); + reset(transformEsqlToolToFormData(tool), { + keepDefaultValues: true, + keepDirty: true, + }); } }, [tool, reset]); @@ -107,6 +115,43 @@ export const EsqlTool: React.FC = ({ ); + const saveAndTestButton = ( + { + const formData = form.getValues(); + if (mode === OnechatEsqlToolFormMode.Edit) { + await saveTool(transformEsqlFormDataForUpdate(formData)); + } else { + await saveTool(transformEsqlFormDataForCreate(formData)); + } + if (currentToolId) { + setShowTestFlyout(true); + } + }} + disabled={Object.keys(errors).length > 0 || isSubmitting} + isLoading={isSubmitting} + > + {i18n.translate('xpack.onechat.tools.esqlToolFlyout.saveAndTestButtonLabel', { + defaultMessage: 'Save and Test', + })} + + ); + + const testButton = ( + { + setShowTestFlyout(true); + }} + disabled={Object.keys(errors).length > 0} + > + {i18n.translate('xpack.onechat.tools.esqlToolFlyout.testButtonLabel', { + defaultMessage: 'Test', + })} + + ); + return ( @@ -127,7 +172,22 @@ export const EsqlTool: React.FC = ({ ) : ( - + <> + + {showTestFlyout && currentToolId && ( + { + setShowTestFlyout(false); + if (mode === OnechatEsqlToolFormMode.Create) { + navigateToOnechatUrl(appPaths.tools.list); + } + }} + /> + )} + )} = ({ {labels.tools.clearButtonLabel} + {!isDirty && {testButton}} + {isDirty && {saveAndTestButton}} {Object.keys(errors).length > 0 ? ( diff --git a/x-pack/platform/plugins/shared/onechat/public/application/components/tools/execute/test_tools.tsx b/x-pack/platform/plugins/shared/onechat/public/application/components/tools/execute/test_tools.tsx new file mode 100644 index 0000000000000..9ce8464dd2f2b --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/public/application/components/tools/execute/test_tools.tsx @@ -0,0 +1,220 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiButton, + EuiFieldText, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiForm, + EuiFormRow, + EuiSpacer, + EuiTitle, + EuiCodeBlock, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { useState } from 'react'; +import { useForm, FormProvider, Controller } from 'react-hook-form'; +import type { ToolDefinition } from '@kbn/onechat-common'; +import { i18n } from '@kbn/i18n'; +import { useExecuteTool } from '../../../hooks/tools/use_execute_tools'; +import type { ExecuteToolResponse } from '../../../../../common/http_api/tools'; +import { useTool } from '../../../hooks/tools/use_tools'; + +interface OnechatTestToolFlyout { + isOpen: boolean; + isLoading?: boolean; + toolId: string; + onClose: () => void; +} + +interface ToolParameter { + name: string; + label: string; + value: string; + type: string; +} + +const getParameters = (tool: ToolDefinition | undefined): Array => { + if (!tool) return []; + + const fields: Array = []; + if (tool.configuration && tool.configuration.params) { + const params = tool.configuration.params as Record; + Object.entries(params).forEach(([paramName, paramConfig]) => { + fields.push({ + name: paramName, + label: paramName, + value: '', + type: paramConfig.type || 'text', + }); + }); + } + + return fields; +}; + +export const OnechatTestFlyout: React.FC = ({ toolId, onClose }) => { + const [response, setResponse] = useState('{}'); + + const form = useForm>({ + mode: 'onChange', + }); + + const { + handleSubmit, + formState: { errors }, + } = form; + + const { tool } = useTool({ toolId }); + + const { executeTool, isLoading: isExecuting } = useExecuteTool({ + onSuccess: (data: ExecuteToolResponse) => { + setResponse(JSON.stringify(data, null, 2)); + }, + onError: (error: Error) => { + setResponse(JSON.stringify({ error: error.message }, null, 2)); + }, + }); + + const onSubmit = async (formData: Record) => { + const toolParams: Record = {}; + getParameters(tool).forEach((field) => { + if (field.name) { + let value = formData[field.name]; + if (field.type === 'integer' || field.type === 'long') { + value = parseInt(value, 10); + } else if (field.type === 'double' || field.type === 'float') { + value = parseFloat(value); + } + toolParams[field.name] = value; + } + }); + + await executeTool({ + toolId: tool!.id, + toolParams, + }); + }; + + return ( + + + + + +

+ {i18n.translate('xpack.onechat.tools.testFlyout.title', { + defaultMessage: 'Test Tool', + })} +

+
+
+
+
+ + + + + +
+ {i18n.translate('xpack.onechat.tools.testTool.inputsTitle', { + defaultMessage: 'Inputs', + })} +
+
+ + + {getParameters(tool)?.map((field) => ( + + {field.type === 'integer' || + field.type === 'long' || + field.type === 'double' || + field.type === 'float' ? ( + ( + onChange(e.target.value)} + placeholder={`Enter ${field.label.toLowerCase()}`} + fullWidth + /> + )} + /> + ) : ( + ( + onChange(e.target.value)} + placeholder={`Enter ${field.label.toLowerCase()}`} + fullWidth + /> + )} + /> + )} + + ))} + + + {i18n.translate('xpack.onechat.tools.testTool.executeButton', { + defaultMessage: 'Submit', + })} + + +
+ + +
+ {i18n.translate('xpack.onechat.tools.testTool.responseTitle', { + defaultMessage: 'Response', + })} +
+
+ + + {response} + +
+
+
+
+
+ ); +}; diff --git a/x-pack/platform/plugins/shared/onechat/public/application/hooks/tools/use_execute_tools.ts b/x-pack/platform/plugins/shared/onechat/public/application/hooks/tools/use_execute_tools.ts new file mode 100644 index 0000000000000..85d8b83d383de --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/public/application/hooks/tools/use_execute_tools.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation } from '@tanstack/react-query'; +import type { ExecuteToolResponse } from '../../../../common/http_api/tools'; +import { useOnechatServices } from '../use_onechat_service'; + +export interface ExecuteToolParams { + toolId: string; + toolParams: Record; +} + +export type ExecuteToolSuccessCallback = (data: ExecuteToolResponse) => void; +export type ExecuteToolErrorCallback = (error: Error) => void; + +export const useExecuteTool = ({ + onSuccess, + onError, +}: { + onSuccess?: ExecuteToolSuccessCallback; + onError?: ExecuteToolErrorCallback; +} = {}) => { + const { toolsService } = useOnechatServices(); + + const mutationFn = ({ toolId, toolParams }: ExecuteToolParams): Promise => + toolsService.execute(toolId, toolParams); + + const { mutateAsync, isLoading, error } = useMutation< + ExecuteToolResponse, + Error, + ExecuteToolParams + >(mutationFn, { + onSuccess, + onError, + }); + + return { + executeTool: mutateAsync, + isLoading, + error, + }; +}; diff --git a/x-pack/platform/plugins/shared/onechat/public/services/tools/tools_service.ts b/x-pack/platform/plugins/shared/onechat/public/services/tools/tools_service.ts index dceca66f30bbc..07d634209095d 100644 --- a/x-pack/platform/plugins/shared/onechat/public/services/tools/tools_service.ts +++ b/x-pack/platform/plugins/shared/onechat/public/services/tools/tools_service.ts @@ -15,6 +15,7 @@ import type { CreateToolResponse, UpdateToolResponse, BulkDeleteToolResponse, + ExecuteToolResponse, } from '../../../common/http_api/tools'; export class ToolsService { @@ -54,4 +55,13 @@ export class ToolsService { body: JSON.stringify(update), }); } + + async execute(toolId: string, toolParams: Record) { + return await this.http.post('/api/chat/tools/_execute', { + body: JSON.stringify({ + tool_id: toolId, + tool_params: toolParams, + }), + }); + } }