diff --git a/x-pack/plugins/integration_assistant/common/constants.ts b/x-pack/plugins/integration_assistant/common/constants.ts
index 296e38c01e71c..3891c1e5e4343 100644
--- a/x-pack/plugins/integration_assistant/common/constants.ts
+++ b/x-pack/plugins/integration_assistant/common/constants.ts
@@ -26,3 +26,11 @@ export const FLEET_PACKAGES_PATH = `/api/fleet/epm/packages`;
// License
export const MINIMUM_LICENSE_TYPE: LicenseType = 'enterprise';
+
+// ErrorCodes
+
+export enum ErrorCode {
+ RECURSION_LIMIT = 'recursion-limit',
+ RECURSION_LIMIT_ANALYZE_LOGS = 'recursion-limit-analyze-logs',
+ UNSUPPORTED_LOG_SAMPLES_FORMAT = 'unsupported-log-samples-format',
+}
diff --git a/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png
index e7a011ad27b9a..a15dbf54d905a 100644
Binary files a/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/categorization_graph.png differ
diff --git a/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png
index f2342cfc594c3..49bb9c3243068 100644
Binary files a/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/ecs_graph.png differ
diff --git a/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png b/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png
index 85358ee453bcf..8e3ea2bc5e0ca 100644
Binary files a/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png and b/x-pack/plugins/integration_assistant/docs/imgs/ecs_subgraph.png differ
diff --git a/x-pack/plugins/integration_assistant/docs/imgs/kv_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/kv_graph.png
new file mode 100644
index 0000000000000..48a6099924248
Binary files /dev/null and b/x-pack/plugins/integration_assistant/docs/imgs/kv_graph.png differ
diff --git a/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png
index 94830a549dcf1..285e012c57a14 100644
Binary files a/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/log_detection_graph.png differ
diff --git a/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png b/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png
index cebb482a4b495..73a2c3acac0d4 100644
Binary files a/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png and b/x-pack/plugins/integration_assistant/docs/imgs/related_graph.png differ
diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx
index 3246caa81b041..a8e6a30ca5dfa 100644
--- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx
+++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.test.tsx
@@ -277,7 +277,78 @@ describe('GenerationModal', () => {
);
});
- describe('when the retrying successfully', () => {
+ describe('when retrying successfully', () => {
+ beforeEach(async () => {
+ await act(async () => {
+ result.getByTestId('retryButton').click();
+ await waitFor(() => expect(mockOnComplete).toBeCalled());
+ });
+ });
+
+ it('should not render the error callout', () => {
+ expect(result.queryByTestId('generationErrorCallout')).not.toBeInTheDocument();
+ });
+ it('should not render the retry button', () => {
+ expect(result.queryByTestId('retryButton')).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('when there are errors and a message body with error code', () => {
+ const errorMessage = 'error message';
+ const errorCode = 'error code';
+ const error = JSON.stringify({
+ body: {
+ message: errorMessage,
+ attributes: {
+ errorCode,
+ },
+ },
+ });
+ let result: RenderResult;
+ beforeEach(async () => {
+ mockRunEcsGraph.mockImplementationOnce(() => {
+ throw new Error(error);
+ });
+
+ await act(async () => {
+ result = render(
+ ,
+ { wrapper }
+ );
+ await waitFor(() =>
+ expect(result.queryByTestId('generationErrorCallout')).toBeInTheDocument()
+ );
+ });
+ });
+
+ it('should show the error text', () => {
+ expect(result.queryByText(error)).toBeInTheDocument();
+ });
+ it('should render the retry button', () => {
+ expect(result.queryByTestId('retryButton')).toBeInTheDocument();
+ });
+ it('should report telemetry for generation error', () => {
+ expect(mockReportEvent).toHaveBeenCalledWith(
+ TelemetryEventType.IntegrationAssistantGenerationComplete,
+ {
+ sessionId: expect.any(String),
+ sampleRows: integrationSettings.logSamples?.length ?? 0,
+ actionTypeId: connector.actionTypeId,
+ model: expect.anything(),
+ provider: connector.apiProvider ?? 'unknown',
+ durationMs: expect.any(Number),
+ errorMessage: error,
+ }
+ );
+ });
+
+ describe('when retrying successfully', () => {
beforeEach(async () => {
await act(async () => {
result.getByTestId('retryButton').click();
diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx
index b4a9c05b62450..aefde66ed83db 100644
--- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx
+++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/generation_modal.tsx
@@ -42,6 +42,7 @@ import { useKibana } from '../../../../../common/hooks/use_kibana';
import type { State } from '../../state';
import * as i18n from './translations';
import { useTelemetry } from '../../../telemetry';
+import type { ErrorCode } from '../../../../../../common/constants';
export type OnComplete = (result: State['result']) => void;
@@ -171,7 +172,7 @@ export const useGeneration = ({
onComplete(result);
} catch (e) {
if (abortController.signal.aborted) return;
- const errorMessage = `${e.message}${
+ const originalErrorMessage = `${e.message}${
e.body ? ` (${e.body.statusCode}): ${e.body.message}` : ''
}`;
@@ -179,9 +180,14 @@ export const useGeneration = ({
connector,
integrationSettings,
durationMs: Date.now() - generationStartedAt,
- error: errorMessage,
+ error: originalErrorMessage,
});
+ let errorMessage = originalErrorMessage;
+ const errorCode = e.body?.attributes?.errorCode as ErrorCode | undefined;
+ if (errorCode != null) {
+ errorMessage = i18n.ERROR_TRANSLATION[errorCode];
+ }
setError(errorMessage);
} finally {
setIsRequesting(false);
diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts
index d44ad97939858..017a1a9c29caa 100644
--- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts
+++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/data_stream_step/translations.ts
@@ -6,6 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { ErrorCode } from '../../../../../../common/constants';
export const INTEGRATION_NAME_TITLE = i18n.translate(
'xpack.integrationAssistant.step.dataStream.integrationNameTitle',
@@ -196,3 +197,25 @@ export const GENERATION_ERROR = (progressStep: string) =>
export const RETRY = i18n.translate('xpack.integrationAssistant.step.dataStream.retryButtonLabel', {
defaultMessage: 'Retry',
});
+
+export const ERROR_TRANSLATION: Record = {
+ [ErrorCode.RECURSION_LIMIT_ANALYZE_LOGS]: i18n.translate(
+ 'xpack.integrationAssistant.errors.recursionLimitAnalyzeLogsErrorMessage',
+ {
+ defaultMessage:
+ 'Please verify the format of log samples is correct and try again. Try with a fewer samples if error persists.',
+ }
+ ),
+ [ErrorCode.RECURSION_LIMIT]: i18n.translate(
+ 'xpack.integrationAssistant.errors.recursionLimitReached',
+ {
+ defaultMessage: 'Max attempts exceeded. Please try again.',
+ }
+ ),
+ [ErrorCode.UNSUPPORTED_LOG_SAMPLES_FORMAT]: i18n.translate(
+ 'xpack.integrationAssistant.errors.unsupportedLogSamples',
+ {
+ defaultMessage: 'Unsupported log format in the samples.',
+ }
+ ),
+};
diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/index.ts b/x-pack/plugins/integration_assistant/server/lib/errors/index.ts
new file mode 100644
index 0000000000000..ae3b009334033
--- /dev/null
+++ b/x-pack/plugins/integration_assistant/server/lib/errors/index.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { ErrorThatHandlesItsOwnResponse } from './types';
+
+export function isErrorThatHandlesItsOwnResponse(
+ e: ErrorThatHandlesItsOwnResponse
+): e is ErrorThatHandlesItsOwnResponse {
+ return typeof (e as ErrorThatHandlesItsOwnResponse).sendResponse === 'function';
+}
+
+export { RecursionLimitError } from './recursion_limit_error';
+export { UnsupportedLogFormatError } from './unsupported_error';
diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/recursion_limit_error.ts b/x-pack/plugins/integration_assistant/server/lib/errors/recursion_limit_error.ts
new file mode 100644
index 0000000000000..09b307cb841e9
--- /dev/null
+++ b/x-pack/plugins/integration_assistant/server/lib/errors/recursion_limit_error.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { KibanaResponseFactory } from '@kbn/core/server';
+import { ErrorThatHandlesItsOwnResponse } from './types';
+
+export class RecursionLimitError extends Error implements ErrorThatHandlesItsOwnResponse {
+ private readonly errorCode: string;
+
+ constructor(message: string, errorCode: string) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ public sendResponse(res: KibanaResponseFactory) {
+ return res.badRequest({
+ body: { message: this.message, attributes: { errorCode: this.errorCode } },
+ });
+ }
+}
diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/types.ts b/x-pack/plugins/integration_assistant/server/lib/errors/types.ts
new file mode 100644
index 0000000000000..2b664da0941e8
--- /dev/null
+++ b/x-pack/plugins/integration_assistant/server/lib/errors/types.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { KibanaResponseFactory, IKibanaResponse } from '@kbn/core/server';
+
+export interface ErrorThatHandlesItsOwnResponse extends Error {
+ sendResponse(res: KibanaResponseFactory): IKibanaResponse;
+}
diff --git a/x-pack/plugins/integration_assistant/server/lib/errors/unsupported_error.ts b/x-pack/plugins/integration_assistant/server/lib/errors/unsupported_error.ts
new file mode 100644
index 0000000000000..79c4f2ccf69a1
--- /dev/null
+++ b/x-pack/plugins/integration_assistant/server/lib/errors/unsupported_error.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { KibanaResponseFactory } from '@kbn/core/server';
+import { ErrorThatHandlesItsOwnResponse } from './types';
+import { ErrorCode } from '../../../common/constants';
+
+export class UnsupportedLogFormatError extends Error implements ErrorThatHandlesItsOwnResponse {
+ private readonly errorCode: string = ErrorCode.UNSUPPORTED_LOG_SAMPLES_FORMAT;
+
+ // eslint-disable-next-line @typescript-eslint/no-useless-constructor
+ constructor(message: string) {
+ super(message);
+ }
+
+ public sendResponse(res: KibanaResponseFactory) {
+ return res.customError({
+ statusCode: 501,
+ body: { message: this.message, attributes: { errorCode: this.errorCode } },
+ });
+ }
+}
diff --git a/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts b/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts
index 29a68c4395a7c..2f0f3db47a7a9 100644
--- a/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts
+++ b/x-pack/plugins/integration_assistant/server/routes/analyze_logs_routes.ts
@@ -16,6 +16,9 @@ import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
+import { isErrorThatHandlesItsOwnResponse, UnsupportedLogFormatError } from '../lib/errors';
+import { handleCustomErrors } from './routes_util';
+import { ErrorCode } from '../../common/constants';
export function registerAnalyzeLogsRoutes(
router: IRouter
@@ -82,14 +85,18 @@ export function registerAnalyzeLogsRoutes(
const graphResults = await graph.invoke(logFormatParameters, options);
const graphLogFormat = graphResults.results.samplesFormat.name;
if (graphLogFormat === 'unsupported' || graphLogFormat === 'csv') {
- return res.customError({
- statusCode: 501,
- body: { message: `Unsupported log samples format` },
- });
+ throw new UnsupportedLogFormatError(ErrorCode.UNSUPPORTED_LOG_SAMPLES_FORMAT);
}
return res.ok({ body: AnalyzeLogsResponse.parse(graphResults) });
- } catch (e) {
- return res.badRequest({ body: e });
+ } catch (err) {
+ try {
+ handleCustomErrors(err, ErrorCode.RECURSION_LIMIT_ANALYZE_LOGS);
+ } catch (e) {
+ if (isErrorThatHandlesItsOwnResponse(e)) {
+ return e.sendResponse(res);
+ }
+ }
+ return res.badRequest({ body: err });
}
})
);
diff --git a/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts b/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts
index d4b4424f5c844..1a7ecb58a2062 100644
--- a/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts
+++ b/x-pack/plugins/integration_assistant/server/routes/build_integration_routes.ts
@@ -11,7 +11,9 @@ import { buildPackage } from '../integration_builder';
import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
-
+import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
+import { handleCustomErrors } from './routes_util';
+import { ErrorCode } from '../../common/constants';
export function registerIntegrationBuilderRoutes(
router: IRouter
) {
@@ -38,8 +40,15 @@ export function registerIntegrationBuilderRoutes(
body: zippedIntegration,
headers: { 'Content-Type': 'application/zip' },
});
- } catch (e) {
- return response.customError({ statusCode: 500, body: e });
+ } catch (err) {
+ try {
+ handleCustomErrors(err, ErrorCode.RECURSION_LIMIT);
+ } catch (e) {
+ if (isErrorThatHandlesItsOwnResponse(e)) {
+ return e.sendResponse(response);
+ }
+ }
+ return response.customError({ statusCode: 500, body: err });
}
})
);
diff --git a/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts b/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts
index c6a3cb5d1682a..635ef08dcdf9c 100644
--- a/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts
+++ b/x-pack/plugins/integration_assistant/server/routes/categorization_routes.ts
@@ -20,6 +20,9 @@ import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
+import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
+import { handleCustomErrors } from './routes_util';
+import { ErrorCode } from '../../common/constants';
export function registerCategorizationRoutes(
router: IRouter
@@ -98,8 +101,15 @@ export function registerCategorizationRoutes(
const results = await graph.invoke(parameters, options);
return res.ok({ body: CategorizationResponse.parse(results) });
- } catch (e) {
- return res.badRequest({ body: e });
+ } catch (err) {
+ try {
+ handleCustomErrors(err, ErrorCode.RECURSION_LIMIT);
+ } catch (e) {
+ if (isErrorThatHandlesItsOwnResponse(e)) {
+ return e.sendResponse(res);
+ }
+ }
+ return res.badRequest({ body: err });
}
}
)
diff --git a/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts b/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts
index 34a9fa5106654..12d77c66a1132 100644
--- a/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts
+++ b/x-pack/plugins/integration_assistant/server/routes/ecs_routes.ts
@@ -16,6 +16,9 @@ import type { IntegrationAssistantRouteHandlerContext } from '../plugin';
import { getLLMClass, getLLMType } from '../util/llm';
import { buildRouteValidationWithZod } from '../util/route_validation';
import { withAvailability } from './with_availability';
+import { isErrorThatHandlesItsOwnResponse } from '../lib/errors';
+import { handleCustomErrors } from './routes_util';
+import { ErrorCode } from '../../common/constants';
export function registerEcsRoutes(router: IRouter) {
router.versioned
@@ -92,8 +95,15 @@ export function registerEcsRoutes(router: IRouter) {
router.versioned
@@ -46,8 +49,15 @@ export function registerPipelineRoutes(router: IRouter) {
router.versioned
@@ -89,8 +92,15 @@ export function registerRelatedRoutes(router: IRouter {
+ it('should throw a RecursionLimitError when given a GraphRecursionError', () => {
+ const errorMessage = 'Recursion limit exceeded';
+ const errorCode = ErrorCode.RECURSION_LIMIT;
+ const recursionError = new GraphRecursionError(errorMessage);
+
+ expect(() => {
+ handleCustomErrors(recursionError, errorCode);
+ }).toThrow(RecursionLimitError);
+ expect(() => {
+ handleCustomErrors(recursionError, errorCode);
+ }).toThrowError(errorMessage);
+ });
+
+ it('should rethrow the error when given an error that is not a GraphRecursionError', () => {
+ const errorMessage = 'Some other error';
+ const errorCode = ErrorCode.RECURSION_LIMIT;
+ const otherError = new Error(errorMessage);
+
+ expect(() => {
+ handleCustomErrors(otherError, errorCode);
+ }).toThrow(otherError);
+ expect(() => {
+ handleCustomErrors(otherError, errorCode);
+ }).toThrowError(errorMessage);
+ });
+});
diff --git a/x-pack/plugins/integration_assistant/server/routes/routes_util.ts b/x-pack/plugins/integration_assistant/server/routes/routes_util.ts
new file mode 100644
index 0000000000000..5622392cd06a9
--- /dev/null
+++ b/x-pack/plugins/integration_assistant/server/routes/routes_util.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { GraphRecursionError } from '@langchain/langgraph';
+import { ErrorCode } from '../../common/constants';
+import { RecursionLimitError } from '../lib/errors';
+
+/**
+ * Handles errors that occur during the execution of a function.
+ * If the error is an instance of GraphRecursionError, it throws a RecursionLimitError with the same message and error code.
+ * Otherwise, it rethrows the original error.
+ *
+ * @param err - The error that occurred.
+ * @param errorCode - The error code associated with the error.
+ * @throws {RecursionLimitError} If the error is an instance of GraphRecursionError.
+ * @throws {Error} The original error.
+ */
+export function handleCustomErrors(
+ err: Error,
+ recursionErrorCode: ErrorCode.RECURSION_LIMIT | ErrorCode.RECURSION_LIMIT_ANALYZE_LOGS
+) {
+ if (err instanceof GraphRecursionError) {
+ throw new RecursionLimitError(err.message, recursionErrorCode);
+ }
+ throw err;
+}