From f4e9bd9f00260bb7749c81f5e197e036064a4fe5 Mon Sep 17 00:00:00 2001 From: Kylie Meli Date: Fri, 11 Oct 2024 14:50:21 -0400 Subject: [PATCH] [Automatic Import] Adding base cel generation as experimental feature (#195309) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR adds base level support for CEL input configuration generation for Automatic Import. ## How this works For this phase of the CEL generation, we will produce three things: 1. A simple CEL program. This will contain logic for querying an endpoint and mapping its response to events for processing based on an OpenAPI spec file. It does **not** contain more complex functionality for things like authentication. 2. An initial state. This will be based on the program and contain defaults based on the openapi spec file. 3. A list of state variables that need redaction from the logs. These three pieces will be available for user review, and then plumbed directly into the manifest file as default values for their corresponding settings where the user can modify as needed. Note: It is not yet expected that the generated output will be fully functional without any tweaking or add-on's from the user for things like authentication. ## (Temporary) UI Flow If a user selects CEL during the datastream step, after completion of the review, the user will then be able to upload and review the new CEL steps. The generated results shown to the user, and are then plumbed as defaults to the input settings, where a user is able to modify during configuration of the integration. (Note: this flow will be changed with forthcoming UX designs) ## Feature flag This feature will be behind an experimental feature flag for now, as the design is still a work in progress. To enable this feature, add `xpack.integration_assistant.enableExperimental: ['generateCel']` to kibana.yml ## Maintainer's notes - UI tests were intentionally omitted for now, as the UI implemented is only temporary until we have a UX design. - Some OpenAPI specs are too large to be uploaded at this time. I am working on adding support for that and have added another item to the [meta issue](https://github.com/elastic/kibana/issues/193074) as such Relates: https://github.com/elastic/kibana/issues/193074 ___
Screenshots After selecting CEL during datastream configuration and reviewing those results, the user will be brought to a new screen to upload an open api spec upload The user can upload the spec file (as long as it isn't over the file upload limit) spec uploaded The user waits while the LLM runs Screenshot 2024-10-09 at 1 37 59 PM The user can view results review The results are automatically pasted into the config, where the user may further edit and configure the input Screenshot 2024-10-08 at 11 17 46 AM
Sample results source: [MISP](https://raw.githubusercontent.com/MISP/MISP/develop/app/webroot/doc/openapi.yaml) program: ``` ( request("POST", state.url + "/events/restSearch?" + { "page": [string(state.page)], "limit": [string(state.limit)], "sort": ["date"], "direction": ["asc"] }.format_query()).with({ "Header": { "Content-Type": ["application/json"] } }).do_request().as(resp, resp.StatusCode == 200 ? bytes(resp.Body).decode_json().as(body, { "events": body.map(e, { "message": e.encode_json() }), "want_more": body.size() == state.limit, "page": state.page + 1, "limit": state.limit }) : { "events": [{ "error": { "code": string(resp.StatusCode), "id": string(resp.Status), "message": string(resp.Body) } }], "want_more": false } ) ) ``` intiial state: ``` page : 1 limit : 50 ``` redact vars: ``` [ ] ```
(cherry picked from commit 7f24e388829933be17f1ed7b690dc0562a354cd8) --- .../__jest__/fixtures/cel.ts | 115 +++++++ .../__jest__/fixtures/index.ts | 5 + .../common/api/cel/cel_input_route.gen.ts | 33 ++ .../api/cel/cel_input_route.schema.yaml | 39 +++ .../common/api/cel/cel_input_route.test.ts | 20 ++ .../common/api/model/api_test.mock.ts | 7 + .../api/model/cel_input_attributes.gen.ts | 33 ++ .../model/cel_input_attributes.schema.yaml | 30 ++ .../common/api/model/common_attributes.gen.ts | 5 + .../api/model/common_attributes.schema.yaml | 3 + .../common/api/model/response_schemas.gen.ts | 6 + .../api/model/response_schemas.schema.yaml | 8 + .../integration_assistant/common/constants.ts | 1 + .../common/experimental_features.ts | 6 +- .../integration_assistant/common/index.ts | 3 + .../public/common/lib/api.ts | 13 + .../create_integration_assistant.test.tsx | 73 +++++ .../create_integration_assistant.tsx | 38 ++- .../footer/footer.test.tsx | 8 + .../footer/footer.tsx | 31 +- .../footer/translations.ts | 4 + .../mocks/state.ts | 1 + .../create_integration_assistant/state.ts | 9 +- .../cel_input_step/api_definition_input.tsx | 118 +++++++ .../steps/cel_input_step/cel_input_step.tsx | 64 ++++ .../steps/cel_input_step/generation_modal.tsx | 222 +++++++++++++ .../steps/cel_input_step/index.ts | 9 + .../steps/cel_input_step/is_step_ready.ts | 17 + .../steps/cel_input_step/translations.ts | 87 +++++ .../steps/deploy_step/deploy_step.tsx | 4 +- .../deploy_step/use_deploy_integration.ts | 4 + .../review_cel_step/cel_config_results.tsx | 65 ++++ .../steps/review_cel_step/index.ts | 8 + .../steps/review_cel_step/is_step_ready.ts | 11 + .../steps/review_cel_step/review_cel_step.tsx | 34 ++ .../steps/review_cel_step/translations.ts | 30 ++ .../create_integration_assistant/types.ts | 1 + .../create_integration/telemetry.tsx | 23 ++ .../public/services/telemetry/events.ts | 45 +++ .../public/services/telemetry/types.ts | 12 + .../server/graphs/cel/build_program.test.ts | 29 ++ .../server/graphs/cel/build_program.ts | 33 ++ .../server/graphs/cel/constants.ts | 298 ++++++++++++++++++ .../server/graphs/cel/graph.test.ts | 99 ++++++ .../server/graphs/cel/graph.ts | 109 +++++++ .../server/graphs/cel/index.ts | 7 + .../server/graphs/cel/prompts.ts | 156 +++++++++ .../graphs/cel/retrieve_state_details.test.ts | 34 ++ .../graphs/cel/retrieve_state_details.ts | 36 +++ .../graphs/cel/retrieve_state_vars.test.ts | 29 ++ .../server/graphs/cel/retrieve_state_vars.ts | 33 ++ .../server/graphs/cel/summarize_query.test.ts | 29 ++ .../server/graphs/cel/summarize_query.ts | 29 ++ .../server/graphs/cel/types.ts | 26 ++ .../server/graphs/cel/util.test.ts | 29 ++ .../server/graphs/cel/util.ts | 32 ++ .../server/integration_builder/constants.ts | 18 ++ .../integration_builder/data_stream.test.ts | 39 +++ .../server/integration_builder/data_stream.ts | 25 +- .../integration_assistant/server/plugin.ts | 8 +- .../server/routes/cel_route.test.ts | 75 +++++ .../server/routes/cel_routes.ts | 92 ++++++ .../server/routes/register_routes.ts | 11 +- .../templates/manifest/cel_manifest.yml.njk | 22 +- .../integration_assistant/server/types.ts | 14 + 65 files changed, 2528 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/integration_assistant/__jest__/fixtures/cel.ts create mode 100644 x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.gen.ts create mode 100644 x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.schema.yaml create mode 100644 x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.test.ts create mode 100644 x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.gen.ts create mode 100644 x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.schema.yaml create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/api_definition_input.tsx create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/cel_input_step.tsx create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/generation_modal.tsx create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/index.ts create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/is_step_ready.ts create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/translations.ts create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/cel_config_results.tsx create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/index.ts create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/is_step_ready.ts create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/review_cel_step.tsx create mode 100644 x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/translations.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/build_program.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/build_program.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/constants.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/graph.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/graph.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/index.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/prompts.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/types.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/util.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/graphs/cel/util.ts create mode 100644 x-pack/plugins/integration_assistant/server/integration_builder/constants.ts create mode 100644 x-pack/plugins/integration_assistant/server/routes/cel_route.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/routes/cel_routes.ts diff --git a/x-pack/plugins/integration_assistant/__jest__/fixtures/cel.ts b/x-pack/plugins/integration_assistant/__jest__/fixtures/cel.ts new file mode 100644 index 0000000000000..e13c4216fbcdd --- /dev/null +++ b/x-pack/plugins/integration_assistant/__jest__/fixtures/cel.ts @@ -0,0 +1,115 @@ +/* + * 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. + */ + +export const celTestState = { + dataStreamName: 'testDataStream', + apiDefinition: 'apiDefinition', + lastExecutedChain: 'testchain', + finalized: false, + apiQuerySummary: 'testQuerySummary', + exampleCelPrograms: [], + currentProgram: 'testProgram', + stateVarNames: ['testVar'], + stateSettings: { test: 'testDetails' }, + redactVars: ['testRedact'], + results: { test: 'testResults' }, +}; + +export const celQuerySummaryMockedResponse = `To cover all events in a chronological manner for the device_tasks endpoint, you should use the /v1/device_tasks GET route with pagination parameters. Specifically, use the pageSize and pageToken query parameters. Start with a large pageSize and use the nextPageToken from each response to fetch subsequent pages until all events are retrieved. +Sample URL path: +/v1/device_tasks?pageSize=1000&pageToken={nextPageToken} +Replace {nextPageToken} with the actual token received from the previous response. Repeat this process, updating the pageToken each time, until you've retrieved all events.`; + +export const celProgramMockedResponse = `Based on the provided context and requirements, here's the CEL program section for the device_tasks datastream: + +\`\`\` +request("GET", state.url + "/v1/device_tasks" + "?" + { + "pageSize": [string(state.page_size)], + "pageToken": [state.page_token] +}.format_query()).with({ + "Header": { + "Content-Type": ["application/json"] + } +}).do_request().as(resp, + resp.StatusCode == 200 ? + bytes(resp.Body).decode_json().as(body, { + "events": body.tasks.map(e, {"message": e.encode_json()}), + "page_token": body.nextPageToken, + "want_more": body.nextPageToken != null + }) : { + "events": { + "error": { + "code": string(resp.StatusCode), + "message": string(resp.Body) + } + }, + "want_more": false + } +) +\`\`\``; + +export const celProgramMock = `request("GET", state.url + "/v1/device_tasks" + "?" + { + "pageSize": [string(state.page_size)], + "pageToken": [state.page_token] +}.format_query()).with({ + "Header": { + "Content-Type": ["application/json"] + } +}).do_request().as(resp, + resp.StatusCode == 200 ? + bytes(resp.Body).decode_json().as(body, { + "events": body.tasks.map(e, {"message": e.encode_json()}), + "page_token": body.nextPageToken, + "want_more": body.nextPageToken != null + }) : { + "events": { + "error": { + "code": string(resp.StatusCode), + "message": string(resp.Body) + } + }, + "want_more": false + } +)`; + +export const celStateVarsMockedResponse = ['config1', 'config2', 'config3']; + +export const celStateDetailsMockedResponse = [ + { + name: 'config1', + default: 50, + redact: false, + }, + { + name: 'config2', + default: '', + redact: true, + }, + { + name: 'config3', + default: 'event', + redact: false, + }, +]; + +export const celStateSettings = { + config1: 50, + config2: '', + config3: 'event', +}; + +export const celRedact = ['config2']; + +export const celExpectedResults = { + program: celProgramMock, + stateSettings: { + config1: 50, + config2: '', + config3: 'event', + }, + redactVars: ['config2'], +}; diff --git a/x-pack/plugins/integration_assistant/__jest__/fixtures/index.ts b/x-pack/plugins/integration_assistant/__jest__/fixtures/index.ts index 7e3e155e67b8a..a25e3d6cf43a7 100644 --- a/x-pack/plugins/integration_assistant/__jest__/fixtures/index.ts +++ b/x-pack/plugins/integration_assistant/__jest__/fixtures/index.ts @@ -52,3 +52,8 @@ export const mockedRequestWithPipeline = { dataStreamName: 'audit', currentPipeline: currentPipelineMock, }; + +export const mockedRequestWithApiDefinition = { + apiDefinition: '{ "openapi": "3.0.0" }', + dataStreamName: 'audit', +}; diff --git a/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.gen.ts b/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.gen.ts new file mode 100644 index 0000000000000..276fd27072c98 --- /dev/null +++ b/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.gen.ts @@ -0,0 +1,33 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Automatic Import CEL Input API endpoint + * version: 1 + */ + +import { z } from '@kbn/zod'; + +import { DataStreamName, Connector, LangSmithOptions } from '../model/common_attributes.gen'; +import { ApiDefinition } from '../model/cel_input_attributes.gen'; +import { CelInputAPIResponse } from '../model/response_schemas.gen'; + +export type CelInputRequestBody = z.infer; +export const CelInputRequestBody = z.object({ + dataStreamName: DataStreamName, + apiDefinition: ApiDefinition, + connectorId: Connector, + langSmithOptions: LangSmithOptions.optional(), +}); +export type CelInputRequestBodyInput = z.input; + +export type CelInputResponse = z.infer; +export const CelInputResponse = CelInputAPIResponse; diff --git a/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.schema.yaml b/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.schema.yaml new file mode 100644 index 0000000000000..18187959fe461 --- /dev/null +++ b/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.schema.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.3 +info: + title: Automatic Import CEL Input API endpoint + version: "1" +paths: + /api/integration_assistant/cel: + post: + summary: Builds CEL input configuration + operationId: CelInput + x-codegen-enabled: true + description: Generate CEL input configuration + tags: + - CEL API + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - apiDefinition + - dataStreamName + - connectorId + properties: + dataStreamName: + $ref: "../model/common_attributes.schema.yaml#/components/schemas/DataStreamName" + apiDefinition: + $ref: "../model/cel_input_attributes.schema.yaml#/components/schemas/ApiDefinition" + connectorId: + $ref: "../model/common_attributes.schema.yaml#/components/schemas/Connector" + langSmithOptions: + $ref: "../model/common_attributes.schema.yaml#/components/schemas/LangSmithOptions" + responses: + 200: + description: Indicates a successful call. + content: + application/json: + schema: + $ref: "../model/response_schemas.schema.yaml#/components/schemas/CelInputAPIResponse" diff --git a/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.test.ts b/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.test.ts new file mode 100644 index 0000000000000..8518decc102e2 --- /dev/null +++ b/x-pack/plugins/integration_assistant/common/api/cel/cel_input_route.test.ts @@ -0,0 +1,20 @@ +/* + * 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 { expectParseSuccess } from '@kbn/zod-helpers'; +import { getCelRequestMock } from '../model/api_test.mock'; +import { CelInputRequestBody } from './cel_input_route.gen'; + +describe('Cel request schema', () => { + test('full request validate', () => { + const payload: CelInputRequestBody = getCelRequestMock(); + + const result = CelInputRequestBody.safeParse(payload); + expectParseSuccess(result); + expect(result.data).toEqual(payload); + }); +}); diff --git a/x-pack/plugins/integration_assistant/common/api/model/api_test.mock.ts b/x-pack/plugins/integration_assistant/common/api/model/api_test.mock.ts index de29624cbb21a..ea2aa61417526 100644 --- a/x-pack/plugins/integration_assistant/common/api/model/api_test.mock.ts +++ b/x-pack/plugins/integration_assistant/common/api/model/api_test.mock.ts @@ -8,6 +8,7 @@ import type { AnalyzeLogsRequestBody } from '../analyze_logs/analyze_logs_route.gen'; import type { BuildIntegrationRequestBody } from '../build_integration/build_integration.gen'; import type { CategorizationRequestBody } from '../categorization/categorization_route.gen'; +import type { CelInputRequestBody } from '../cel/cel_input_route.gen'; import type { EcsMappingRequestBody } from '../ecs/ecs_route.gen'; import type { RelatedRequestBody } from '../related/related_route.gen'; import type { DataStream, Integration, Pipeline } from './common_attributes.gen'; @@ -65,6 +66,12 @@ export const getCategorizationRequestMock = (): CategorizationRequestBody => ({ samplesFormat: { name: 'ndjson' }, }); +export const getCelRequestMock = (): CelInputRequestBody => ({ + dataStreamName: 'test-data-stream-name', + apiDefinition: 'test-api-definition', + connectorId: 'test-connector-id', +}); + export const getBuildIntegrationRequestMock = (): BuildIntegrationRequestBody => ({ integration: getIntegrationMock(), }); diff --git a/x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.gen.ts b/x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.gen.ts new file mode 100644 index 0000000000000..9ee1ee2ed290f --- /dev/null +++ b/x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.gen.ts @@ -0,0 +1,33 @@ +/* + * 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. + */ + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Cel Input Attributes + * version: not applicable + */ + +import { z } from '@kbn/zod'; + +/** + * String form of the Open API schema. + */ +export type ApiDefinition = z.infer; +export const ApiDefinition = z.string(); + +/** + * Optional CEL input details. + */ +export type CelInput = z.infer; +export const CelInput = z.object({ + program: z.string(), + stateSettings: z.object({}).catchall(z.unknown()), + redactVars: z.array(z.string()), +}); diff --git a/x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.schema.yaml b/x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.schema.yaml new file mode 100644 index 0000000000000..cd05202ddfda0 --- /dev/null +++ b/x-pack/plugins/integration_assistant/common/api/model/cel_input_attributes.schema.yaml @@ -0,0 +1,30 @@ +openapi: 3.0.3 +info: + title: Cel Input Attributes + version: "not applicable" +paths: {} +components: + x-codegen-enabled: true + schemas: + ApiDefinition: + type: string + description: String form of the Open API schema. + + CelInput: + type: object + description: Optional CEL input details. + required: + - program + - stateSettings + - redactVars + properties: + program: + type: string + stateSettings: + type: object + additionalProperties: true + redactVars: + type: array + items: + type: string + \ No newline at end of file diff --git a/x-pack/plugins/integration_assistant/common/api/model/common_attributes.gen.ts b/x-pack/plugins/integration_assistant/common/api/model/common_attributes.gen.ts index 49e6f12691429..7b64b4f8a88d8 100644 --- a/x-pack/plugins/integration_assistant/common/api/model/common_attributes.gen.ts +++ b/x-pack/plugins/integration_assistant/common/api/model/common_attributes.gen.ts @@ -17,6 +17,7 @@ import { z } from '@kbn/zod'; import { ESProcessorItem } from './processor_attributes.gen'; +import { CelInput } from './cel_input_attributes.gen'; /** * Package name for the integration to be built. @@ -178,6 +179,10 @@ export const DataStream = z.object({ * The format of log samples in this dataStream. */ samplesFormat: SamplesFormat, + /** + * The optional CEL input configuration for the dataStream. + */ + celInput: CelInput.optional(), }); /** diff --git a/x-pack/plugins/integration_assistant/common/api/model/common_attributes.schema.yaml b/x-pack/plugins/integration_assistant/common/api/model/common_attributes.schema.yaml index 073b485b1cb3d..aba43d0174bb8 100644 --- a/x-pack/plugins/integration_assistant/common/api/model/common_attributes.schema.yaml +++ b/x-pack/plugins/integration_assistant/common/api/model/common_attributes.schema.yaml @@ -156,6 +156,9 @@ components: samplesFormat: $ref: "#/components/schemas/SamplesFormat" description: The format of log samples in this dataStream. + celInput: + $ref: "./cel_input_attributes.schema.yaml#/components/schemas/CelInput" + description: The optional CEL input configuration for the dataStream. Integration: type: object diff --git a/x-pack/plugins/integration_assistant/common/api/model/response_schemas.gen.ts b/x-pack/plugins/integration_assistant/common/api/model/response_schemas.gen.ts index acb4954c21b90..f13b6fc537235 100644 --- a/x-pack/plugins/integration_assistant/common/api/model/response_schemas.gen.ts +++ b/x-pack/plugins/integration_assistant/common/api/model/response_schemas.gen.ts @@ -18,6 +18,7 @@ import { z } from '@kbn/zod'; import { Mapping, Pipeline, Docs, SamplesFormat } from './common_attributes.gen'; import { ESProcessorItem } from './processor_attributes.gen'; +import { CelInput } from './cel_input_attributes.gen'; export type EcsMappingAPIResponse = z.infer; export const EcsMappingAPIResponse = z.object({ @@ -58,3 +59,8 @@ export const AnalyzeLogsAPIResponse = z.object({ parsedSamples: z.array(z.string()), }), }); + +export type CelInputAPIResponse = z.infer; +export const CelInputAPIResponse = z.object({ + results: CelInput, +}); diff --git a/x-pack/plugins/integration_assistant/common/api/model/response_schemas.schema.yaml b/x-pack/plugins/integration_assistant/common/api/model/response_schemas.schema.yaml index c504ad8b17d16..62776b9dc5c13 100644 --- a/x-pack/plugins/integration_assistant/common/api/model/response_schemas.schema.yaml +++ b/x-pack/plugins/integration_assistant/common/api/model/response_schemas.schema.yaml @@ -88,3 +88,11 @@ components: type: array items: type: string + + CelInputAPIResponse: + type: object + required: + - results + properties: + results: + $ref: "./cel_input_attributes.schema.yaml#/components/schemas/CelInput" \ No newline at end of file diff --git a/x-pack/plugins/integration_assistant/common/constants.ts b/x-pack/plugins/integration_assistant/common/constants.ts index 3891c1e5e4343..1472a260fadf0 100644 --- a/x-pack/plugins/integration_assistant/common/constants.ts +++ b/x-pack/plugins/integration_assistant/common/constants.ts @@ -20,6 +20,7 @@ export const ECS_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/ecs`; export const CATEGORIZATION_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/categorization`; export const ANALYZE_LOGS_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/analyzelogs`; export const RELATED_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/related`; +export const CEL_INPUT_GRAPH_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/cel`; export const CHECK_PIPELINE_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/pipeline`; export const INTEGRATION_BUILDER_PATH = `${INTEGRATION_ASSISTANT_BASE_PATH}/build`; export const FLEET_PACKAGES_PATH = `/api/fleet/epm/packages`; diff --git a/x-pack/plugins/integration_assistant/common/experimental_features.ts b/x-pack/plugins/integration_assistant/common/experimental_features.ts index 7de449b1b31da..76b4ed6f28fee 100644 --- a/x-pack/plugins/integration_assistant/common/experimental_features.ts +++ b/x-pack/plugins/integration_assistant/common/experimental_features.ts @@ -8,8 +8,10 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; const _allowedExperimentalValues = { - // Leaving this in here until we have a 'real' experimental feature - testFeature: false, + /** + * Enables whether the user is able to utilize the LLM to generate the CEL input configuration. + */ + generateCel: false, }; /** diff --git a/x-pack/plugins/integration_assistant/common/index.ts b/x-pack/plugins/integration_assistant/common/index.ts index 21ee814655e10..5e90e6e6a6217 100644 --- a/x-pack/plugins/integration_assistant/common/index.ts +++ b/x-pack/plugins/integration_assistant/common/index.ts @@ -19,6 +19,7 @@ export { AnalyzeLogsRequestBody, AnalyzeLogsResponse, } from './api/analyze_logs/analyze_logs_route.gen'; +export { CelInputRequestBody, CelInputResponse } from './api/cel/cel_input_route.gen'; export type { DataStream, @@ -31,9 +32,11 @@ export type { } from './api/model/common_attributes.gen'; export { SamplesFormatName } from './api/model/common_attributes.gen'; export type { ESProcessorItem } from './api/model/processor_attributes.gen'; +export type { CelInput } from './api/model/cel_input_attributes.gen'; export { CATEGORIZATION_GRAPH_PATH, + CEL_INPUT_GRAPH_PATH, ECS_GRAPH_PATH, INTEGRATION_ASSISTANT_APP_ROUTE, INTEGRATION_ASSISTANT_BASE_PATH, diff --git a/x-pack/plugins/integration_assistant/public/common/lib/api.ts b/x-pack/plugins/integration_assistant/public/common/lib/api.ts index 46c9487df4b7f..0a6d91375219e 100644 --- a/x-pack/plugins/integration_assistant/public/common/lib/api.ts +++ b/x-pack/plugins/integration_assistant/public/common/lib/api.ts @@ -18,12 +18,15 @@ import type { BuildIntegrationRequestBody, AnalyzeLogsRequestBody, AnalyzeLogsResponse, + CelInputRequestBody, + CelInputResponse, } from '../../../common'; import { INTEGRATION_BUILDER_PATH, ECS_GRAPH_PATH, CATEGORIZATION_GRAPH_PATH, RELATED_GRAPH_PATH, + CEL_INPUT_GRAPH_PATH, CHECK_PIPELINE_PATH, } from '../../../common'; import { ANALYZE_LOGS_PATH, FLEET_PACKAGES_PATH } from '../../../common/constants'; @@ -84,6 +87,16 @@ export const runRelatedGraph = async ( signal: abortSignal, }); +export const runCelGraph = async ( + body: CelInputRequestBody, + { http, abortSignal }: RequestDeps +): Promise => + http.post(CEL_INPUT_GRAPH_PATH, { + headers: defaultHeaders, + body: JSON.stringify(body), + signal: abortSignal, + }); + export const runCheckPipelineResults = async ( body: CheckPipelineRequestBody, { http, abortSignal }: RequestDeps diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx index 496f34a943bed..b6fe577865822 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.test.tsx @@ -10,6 +10,7 @@ import { render } from '@testing-library/react'; import { TestProvider } from '../../../mocks/test_provider'; import { CreateIntegrationAssistant } from './create_integration_assistant'; import type { State } from './state'; +import { ExperimentalFeaturesService } from '../../../services'; export const defaultInitialState: State = { step: 1, @@ -26,16 +27,23 @@ jest.mock('./state', () => ({ }, })); +jest.mock('../../../services'); +const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); + const mockConnectorStep = jest.fn(() =>
); const mockIntegrationStep = jest.fn(() =>
); const mockDataStreamStep = jest.fn(() =>
); const mockReviewStep = jest.fn(() =>
); +const mockCelInputStep = jest.fn(() =>
); +const mockReviewCelStep = jest.fn(() =>
); const mockDeployStep = jest.fn(() =>
); const mockIsConnectorStepReady = jest.fn(); const mockIsIntegrationStepReady = jest.fn(); const mockIsDataStreamStepReady = jest.fn(); const mockIsReviewStepReady = jest.fn(); +const mockIsCelInputStepReady = jest.fn(); +const mockIsCelReviewStepReady = jest.fn(); jest.mock('./steps/connector_step', () => ({ ConnectorStep: () => mockConnectorStep(), @@ -53,6 +61,14 @@ jest.mock('./steps/review_step', () => ({ ReviewStep: () => mockReviewStep(), isReviewStepReady: () => mockIsReviewStepReady(), })); +jest.mock('./steps/cel_input_step', () => ({ + CelInputStep: () => mockCelInputStep(), + isCelInputStepReady: () => mockIsCelInputStepReady(), +})); +jest.mock('./steps/review_cel_step', () => ({ + ReviewCelStep: () => mockReviewCelStep(), + isCelReviewStepReady: () => mockIsCelReviewStepReady(), +})); jest.mock('./steps/deploy_step', () => ({ DeployStep: () => mockDeployStep() })); const renderIntegrationAssistant = () => @@ -61,6 +77,10 @@ const renderIntegrationAssistant = () => describe('CreateIntegration', () => { beforeEach(() => { jest.clearAllMocks(); + + mockedExperimentalFeaturesService.get.mockReturnValue({ + generateCel: false, + } as never); }); describe('when step is 1', () => { @@ -138,3 +158,56 @@ describe('CreateIntegration', () => { }); }); }); + +describe('CreateIntegration with generateCel enabled', () => { + beforeEach(() => { + jest.clearAllMocks(); + + mockedExperimentalFeaturesService.get.mockReturnValue({ + generateCel: true, + } as never); + }); + + describe('when step is 5', () => { + beforeEach(() => { + mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 5 }); + }); + + it('should render cel input', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('celInputStepMock')).toBeInTheDocument(); + }); + + it('should call isCelInputStepReady', () => { + renderIntegrationAssistant(); + expect(mockIsCelInputStepReady).toHaveBeenCalled(); + }); + }); + + describe('when step is 6', () => { + beforeEach(() => { + mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 6 }); + }); + + it('should render review', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('reviewCelStepMock')).toBeInTheDocument(); + }); + + it('should call isReviewCelStepReady', () => { + renderIntegrationAssistant(); + expect(mockIsCelReviewStepReady).toHaveBeenCalled(); + }); + }); + + describe('when step is 7', () => { + beforeEach(() => { + mockInitialState.mockReturnValueOnce({ ...defaultInitialState, step: 7 }); + }); + + it('should render deploy', () => { + const result = renderIntegrationAssistant(); + expect(result.queryByTestId('deployStepMock')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx index adc8a05654551..1297e7c975e3b 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/create_integration_assistant.tsx @@ -13,13 +13,18 @@ import { ConnectorStep, isConnectorStepReady } from './steps/connector_step'; import { IntegrationStep, isIntegrationStepReady } from './steps/integration_step'; import { DataStreamStep, isDataStreamStepReady } from './steps/data_stream_step'; import { ReviewStep, isReviewStepReady } from './steps/review_step'; +import { CelInputStep, isCelInputStepReady } from './steps/cel_input_step'; +import { ReviewCelStep, isCelReviewStepReady } from './steps/review_cel_step'; import { DeployStep } from './steps/deploy_step'; import { reducer, initialState, ActionsProvider, type Actions } from './state'; import { useTelemetry } from '../telemetry'; +import { ExperimentalFeaturesService } from '../../../services'; export const CreateIntegrationAssistant = React.memo(() => { const [state, dispatch] = useReducer(reducer, initialState); + const { generateCel: isGenerateCelEnabled } = ExperimentalFeaturesService.get(); + const telemetry = useTelemetry(); useEffect(() => { telemetry.reportAssistantOpen(); @@ -42,6 +47,9 @@ export const CreateIntegrationAssistant = React.memo(() => { setResult: (payload) => { dispatch({ type: 'SET_GENERATED_RESULT', payload }); }, + setCelInputResult: (payload) => { + dispatch({ type: 'SET_CEL_INPUT_RESULT', payload }); + }, }), [] ); @@ -55,9 +63,13 @@ export const CreateIntegrationAssistant = React.memo(() => { return isDataStreamStepReady(state); } else if (state.step === 4) { return isReviewStepReady(state); + } else if (isGenerateCelEnabled && state.step === 5) { + return isCelInputStepReady(state); + } else if (isGenerateCelEnabled && state.step === 6) { + return isCelReviewStepReady(state); } return false; - }, [state]); + }, [state, isGenerateCelEnabled]); return ( @@ -80,10 +92,32 @@ export const CreateIntegrationAssistant = React.memo(() => { result={state.result} /> )} - {state.step === 5 && ( + {state.step === 5 && + (isGenerateCelEnabled ? ( + + ) : ( + + ))} + + {isGenerateCelEnabled && state.step === 6 && ( + + )} + {isGenerateCelEnabled && state.step === 7 && ( )} diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.test.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.test.tsx index d06762b4656fa..900a72ab272a0 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.test.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.test.tsx @@ -13,6 +13,7 @@ import { ActionsProvider } from '../state'; import { mockActions } from '../mocks/state'; import { mockReportEvent } from '../../../../services/telemetry/mocks/service'; import { TelemetryEventType } from '../../../../services/telemetry/types'; +import { ExperimentalFeaturesService } from '../../../../services'; const mockNavigate = jest.fn(); jest.mock('../../../../common/hooks/use_navigate', () => ({ @@ -20,6 +21,9 @@ jest.mock('../../../../common/hooks/use_navigate', () => ({ useNavigate: () => mockNavigate, })); +jest.mock('../../../../services'); +const mockedExperimentalFeaturesService = jest.mocked(ExperimentalFeaturesService); + const wrapper: React.FC> = ({ children }) => ( {children} @@ -29,6 +33,10 @@ const wrapper: React.FC> = ({ children }) => ( describe('Footer', () => { beforeEach(() => { jest.clearAllMocks(); + + mockedExperimentalFeaturesService.get.mockReturnValue({ + generateCel: false, + } as never); }); describe('when rendered', () => { diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.tsx index eefbbb6b385c3..9a2f862264e27 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/footer.tsx @@ -12,6 +12,7 @@ import { useNavigate, Page } from '../../../../common/hooks/use_navigate'; import { useTelemetry } from '../../telemetry'; import { useActions, type State } from '../state'; import * as i18n from './translations'; +import { ExperimentalFeaturesService } from '../../../../services'; // Generation button for Step 3 const AnalyzeButtonText = React.memo<{ isGenerating: boolean }>(({ isGenerating }) => { @@ -27,6 +28,20 @@ const AnalyzeButtonText = React.memo<{ isGenerating: boolean }>(({ isGenerating }); AnalyzeButtonText.displayName = 'AnalyzeButtonText'; +// Generation button for Step 5 +const AnalyzeCelButtonText = React.memo<{ isGenerating: boolean }>(({ isGenerating }) => { + if (!isGenerating) { + return <>{i18n.ANALYZE_CEL}; + } + return ( + <> + + {i18n.LOADING} + + ); +}); +AnalyzeCelButtonText.displayName = 'AnalyzeCelButtonText'; + interface FooterProps { currentStep: State['step']; isGenerating: State['isGenerating']; @@ -39,6 +54,8 @@ export const Footer = React.memo( const { setStep, setIsGenerating } = useActions(); const navigate = useNavigate(); + const { generateCel: isGenerateCelEnabled } = ExperimentalFeaturesService.get(); + const onBack = useCallback(() => { if (currentStep === 1) { navigate(Page.landing); @@ -49,7 +66,7 @@ export const Footer = React.memo( const onNext = useCallback(() => { telemetry.reportAssistantStepComplete({ step: currentStep }); - if (currentStep === 3) { + if (currentStep === 3 || currentStep === 5) { setIsGenerating(true); } else { setStep(currentStep + 1); @@ -60,12 +77,18 @@ export const Footer = React.memo( if (currentStep === 3) { return ; } - if (currentStep === 4) { + if (currentStep === 4 && !isGenerateCelEnabled) { + return i18n.ADD_TO_ELASTIC; + } + if (currentStep === 5 && isGenerateCelEnabled) { + return ; + } + if (currentStep === 6 && isGenerateCelEnabled) { return i18n.ADD_TO_ELASTIC; } - }, [currentStep, isGenerating]); + }, [currentStep, isGenerating, isGenerateCelEnabled]); - if (currentStep === 5) { + if (currentStep === 7 || (currentStep === 5 && !isGenerateCelEnabled)) { return ; } return ( diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/translations.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/translations.ts index aedfd63ba2213..3f568486617f2 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/translations.ts +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/footer/translations.ts @@ -11,6 +11,10 @@ export const ANALYZE_LOGS = i18n.translate('xpack.integrationAssistant.bottomBar defaultMessage: 'Analyze logs', }); +export const ANALYZE_CEL = i18n.translate('xpack.integrationAssistant.bottomBar.analyzeCel', { + defaultMessage: 'Generate CEL input configuration', +}); + export const LOADING = i18n.translate('xpack.integrationAssistant.bottomBar.loading', { defaultMessage: 'Loading', }); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/mocks/state.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/mocks/state.ts index c842d097fa7c3..452d5e65a972c 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/mocks/state.ts +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/mocks/state.ts @@ -432,4 +432,5 @@ export const mockActions: Actions = { setIntegrationSettings: jest.fn(), setIsGenerating: jest.fn(), setResult: jest.fn(), + setCelInputResult: jest.fn(), }; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/state.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/state.ts index 99f95a3eee039..0492012ab8686 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/state.ts +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/state.ts @@ -5,7 +5,7 @@ * 2.0. */ import { createContext, useContext } from 'react'; -import type { Pipeline, Docs, SamplesFormat } from '../../../../common'; +import type { Pipeline, Docs, SamplesFormat, CelInput } from '../../../../common'; import type { AIConnector, IntegrationSettings } from './types'; export interface State { @@ -18,6 +18,7 @@ export interface State { docs: Docs; samplesFormat?: SamplesFormat; }; + celInputResult?: CelInput; } export const initialState: State = { @@ -33,7 +34,8 @@ type Action = | { type: 'SET_CONNECTOR'; payload: State['connector'] } | { type: 'SET_INTEGRATION_SETTINGS'; payload: State['integrationSettings'] } | { type: 'SET_IS_GENERATING'; payload: State['isGenerating'] } - | { type: 'SET_GENERATED_RESULT'; payload: State['result'] }; + | { type: 'SET_GENERATED_RESULT'; payload: State['result'] } + | { type: 'SET_CEL_INPUT_RESULT'; payload: State['celInputResult'] }; export const reducer = (state: State, action: Action): State => { switch (action.type) { @@ -56,6 +58,8 @@ export const reducer = (state: State, action: Action): State => { // keep original result as the samplesFormat is not always included in the payload result: state.result ? { ...state.result, ...action.payload } : action.payload, }; + case 'SET_CEL_INPUT_RESULT': + return { ...state, celInputResult: action.payload }; default: return state; } @@ -67,6 +71,7 @@ export interface Actions { setIntegrationSettings: (payload: State['integrationSettings']) => void; setIsGenerating: (payload: State['isGenerating']) => void; setResult: (payload: State['result']) => void; + setCelInputResult: (payload: State['celInputResult']) => void; } const ActionsContext = createContext(undefined); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/api_definition_input.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/api_definition_input.tsx new file mode 100644 index 0000000000000..9a9f36efcddb8 --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/api_definition_input.tsx @@ -0,0 +1,118 @@ +/* + * 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 React, { useCallback, useState } from 'react'; +import { EuiFilePicker, EuiFormRow, EuiText } from '@elastic/eui'; +import type { IntegrationSettings } from '../../types'; +import * as i18n from './translations'; +import { useActions } from '../../state'; + +interface ApiDefinitionInputProps { + integrationSettings: IntegrationSettings | undefined; +} + +export const ApiDefinitionInput = React.memo(({ integrationSettings }) => { + const { setIntegrationSettings } = useActions(); + const [isParsing, setIsParsing] = useState(false); + const [apiFileError, setApiFileError] = useState(); + + const onChangeApiDefinition = useCallback( + (files: FileList | null) => { + if (!files) { + return; + } + + setApiFileError(undefined); + setIntegrationSettings({ + ...integrationSettings, + apiDefinition: undefined, + }); + + const apiDefinitionFile = files[0]; + const reader = new FileReader(); + + reader.onloadstart = function () { + setIsParsing(true); + }; + + reader.onloadend = function () { + setIsParsing(false); + }; + + reader.onload = function (e) { + const fileContent = e.target?.result as string | undefined; // We can safely cast to string since we call `readAsText` to load the file. + + if (fileContent == null) { + setApiFileError(i18n.API_DEFINITION_ERROR.CAN_NOT_READ); + return; + } + + if (fileContent === '' && e.loaded > 100000) { + // V8-based browsers can't handle large files and return an empty string + // instead of an error; see https://stackoverflow.com/a/61316641 + setApiFileError(i18n.API_DEFINITION_ERROR.TOO_LARGE_TO_PARSE); + return; + } + + setIntegrationSettings({ + ...integrationSettings, + apiDefinition: fileContent, + }); + }; + + const handleReaderError = function () { + const message = reader.error?.message; + if (message) { + setApiFileError(i18n.API_DEFINITION_ERROR.CAN_NOT_READ_WITH_REASON(message)); + } else { + setApiFileError(i18n.API_DEFINITION_ERROR.CAN_NOT_READ); + } + }; + + reader.onerror = handleReaderError; + reader.onabort = handleReaderError; + + reader.readAsText(apiDefinitionFile); + }, + [integrationSettings, setIntegrationSettings, setIsParsing] + ); + + return ( + + {apiFileError} + + } + isInvalid={apiFileError != null} + > + <> + + + {i18n.API_DEFINITION_DESCRIPTION} + + + {i18n.API_DEFINITION_DESCRIPTION_2} + + + } + onChange={onChangeApiDefinition} + display="large" + aria-label="Upload API definition file" + isLoading={isParsing} + data-test-subj="apiDefinitionFilePicker" + data-loading={isParsing} + /> + + + ); +}); +ApiDefinitionInput.displayName = 'ApiDefinitionInput'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/cel_input_step.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/cel_input_step.tsx new file mode 100644 index 0000000000000..b0a0b3194ec33 --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/cel_input_step.tsx @@ -0,0 +1,64 @@ +/* + * 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 React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiForm, EuiPanel } from '@elastic/eui'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { useActions, type State } from '../../state'; +import type { OnComplete } from './generation_modal'; +import { GenerationModal } from './generation_modal'; +import { ApiDefinitionInput } from './api_definition_input'; +import * as i18n from './translations'; + +interface CelInputStepProps { + integrationSettings: State['integrationSettings']; + connector: State['connector']; + isGenerating: State['isGenerating']; +} + +export const CelInputStep = React.memo( + ({ integrationSettings, connector, isGenerating }) => { + const { setIsGenerating, setStep, setCelInputResult } = useActions(); + + const onGenerationCompleted = useCallback( + (result: State['celInputResult']) => { + if (result) { + setCelInputResult(result); + setIsGenerating(false); + setStep(6); + } + }, + [setCelInputResult, setIsGenerating, setStep] + ); + const onGenerationClosed = useCallback(() => { + setIsGenerating(false); // aborts generation + }, [setIsGenerating]); + + return ( + + + + + + + + + + {isGenerating && ( + + )} + + + ); + } +); +CelInputStep.displayName = 'CelInputStep'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/generation_modal.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/generation_modal.tsx new file mode 100644 index 0000000000000..db4a89754bbc8 --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/generation_modal.tsx @@ -0,0 +1,222 @@ +/* + * 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 { + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiSpacer, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useState } from 'react'; +import { css } from '@emotion/react'; +import { getLangSmithOptions } from '../../../../../common/lib/lang_smith'; +import { type CelInputRequestBody } from '../../../../../../common'; +import { runCelGraph } from '../../../../../common/lib/api'; +import { useKibana } from '../../../../../common/hooks/use_kibana'; +import type { State } from '../../state'; +import * as i18n from './translations'; +import { useTelemetry } from '../../../telemetry'; + +export type OnComplete = (result: State['celInputResult']) => void; + +interface UseGenerationProps { + integrationSettings: State['integrationSettings']; + connector: State['connector']; + onComplete: OnComplete; +} +export const useGeneration = ({ + integrationSettings, + connector, + onComplete, +}: UseGenerationProps) => { + const { reportCelGenerationComplete } = useTelemetry(); + const { http, notifications } = useKibana().services; + const [error, setError] = useState(null); + const [isRequesting, setIsRequesting] = useState(true); + + useEffect(() => { + if ( + !isRequesting || + http == null || + connector == null || + integrationSettings == null || + notifications?.toasts == null + ) { + return; + } + const generationStartedAt = Date.now(); + const abortController = new AbortController(); + const deps = { http, abortSignal: abortController.signal }; + + (async () => { + try { + const apiDefinition = integrationSettings.apiDefinition; + const celRequest: CelInputRequestBody = { + dataStreamName: integrationSettings.dataStreamName ?? '', + apiDefinition: apiDefinition ?? '', + connectorId: connector.id, + langSmithOptions: getLangSmithOptions(), + }; + const celGraphResult = await runCelGraph(celRequest, deps); + + if (abortController.signal.aborted) return; + + if (isEmpty(celGraphResult?.results)) { + throw new Error('Results not found in response'); + } + + reportCelGenerationComplete({ + connector, + integrationSettings, + durationMs: Date.now() - generationStartedAt, + }); + + const result = { + program: celGraphResult.results.program, + stateSettings: celGraphResult.results.stateSettings, + redactVars: celGraphResult.results.redactVars, + }; + + onComplete(result); + } catch (e) { + if (abortController.signal.aborted) return; + const errorMessage = `${e.message}${ + e.body ? ` (${e.body.statusCode}): ${e.body.message}` : '' + }`; + + reportCelGenerationComplete({ + connector, + integrationSettings, + durationMs: Date.now() - generationStartedAt, + error: errorMessage, + }); + + setError(errorMessage); + } finally { + setIsRequesting(false); + } + })(); + return () => { + abortController.abort(); + }; + }, [ + isRequesting, + onComplete, + connector, + http, + integrationSettings, + reportCelGenerationComplete, + notifications?.toasts, + ]); + + const retry = useCallback(() => { + setError(null); + setIsRequesting(true); + }, []); + + return { error, retry }; +}; + +const useModalCss = () => { + const { euiTheme } = useEuiTheme(); + return { + headerCss: css` + justify-content: center; + margin-top: ${euiTheme.size.m}; + `, + bodyCss: css` + padding: ${euiTheme.size.xxxxl}; + min-width: 600px; + `, + }; +}; + +interface GenerationModalProps { + integrationSettings: State['integrationSettings']; + connector: State['connector']; + onComplete: OnComplete; + onClose: () => void; +} +export const GenerationModal = React.memo( + ({ integrationSettings, connector, onComplete, onClose }) => { + const { headerCss, bodyCss } = useModalCss(); + const { error, retry } = useGeneration({ + integrationSettings, + connector, + onComplete, + }); + + return ( + + + {i18n.ANALYZING} + + + + {error ? ( + + + {error} + + + ) : ( + <> + + + + + + + + {i18n.PROGRESS_CEL_INPUT_GRAPH} + + + + + + + )} + + + + {error ? ( + + + + {i18n.RETRY} + + + + ) : ( + + )} + + + ); + } +); +GenerationModal.displayName = 'GenerationModal'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/index.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/index.ts new file mode 100644 index 0000000000000..a04847f478de0 --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { CelInputStep } from './cel_input_step'; +export * from './is_step_ready'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/is_step_ready.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/is_step_ready.ts new file mode 100644 index 0000000000000..594f4230164ce --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/is_step_ready.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 type { State } from '../../state'; + +export const isCelInputStepReady = ({ integrationSettings }: State) => + Boolean( + integrationSettings?.name && + integrationSettings?.dataStreamTitle && + integrationSettings?.dataStreamDescription && + integrationSettings?.dataStreamName && + integrationSettings?.apiDefinition + ); +// TODO add support for not uploading a spec file diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/translations.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/translations.ts new file mode 100644 index 0000000000000..60853f9665d74 --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/cel_input_step/translations.ts @@ -0,0 +1,87 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ANALYZING = i18n.translate('xpack.integrationAssistant.step.dataStream.analyzing', { + defaultMessage: 'Analyzing', +}); + +export const CEL_INPUT_TITLE = i18n.translate( + 'xpack.integrationAssistant.step.celInput.celInputTitle', + { + defaultMessage: 'Generate CEL input configuration', + } +); +export const CEL_INPUT_DESCRIPTION = i18n.translate( + 'xpack.integrationAssistant.step.celInput.celInputDescription', + { + defaultMessage: 'Upload an OpenAPI spec file to generate a configuration for the CEL input', + } +); + +export const API_DEFINITION_LABEL = i18n.translate( + 'xpack.integrationAssistant.step.celInput.apiDefinition.label', + { + defaultMessage: 'OpenAPI spec', + } +); + +export const API_DEFINITION_DESCRIPTION = i18n.translate( + 'xpack.integrationAssistant.step.celInput.apiDefinition.description', + { + defaultMessage: 'Drag and drop a file or browse files.', + } +); + +export const API_DEFINITION_DESCRIPTION_2 = i18n.translate( + 'xpack.integrationAssistant.step.celInput.apiDefinition.description2', + { + defaultMessage: 'OpenAPI specification', + } +); + +export const API_DEFINITION_ERROR = { + CAN_NOT_READ: i18n.translate( + 'xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotRead', + { + defaultMessage: 'Failed to read the logs sample file', + } + ), + CAN_NOT_READ_WITH_REASON: (reason: string) => + i18n.translate( + 'xpack.integrationAssistant.step.celInput.openapiSpec.errorCanNotReadWithReason', + { + values: { reason }, + defaultMessage: 'An error occurred when reading spec file: {reason}', + } + ), + TOO_LARGE_TO_PARSE: i18n.translate( + 'xpack.integrationAssistant.step.celInput.openapiSpec.errorTooLargeToParse', + { + defaultMessage: 'This spec file is too large to parse', + } + ), +}; + +export const PROGRESS_CEL_INPUT_GRAPH = i18n.translate( + 'xpack.integrationAssistant.step.celInput.progress.relatedGraph', + { + defaultMessage: 'Generating CEL input configuration', + } +); + +export const GENERATION_ERROR = i18n.translate( + 'xpack.integrationAssistant.step.celInput.generationError', + { + defaultMessage: 'An error occurred during: CEL input generation', + } +); + +export const RETRY = i18n.translate('xpack.integrationAssistant.step.celInput.retryButtonLabel', { + defaultMessage: 'Retry', +}); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/deploy_step.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/deploy_step.tsx index 81f211de80b1e..0f50fa8104eb5 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/deploy_step.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/deploy_step.tsx @@ -26,14 +26,16 @@ import * as i18n from './translations'; interface DeployStepProps { integrationSettings: State['integrationSettings']; result: State['result']; + celInputResult?: State['celInputResult']; connector: State['connector']; } export const DeployStep = React.memo( - ({ integrationSettings, result, connector }) => { + ({ integrationSettings, result, celInputResult, connector }) => { const { isLoading, error, integrationFile, integrationName } = useDeployIntegration({ integrationSettings, result, + celInputResult, connector, }); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/use_deploy_integration.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/use_deploy_integration.ts index 5ec27b4e6de65..e1947e090f2fd 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/use_deploy_integration.ts +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/deploy_step/use_deploy_integration.ts @@ -16,12 +16,14 @@ import { useTelemetry } from '../../../telemetry'; interface PipelineGenerationProps { integrationSettings: State['integrationSettings']; result: State['result']; + celInputResult: State['celInputResult']; connector: State['connector']; } export const useDeployIntegration = ({ integrationSettings, result, + celInputResult, connector, }: PipelineGenerationProps) => { const telemetry = useTelemetry(); @@ -63,6 +65,7 @@ export const useDeployIntegration = ({ docs: result.docs ?? [], samplesFormat: result.samplesFormat ?? { name: 'json' }, pipeline: result.pipeline, + celInput: celInputResult, }, ], }, @@ -119,6 +122,7 @@ export const useDeployIntegration = ({ result?.docs, result?.pipeline, result?.samplesFormat, + celInputResult, telemetry, ]); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/cel_config_results.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/cel_config_results.tsx new file mode 100644 index 0000000000000..91577ed69491a --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/cel_config_results.tsx @@ -0,0 +1,65 @@ +/* + * 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 React from 'react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiCodeBlock, + EuiText, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; +import { type State } from '../../state'; +import * as i18n from './translations'; + +interface CelConfigResultsProps { + celInputResult: State['celInputResult']; +} + +export const CelConfigResults = React.memo(({ celInputResult }) => { + return ( + + + + +

{i18n.PROGRAM}

+
+ + + {celInputResult?.program} + +
+
+ + + + +

{i18n.STATE}

+
+ + + {JSON.stringify(celInputResult?.stateSettings, null, 2)} + +
+
+ + + + +

{i18n.REDACT_VARS}

+
+ + + {JSON.stringify(celInputResult?.redactVars)} + +
+
+
+ ); +}); +CelConfigResults.displayName = 'CelConfigForm'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/index.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/index.ts new file mode 100644 index 0000000000000..d396bd17d7a9f --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ +export { ReviewCelStep } from './review_cel_step'; +export * from './is_step_ready'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/is_step_ready.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/is_step_ready.ts new file mode 100644 index 0000000000000..166c40e8e2614 --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/is_step_ready.ts @@ -0,0 +1,11 @@ +/* + * 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 type { State } from '../../state'; + +export const isCelReviewStepReady = ({ isGenerating, celInputResult }: State) => + isGenerating === false && celInputResult != null; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/review_cel_step.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/review_cel_step.tsx new file mode 100644 index 0000000000000..a40fec082894e --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/review_cel_step.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; +import React from 'react'; +import type { State } from '../../state'; +import { StepContentWrapper } from '../step_content_wrapper'; +import * as i18n from './translations'; +import { CelConfigResults } from './cel_config_results'; + +interface ReviewCelStepProps { + celInputResult: State['celInputResult']; + isGenerating: State['isGenerating']; +} + +export const ReviewCelStep = React.memo(({ isGenerating, celInputResult }) => { + return ( + + + {isGenerating ? ( + + ) : ( + <> + + + )} + + + ); +}); +ReviewCelStep.displayName = 'ReviewCelStep'; diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/translations.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/translations.ts new file mode 100644 index 0000000000000..25cd90565a3ec --- /dev/null +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/steps/review_cel_step/translations.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 { i18n } from '@kbn/i18n'; + +export const TITLE = i18n.translate('xpack.integrationAssistant.step.reviewCel.title', { + defaultMessage: 'Review results', +}); +export const DESCRIPTION = i18n.translate('xpack.integrationAssistant.step.reviewCel.description', { + defaultMessage: + 'Review the generated CEL input configuration settings for your integration. These settings will be auto-populated into the integration configuration where editing will be possible.', +}); + +export const PROGRAM = i18n.translate('xpack.integrationAssistant.step.reviewCel.program', { + defaultMessage: 'The CEL program to be run for each polling', +}); +export const STATE = i18n.translate('xpack.integrationAssistant.step.reviewCel.state', { + defaultMessage: 'Initial CEL evaluation state', +}); +export const REDACT_VARS = i18n.translate('xpack.integrationAssistant.step.reviewCel.redact', { + defaultMessage: 'Redacted fields', +}); + +export const SAVE_BUTTON = i18n.translate('xpack.integrationAssistant.step.reviewCel.save', { + defaultMessage: 'Save', +}); diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/types.ts b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/types.ts index 6ba7b2945b7a8..27d053a626775 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/types.ts +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/create_integration_assistant/types.ts @@ -35,4 +35,5 @@ export interface IntegrationSettings { inputTypes?: InputType[]; logSamples?: string[]; samplesFormat?: SamplesFormat; + apiDefinition?: string; } diff --git a/x-pack/plugins/integration_assistant/public/components/create_integration/telemetry.tsx b/x-pack/plugins/integration_assistant/public/components/create_integration/telemetry.tsx index f4dd6d7d436be..39076726b7f9d 100644 --- a/x-pack/plugins/integration_assistant/public/components/create_integration/telemetry.tsx +++ b/x-pack/plugins/integration_assistant/public/components/create_integration/telemetry.tsx @@ -35,6 +35,12 @@ type ReportGenerationComplete = (params: { durationMs: number; error?: string; }) => void; +type ReportCelGenerationComplete = (params: { + connector: AIConnector; + integrationSettings: IntegrationSettings; + durationMs: number; + error?: string; +}) => void; type ReportAssistantComplete = (params: { integrationName: string; integrationSettings: IntegrationSettings; @@ -47,6 +53,7 @@ interface TelemetryContextProps { reportAssistantOpen: ReportAssistantOpen; reportAssistantStepComplete: ReportAssistantStepComplete; reportGenerationComplete: ReportGenerationComplete; + reportCelGenerationComplete: ReportCelGenerationComplete; reportAssistantComplete: ReportAssistantComplete; } @@ -113,6 +120,20 @@ export const TelemetryContextProvider = React.memo>(({ chi [telemetry] ); + const reportCelGenerationComplete = useCallback( + ({ connector, integrationSettings, durationMs, error }) => { + telemetry.reportEvent(TelemetryEventType.IntegrationAssistantCelGenerationComplete, { + sessionId: sessionData.current.sessionId, + actionTypeId: connector.actionTypeId, + model: getConnectorModel(connector), + provider: connector.apiProvider ?? 'unknown', + durationMs, + errorMessage: error, + }); + }, + [telemetry] + ); + const reportAssistantComplete = useCallback( ({ integrationName, integrationSettings, connector, error }) => { telemetry.reportEvent(TelemetryEventType.IntegrationAssistantComplete, { @@ -137,6 +158,7 @@ export const TelemetryContextProvider = React.memo>(({ chi reportAssistantOpen, reportAssistantStepComplete, reportGenerationComplete, + reportCelGenerationComplete, reportAssistantComplete, }), [ @@ -144,6 +166,7 @@ export const TelemetryContextProvider = React.memo>(({ chi reportAssistantOpen, reportAssistantStepComplete, reportGenerationComplete, + reportCelGenerationComplete, reportAssistantComplete, ] ); diff --git a/x-pack/plugins/integration_assistant/public/services/telemetry/events.ts b/x-pack/plugins/integration_assistant/public/services/telemetry/events.ts index 4d2d24ff7a657..0da7cdb9b288c 100644 --- a/x-pack/plugins/integration_assistant/public/services/telemetry/events.ts +++ b/x-pack/plugins/integration_assistant/public/services/telemetry/events.ts @@ -133,6 +133,51 @@ export const telemetryEventsSchemas: TelemetryEventsSchemas = { }, }, + [TelemetryEventType.IntegrationAssistantCelGenerationComplete]: { + sessionId: { + type: 'keyword', + _meta: { + description: 'The ID to identify all the events the same session', + optional: false, + }, + }, + durationMs: { + type: 'long', + _meta: { + description: 'Time spent in the generation process', + optional: false, + }, + }, + actionTypeId: { + type: 'keyword', + _meta: { + description: 'The connector action type ID', + optional: false, + }, + }, + model: { + type: 'keyword', + _meta: { + description: 'The model used to generate the integration', + optional: false, + }, + }, + provider: { + type: 'keyword', + _meta: { + description: 'The provider of the LLM', + optional: false, + }, + }, + errorMessage: { + type: 'text', + _meta: { + description: 'The error message if the generation failed', + optional: true, + }, + }, + }, + [TelemetryEventType.IntegrationAssistantComplete]: { sessionId: { type: 'keyword', diff --git a/x-pack/plugins/integration_assistant/public/services/telemetry/types.ts b/x-pack/plugins/integration_assistant/public/services/telemetry/types.ts index 2f674b5880514..6b5f8e8b558fe 100644 --- a/x-pack/plugins/integration_assistant/public/services/telemetry/types.ts +++ b/x-pack/plugins/integration_assistant/public/services/telemetry/types.ts @@ -11,6 +11,7 @@ export enum TelemetryEventType { IntegrationAssistantOpen = 'integration_assistant_open', IntegrationAssistantStepComplete = 'integration_assistant_step_complete', IntegrationAssistantGenerationComplete = 'integration_assistant_generation_complete', + IntegrationAssistantCelGenerationComplete = 'integration_assistant_cel_generation_complete', IntegrationAssistantComplete = 'integration_assistant_complete', } @@ -43,6 +44,15 @@ interface IntegrationAssistantGenerationCompleteData { errorMessage?: string; } +interface IntegrationAssistantCelGenerationCompleteData { + sessionId: string; + durationMs: number; + actionTypeId: string; + model: string; + provider: string; + errorMessage?: string; +} + interface IntegrationAssistantCompleteData { sessionId: string; durationMs: number; @@ -69,6 +79,8 @@ export type TelemetryEventTypeData = ? IntegrationAssistantStepCompleteData : T extends TelemetryEventType.IntegrationAssistantGenerationComplete ? IntegrationAssistantGenerationCompleteData + : T extends TelemetryEventType.IntegrationAssistantCelGenerationComplete + ? IntegrationAssistantCelGenerationCompleteData : T extends TelemetryEventType.IntegrationAssistantComplete ? IntegrationAssistantCompleteData : never; diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/build_program.test.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/build_program.test.ts new file mode 100644 index 0000000000000..39a3902399848 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/build_program.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { + ActionsClientChatOpenAI, + ActionsClientSimpleChatModel, +} from '@kbn/langchain/server/language_models'; +import { FakeLLM } from '@langchain/core/utils/testing'; +import { celTestState } from '../../../__jest__/fixtures/cel'; +import type { CelInputState } from '../../types'; +import { handleBuildProgram } from './build_program'; + +const model = new FakeLLM({ + response: 'my_cel_program', +}) as unknown as ActionsClientChatOpenAI | ActionsClientSimpleChatModel; + +const state: CelInputState = celTestState; + +describe('Testing cel handler', () => { + it('handleBuildProgram()', async () => { + const response = await handleBuildProgram({ state, model }); + expect(response.currentProgram).toStrictEqual('my_cel_program'); + expect(response.lastExecutedChain).toBe('buildCelProgram'); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/build_program.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/build_program.ts new file mode 100644 index 0000000000000..8ebd24cd93326 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/build_program.ts @@ -0,0 +1,33 @@ +/* + * 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 { StringOutputParser } from '@langchain/core/output_parsers'; +import { CelInputState } from '../../types'; +import { EX_ANSWER_PROGRAM, SAMPLE_CEL_PROGRAMS } from './constants'; +import { CEL_BASE_PROGRAM_PROMPT } from './prompts'; +import { CelInputNodeParams } from './types'; + +export async function handleBuildProgram({ + state, + model, +}: CelInputNodeParams): Promise> { + const outputParser = new StringOutputParser(); + const celProgramGraph = CEL_BASE_PROGRAM_PROMPT.pipe(model).pipe(outputParser); + + const program = await celProgramGraph.invoke({ + data_stream_name: state.dataStreamName, + example_cel_programs: SAMPLE_CEL_PROGRAMS, + open_api_spec: state.apiDefinition, + api_query_summary: state.apiQuerySummary, + ex_answer: EX_ANSWER_PROGRAM, + }); + + return { + currentProgram: program.trim(), + lastExecutedChain: 'buildCelProgram', + }; +} diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/constants.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/constants.ts new file mode 100644 index 0000000000000..3119346091e4e --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/constants.ts @@ -0,0 +1,298 @@ +/* + * 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. + */ +export const EX_ANSWER_PROGRAM = `( + !has(state.cursor) || !has(state.cursor.scroll_id) ? + post(state.url+"?scroll=5m", "", "") + : + post( + state.url+"/scroll?"+{"scroll_id": [state.cursor.scroll_id]}.format_query(), + "application/json", + {"scroll": state.scroll}.encode_json() + ) + ).as(resp, bytes(resp.Body).decode_json().as(body, { + "events": body.hits.hits, + "cursor": {"scroll_id": body._scroll_id}, +}))`; + +export const EX_ANSWER_STATE = `["config1", "config2"]`; + +export const EX_ANSWER_CONFIG = [ + { + name: 'config1', + default: 50, + redact: false, + }, + { + name: 'config2', + default: '', + redact: false, + }, + { + name: 'config3', + default: 'event', + redact: true, + }, +]; + +export const SAMPLE_CEL_PROGRAMS = [ + `request("GET", (state.url+"&startingAfter="+state.cursor.checkpoint)).with({ + "Header":{ + "Content-Type": ["application/json"], + "Authorization": [state.token], + }, + }).do_request().as(resp, resp.StatusCode == 200 ? + bytes(resp.Body).decode_json().as(body,{ + "events": body.map(e, { "message": e.encode_json(), }), + "cursor": { + "initial_checkpoint": state.cursor.initial_checkpoint, + "checkpoint": body.size() > 0 ? body[body.size()-1].?serial : state.cursor.initial_checkpoint, + }, + "want_more": body.size() != 0, + "token": state.token, + }) + : + { + "events": { + "error": { + "code": string(resp.StatusCode), + "id": string(resp.Status), + "message": string(resp.Body) + }, + }, + "want_more": false, + } + )`, + `request("GET", state.url).with({ + "Header":{ + "Content-Type": ["application/json"], + } + }).as(req, req.do_request().as(resp, + bytes(resp.Body).decode_json().as(body, { + "events": body.payloads.map(payload, { + "message": payload.encode_json() + }), + "url": state.url + }) + ))`, + `( + !state.want_more ? + request("GET", state.url + "/iocs/combined/indicator/v1?sort=modified_on&offset=0&limit=" + string(state.batch_size) + '&filter=modified_on:>"' + ( + has(state.cursor) && has(state.cursor.last_timestamp) && state.cursor.last_timestamp != null ? + state.cursor.last_timestamp + '"' + : + (now - duration(state.initial_interval)).format(time_layout.RFC3339) + '"' + )) + : + request("GET", state.url + "/iocs/combined/indicator/v1?sort=modified_on&limit=" + string(state.batch_size) + "&offset=" + string(state.offset) + '&filter=modified_on:>"' + ( + has(state.cursor) && has(state.cursor.first_timestamp) && state.cursor.first_timestamp != null ? + state.cursor.first_timestamp + '"' + : + '"' + )) + ).do_request().as(resp, + resp.StatusCode == 200 ? + bytes(resp.Body).decode_json().as(body, { + "events": body.resources.map(e, { + "message": e.encode_json(), + }), + "want_more": has(body.meta.pagination) && (int(state.offset) + body.resources.size()) < body.meta.pagination.total, + "offset": has(body.meta.pagination) && ((int(state.offset) + body.resources.size()) < body.meta.pagination.total) ? + int(state.offset) + int(body.resources.size()) + : + 0, + "url": state.url, + "batch_size": state.batch_size, + "initial_interval": state.initial_interval, + "cursor": { + "last_timestamp": ( + has(body.resources) && body.resources.size() > 0 ? + ( + has(state.cursor) && has(state.cursor.last_timestamp) && body.resources.map(e, e.modified_on).max() < state.cursor.last_timestamp ? + state.cursor.last_timestamp + : + body.resources.map(e, e.modified_on).max() + ) + : + ( + has(state.cursor) && has(state.cursor.last_timestamp) ? + state.cursor.last_timestamp + : + null + ) + ), + "first_timestamp": ( + has(state.cursor) && has(state.cursor.first_timestamp) && state.cursor.first_timestamp != null ? + ( + state.want_more ? + state.cursor.first_timestamp + : + state.cursor.last_timestamp + ) + : + (now - duration(state.initial_interval)).format(time_layout.RFC3339) + ), + }, + }) + : + { + "events": { + "error": { + "code": string(resp.StatusCode), + "id": string(resp.Status), + "message": string(resp.Body) + }, + }, + "want_more": false, + "offset": 0, + "url": state.url, + "batch_size": state.batch_size, + "initial_interval": state.initial_interval, + } + ) + `, + ` + state.with({ + "Header": { + "Accept": ["application/vnd.api+json"], + "Authorization": ["Token " + state.api_token], + } +}.as(auth_header, + ( + has(state.work_list) ? + state.work_list + : (state.audit_id == "*" && state.end_point_type == "/rest/orgs/") ? + get_request( + state.url.trim_right("/") + "/rest/orgs?" + { + "version": [state.version], + }.format_query() + ).with(auth_header).do_request().as(resp, resp.StatusCode != 200 ? [] : + resp.Body.decode_json().data.map(org, { + "id": org.id, + ?"last_created": state.?cursor[org.id].last_created + }) + ) + : has(state.?cursor.last_created) ? + [{ + "id": state.audit_id, + "last_created": state.cursor.last_created, + }] + : has(state.cursor) && state.end_point_type == "/rest/orgs/" ? + state.cursor.map(audit_id, state.cursor[audit_id].with({"id": audit_id})) + : + [{"id": state.audit_id}] + ).map(item, + get_request( + debug("GET", state.url.trim_right("/") + item.?next.orValue( + state.end_point_type + item.id + "/audit_logs/search?" + debug("QUERY",{ + "version": [state.version], + "sort_order": ['ASC'], + ?"from": has(item.last_created) ? + optional.of([string(timestamp(item.last_created)+duration("1us"))]) + : has(state.lookback) ? + optional.of([string(now-duration(state.lookback))]) + : + optional.none(), + ?"size": has(state.size) ? + optional.of([string(int(state.size))]) + : + optional.none(), + ?"user_id": has(state.user_id) ? + optional.of([state.user_id]) + : + optional.none(), + ?"project_id": has(state.project_id) ? + optional.of([state.project_id]) + : + optional.none(), + ?"events": state.?events_filter, + }).format_query() + )) + ).with(auth_header).do_request().as(resp, resp.StatusCode != 200 ? + { + "id": item.id, + "events": [{ + "error": { + "code": string(resp.StatusCode), + "id": string(resp.Status), + "message": size(resp.Body) != 0 ? + string(resp.Body) + : + string(resp.Status) + ' (' + string(resp.StatusCode) + ')', + } + }], + "want_more": false, + } + : + bytes(resp.Body).decode_json().as(body, !has(body.?data.items) ? + { + "id": item.id, + "events":[], + "want_more": false, + } + : + { + "id": item.id, + "events": body.data.items.map(item, { + "message": item.encode_json() + }), + "cursor": { + "id": item.id, + ?"next": body.?links.next, + "last_created": body.data.items.map(item, + has(item.created), timestamp(item.created) + ).as(times, size(times) == 0 ? item.?last_created.orValue(now) : times.max()), + }, + "want_more": has(body.?links.next), + } + ) + ) + ).as(result, { + "cursor": state.?cursor.orValue({}).drop("last_created").with(zip( + result.map(r, has(r.?cursor.id), r.cursor.id), + result.map(r, has(r.?cursor.id), r.cursor.drop(["id","next"])) + )), + "work_list": result.map(r, has(r.?cursor.next), { + "id": r.cursor.id, + "next": r.cursor.next, + }), + "events": result.map(r, r.events).flatten(), + "want_more": result.exists(r, r.want_more), + }) +))`, + `( + has(state.hostlist) && size(state.hostlist) > 0 ? + state + : + state.with(request("GET", state.url + "/api/atlas/v2/groups/" + state.group_id + "/processes?pageNum=" + string(state.page_num) + "&itemsPerPage=state.page_size").with({ + "Header": { + "Accept": ["application/vnd.atlas." + string(now.getFullYear()) + "-01-01+json"] + } + }).do_request().as(resp, bytes(resp.Body).decode_json().as(body, { + "hostlist": body.results.map(e, state.url + "/api/atlas/v2/groups/" + state.group_id + "/processes/" + e.id + state.query), + "next": 0, + "page_num": body.links.exists_one(res, res.rel == "next") ? int(state.page_num)+1 : 1 + }))) + ).as(state, state.next >= size(state.hostlist) ? {} : + request("GET", string(state.hostlist[state.next])).with({ + "Header": { + "Accept": ["application/vnd.atlas." + string(now.getFullYear()) + "-01-01+json"] + } + }).do_request().as(res, { + "events": bytes(res.Body).decode_json().as(f, f.with({"response": zip( + //Combining measurement names and actual values of measurement to generate \`key : value\` pairs. + f.measurements.map(m, m.name), + f.measurements.map(m, m.dataPoints.map(d, d.value).as(v, size(v) == 0 ? null : v[0])) + )}).drop(["measurements", "links"])), + "hostlist": (int(state.next)+1) < size(state.hostlist) ? state.hostlist : [], + "next": (int(state.next)+1) < size(state.hostlist) ? (int(state.next)+1) : 0, + "want_more": (int(state.next)+1) < size(state.hostlist) || state.page_num != 1, + "page_num": state.page_num, + "group_id": state.group_id, + "query": state.query, + }) + )`, +]; diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/graph.test.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/graph.test.ts new file mode 100644 index 0000000000000..7af5e9c83c3fc --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/graph.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { FakeLLM } from '@langchain/core/utils/testing'; +import { getCelGraph } from './graph'; +import { + celProgramMock, + celQuerySummaryMockedResponse, + celStateVarsMockedResponse, + celExpectedResults, + celStateSettings, + celRedact, +} from '../../../__jest__/fixtures/cel'; +import { mockedRequestWithApiDefinition } from '../../../__jest__/fixtures'; +import { handleSummarizeQuery } from './summarize_query'; +import { handleBuildProgram } from './build_program'; +import { handleGetStateVariables } from './retrieve_state_vars'; +import { handleGetStateDetails } from './retrieve_state_details'; + +import { + ActionsClientChatOpenAI, + ActionsClientSimpleChatModel, +} from '@kbn/langchain/server/language_models'; + +const model = new FakeLLM({ + response: "I'll callback later.", +}) as unknown as ActionsClientChatOpenAI | ActionsClientSimpleChatModel; + +jest.mock('./summarize_query'); +jest.mock('./build_program'); +jest.mock('./retrieve_state_vars'); +jest.mock('./retrieve_state_details'); + +describe('CelGraph', () => { + beforeEach(() => { + // Mocked responses for each node that requires an LLM API call/response. + const mockInvokeCelSummarizeQuery = jest.fn().mockResolvedValue(celQuerySummaryMockedResponse); + const mockInvokeCelProgram = jest.fn().mockResolvedValue(celProgramMock); + const mockInvokeCelStateVars = jest.fn().mockResolvedValue(celStateVarsMockedResponse); + const mockInvokeCelStateSettings = jest.fn().mockResolvedValue(celStateSettings); + const mockInvokeCelRedactVars = jest.fn().mockResolvedValue(celRedact); + + // Returns the initial query summary for the api, to trigger the next step. + (handleSummarizeQuery as jest.Mock).mockImplementation(async () => ({ + apiQuerySummary: await mockInvokeCelSummarizeQuery(), + lastExecutedChain: 'summarizeQuery', + })); + + // Returns the CEL program, to trigger the next step. + (handleBuildProgram as jest.Mock).mockImplementation(async () => ({ + currentProgram: await mockInvokeCelProgram(), + lastExecutedChain: 'buildProgram', + })); + + // Returns the state variable names for the CEL program, to trigger the next step. + (handleGetStateVariables as jest.Mock).mockImplementation(async () => ({ + stateVarNames: await mockInvokeCelStateVars(), + lastExecutedChain: 'getStateVars', + })); + + // Returns the state details for the CEL program. + (handleGetStateDetails as jest.Mock).mockImplementation(async () => ({ + stateSettings: await mockInvokeCelStateSettings(), + redactVars: await mockInvokeCelRedactVars(), + lastExecutedChain: 'getStateDetails', + })); + }); + + it('Ensures that the graph compiles', async () => { + // When getCelGraph runs, langgraph compiles the graph it will error if the graph has any issues. + // Common issues for example detecting a node has no next step, or there is a infinite loop between them. + try { + await getCelGraph({ model }); + } catch (error) { + throw Error(`getCelGraph threw an error: ${error}`); + } + }); + + it('Runs the whole graph, with mocked outputs from the LLM.', async () => { + const celGraph = await getCelGraph({ model }); + let response; + try { + response = await celGraph.invoke(mockedRequestWithApiDefinition); + } catch (error) { + throw Error(`getCelGraph threw an error: ${error}`); + } + + expect(handleSummarizeQuery).toHaveBeenCalled(); + expect(handleBuildProgram).toHaveBeenCalled(); + expect(handleGetStateVariables).toHaveBeenCalled(); + expect(handleGetStateDetails).toHaveBeenCalled(); + + expect(response.results).toStrictEqual(celExpectedResults); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/graph.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/graph.ts new file mode 100644 index 0000000000000..a8f2e0521c788 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/graph.ts @@ -0,0 +1,109 @@ +/* + * 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 type { StateGraphArgs } from '@langchain/langgraph'; +import { END, START, StateGraph } from '@langchain/langgraph'; +import type { CelInputState } from '../../types'; +import { handleBuildProgram } from './build_program'; +import { handleGetStateDetails } from './retrieve_state_details'; +import { handleGetStateVariables } from './retrieve_state_vars'; +import { handleSummarizeQuery } from './summarize_query'; +import { CelInputBaseNodeParams, CelInputGraphParams } from './types'; + +const graphState: StateGraphArgs['channels'] = { + lastExecutedChain: { + value: (x: string, y?: string) => y ?? x, + default: () => '', + }, + dataStreamName: { + value: (x: string, y?: string) => y ?? x, + default: () => '', + }, + finalized: { + value: (x: boolean, y?: boolean) => y ?? x, + default: () => false, + }, + results: { + value: (x: object, y?: object) => y ?? x, + default: () => ({}), + }, + apiDefinition: { + value: (x: string, y?: string) => y ?? x, + default: () => '', + }, + apiQuerySummary: { + value: (x: string, y?: string) => y ?? x, + default: () => '', + }, + exampleCelPrograms: { + value: (x: string[], y?: string[]) => y ?? x, + default: () => [], + }, + currentProgram: { + value: (x: string, y?: string) => y ?? x, + default: () => '', + }, + stateVarNames: { + value: (x: string[], y?: string[]) => y ?? x, + default: () => [], + }, + stateSettings: { + value: (x: object, y?: object) => y ?? x, + default: () => ({}), + }, + redactVars: { + value: (x: string[], y?: string[]) => y ?? x, + default: () => [], + }, +}; + +function modelInput({ state }: CelInputBaseNodeParams): Partial { + return { + finalized: false, + lastExecutedChain: 'modelInput', + apiDefinition: state.apiDefinition, + dataStreamName: state.dataStreamName, + }; +} + +function modelOutput({ state }: CelInputBaseNodeParams): Partial { + return { + finalized: true, + lastExecutedChain: 'modelOutput', + results: { + program: state.currentProgram, + stateSettings: state.stateSettings, + redactVars: state.redactVars, + }, + }; +} + +export async function getCelGraph({ model }: CelInputGraphParams) { + const workflow = new StateGraph({ channels: graphState }) + .addNode('modelInput', (state: CelInputState) => modelInput({ state })) + .addNode('handleSummarizeQuery', (state: CelInputState) => + handleSummarizeQuery({ state, model }) + ) + .addNode('handleBuildProgram', (state: CelInputState) => handleBuildProgram({ state, model })) + .addNode('handleGetStateVariables', (state: CelInputState) => + handleGetStateVariables({ state, model }) + ) + .addNode('handleGetStateDetails', (state: CelInputState) => + handleGetStateDetails({ state, model }) + ) + .addNode('modelOutput', (state: CelInputState) => modelOutput({ state })) + .addEdge(START, 'modelInput') + .addEdge('modelOutput', END) + .addEdge('modelInput', 'handleSummarizeQuery') + .addEdge('handleSummarizeQuery', 'handleBuildProgram') + .addEdge('handleBuildProgram', 'handleGetStateVariables') + .addEdge('handleGetStateVariables', 'handleGetStateDetails') + .addEdge('handleGetStateDetails', 'modelOutput'); + + const compiledCelGraph = workflow.compile().withConfig({ runName: 'CEL' }); + return compiledCelGraph; +} diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/index.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/index.ts new file mode 100644 index 0000000000000..c30f5e32e0fe5 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ +export { getCelGraph } from './graph'; diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/prompts.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/prompts.ts new file mode 100644 index 0000000000000..71cab16ae2c88 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/prompts.ts @@ -0,0 +1,156 @@ +/* + * 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 { ChatPromptTemplate } from '@langchain/core/prompts'; + +export const CEL_QUERY_SUMMARY_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are a helpful, expert assistant in REST APIs and OpenAPI specifications. +Here is some context for you to reference for your task, read it carefully as you will get questions about it later: + + +{open_api_spec} + +`, + ], + [ + 'human', + `For the {data_stream_name} endpoint and provided OpenAPI specification, please describe which query parameters you would use so that all events are covered in a chronological manner. + +You ALWAYS follow these guidelines when writing your response: + + - Prioritize bulk api routes over more specialized routes. + + +Please respond with a concise text answer, and a sample URL path.`, + ], + ['ai', `Please find the query summary text below:`], +]); + +export const CEL_BASE_PROGRAM_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are a helpful, expert assistant in building Elastic filebeat input configurations utilizing the Common Expression Language (CEL) input type. +Here is some context for you to reference your task, review it carefully as you will get questions about it later: + + +{open_api_spec} + + +{example_cel_programs} + +`, + ], + [ + 'human', + `Please build only the program section of the CEL filebeat input configuration for the the datastream {data_stream_name} such that the program is able to iterate through and ingest pages of events into Elasticsearch. +Utilize the following paging summary details and sample URL for implementing paging when building your output. + + + +{api_query_summary} + + + +Each of the following criteria must be addressed in final configuration output: +- The REST verb must be specified. +- The request URL must include the 'state.url'. +- The request URL must include the API path. +- The request URL must include all query parameters from the paging summary using a 'format_query' function. Remember to utilize the state variables inside brackets when building the function and be sure to cast any numeric variables to string using 'string(variable)'. +- All request URL parameters must utilize state variables. +- The request URL must include a '?' at the end of API path string. +- Always use the casing specified by the API spec when building the API path and query parameters. +- There must not be configuration for authentication or authorization. +- There must be configuration of any required headers. +- There must be configuration for parsing the events returned from the API mapped to the 'message' field and encoded in JSON. +- There must be configuration in the API response handling for 'want_more' based on the paging token. +- There must be configuration for error handling. This includes setting the 'want_more' flag to false. +- All state variables must use snake casing. +- The page tokens must be updated the corresponding state variable(s). + +You ALWAYS follow these guidelines when writing your response: + +- You must never include any code for writing data to the API. +- You must respond only with the code block containing the program formatted like human-readable C code. See example response below. +- You must use 2 spaces for tab size. +- Do not enclose the final output in backticks, only return the codeblock and nothing else. + + +Example response: +A: Please find the CEL program below: +{ex_answer}`, + ], + ['ai', `Please find the CEL program below:`], +]); + +export const CEL_STATE_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are a helpful, expert assistant in building Elastic filebeat input configurations utilizing the Common Expression Language (CEL) input type. +Here is some context for you to reference for your task, read it carefully as you will get questions about it later: + + + +{cel_program} + +`, + ], + [ + 'human', + `Looking at the CEL program provided in the context, please return a string array of the state variables + +You ALWAYS follow these guidelines when writing your response: + + + - Respond with the array only. + + + + A: Please find the JSON list below: + \`\`\` +{ex_answer} + \`\`\` + `, + ], + ['ai', `Please find the JSON list below:`], +]); + +export const CEL_CONFIG_DETAILS_PROMPT = ChatPromptTemplate.fromMessages([ + [ + 'system', + `You are a helpful, expert assistant on OpenAPI specifications and building Elastic integrations. +Here is some context for you to reference for your task, read it carefully as you will get questions about it later: + + + +{open_api_spec} + +`, + ], + [ + 'human', + `For the identified state variables {state_variables}, iterate through each variable (name) and identify a default value (default) and a boolean representing if it should be redacted(redact). Return all of this information in a JSON object like the sample below. + +You ALWAYS follow these guidelines when writing your response: + + + - Page sizing default should always be non-zero. + - Redact anything that could possibly contain PII, tokens or keys, or expose any sensitive information in the logs. + - You must use the variable names in parentheses when building the return object. Each item in the response must contain the fields: name, redact and default. + - Do not respond with anything except the JSON object enclosed with 3 backticks (\`), see example response below. + +Example response format: + + A: Please find the JSON object below: + \`\`\`json +{ex_answer} + \`\`\` + +`, + ], + ['ai', `Please find the JSON object below:`], +]); diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.test.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.test.ts new file mode 100644 index 0000000000000..9523ac708fbd5 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { + ActionsClientChatOpenAI, + ActionsClientSimpleChatModel, +} from '@kbn/langchain/server/language_models'; +import { FakeLLM } from '@langchain/core/utils/testing'; +import { + celExpectedResults, + celStateDetailsMockedResponse, + celTestState, +} from '../../../__jest__/fixtures/cel'; +import type { CelInputState } from '../../types'; +import { handleGetStateDetails } from './retrieve_state_details'; + +const model = new FakeLLM({ + response: JSON.stringify(celStateDetailsMockedResponse, null, 2), +}) as unknown as ActionsClientChatOpenAI | ActionsClientSimpleChatModel; + +const state: CelInputState = celTestState; + +describe('Testing cel handler', () => { + it('handleGetStateDetails()', async () => { + const response = await handleGetStateDetails({ state, model }); + expect(response.stateSettings).toStrictEqual(celExpectedResults.stateSettings); + expect(response.redactVars).toStrictEqual(celExpectedResults.redactVars); + expect(response.lastExecutedChain).toBe('getStateDetails'); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.ts new file mode 100644 index 0000000000000..884767ee56fcb --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_details.ts @@ -0,0 +1,36 @@ +/* + * 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 { JsonOutputParser } from '@langchain/core/output_parsers'; +import { CelInputState } from '../../types'; +import { EX_ANSWER_CONFIG } from './constants'; +import { CEL_CONFIG_DETAILS_PROMPT } from './prompts'; +import { CelInputNodeParams, CelInputStateDetails } from './types'; +import { getRedactVariables, getStateVarsAndDefaultValues } from './util'; + +export async function handleGetStateDetails({ + state, + model, +}: CelInputNodeParams): Promise> { + const outputParser = new JsonOutputParser(); + const celConfigGraph = CEL_CONFIG_DETAILS_PROMPT.pipe(model).pipe(outputParser); + + const stateDetails = (await celConfigGraph.invoke({ + state_variables: state.stateVarNames, + open_api_spec: state.apiDefinition, + ex_answer: EX_ANSWER_CONFIG, + })) as CelInputStateDetails[]; + + const stateSettings = getStateVarsAndDefaultValues(stateDetails); + const redactVars = getRedactVariables(stateDetails); + + return { + stateSettings, + redactVars, + lastExecutedChain: 'getStateDetails', + }; +} diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.test.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.test.ts new file mode 100644 index 0000000000000..03165ae8c8692 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { + ActionsClientChatOpenAI, + ActionsClientSimpleChatModel, +} from '@kbn/langchain/server/language_models'; +import { FakeLLM } from '@langchain/core/utils/testing'; +import { celTestState } from '../../../__jest__/fixtures/cel'; +import type { CelInputState } from '../../types'; +import { handleGetStateVariables } from './retrieve_state_vars'; + +const model = new FakeLLM({ + response: '[ "my_state_var", "url" ]', +}) as unknown as ActionsClientChatOpenAI | ActionsClientSimpleChatModel; + +const state: CelInputState = celTestState; + +describe('Testing cel handler', () => { + it('handleGetStateVariables()', async () => { + const response = await handleGetStateVariables({ state, model }); + expect(response.stateVarNames).toStrictEqual(['my_state_var']); + expect(response.lastExecutedChain).toBe('getStateVars'); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.ts new file mode 100644 index 0000000000000..a5c252778ace5 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/retrieve_state_vars.ts @@ -0,0 +1,33 @@ +/* + * 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 { JsonOutputParser } from '@langchain/core/output_parsers'; +import { CelInputState } from '../../types'; +import { EX_ANSWER_STATE } from './constants'; +import { CEL_STATE_PROMPT } from './prompts'; +import { CelInputNodeParams } from './types'; + +export async function handleGetStateVariables({ + state, + model, +}: CelInputNodeParams): Promise> { + const outputParser = new JsonOutputParser(); + const celStateGraph = CEL_STATE_PROMPT.pipe(model).pipe(outputParser); + + const celState = await celStateGraph.invoke({ + cel_program: state.currentProgram, + ex_answer: EX_ANSWER_STATE, + }); + + // Return all state vars besides the URL as it gets included automatically + const filteredState = celState.filter((stateVar: string) => stateVar !== 'url'); + + return { + stateVarNames: filteredState, + lastExecutedChain: 'getStateVars', + }; +} diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.test.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.test.ts new file mode 100644 index 0000000000000..6dcb28813df70 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { + ActionsClientChatOpenAI, + ActionsClientSimpleChatModel, +} from '@kbn/langchain/server/language_models'; +import { FakeLLM } from '@langchain/core/utils/testing'; +import { celTestState } from '../../../__jest__/fixtures/cel'; +import type { CelInputState } from '../../types'; +import { handleSummarizeQuery } from './summarize_query'; + +const model = new FakeLLM({ + response: 'my_api_query_summary', +}) as unknown as ActionsClientChatOpenAI | ActionsClientSimpleChatModel; + +const state: CelInputState = celTestState; + +describe('Testing cel handler', () => { + it('handleSummarizeQuery()', async () => { + const response = await handleSummarizeQuery({ state, model }); + expect(response.apiQuerySummary).toStrictEqual('my_api_query_summary'); + expect(response.lastExecutedChain).toBe('summarizeQuery'); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.ts new file mode 100644 index 0000000000000..dc5e7e32e8697 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/summarize_query.ts @@ -0,0 +1,29 @@ +/* + * 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 { StringOutputParser } from '@langchain/core/output_parsers'; +import { CelInputState } from '../../types'; +import { CEL_QUERY_SUMMARY_PROMPT } from './prompts'; +import { CelInputNodeParams } from './types'; + +export async function handleSummarizeQuery({ + state, + model, +}: CelInputNodeParams): Promise> { + const outputParser = new StringOutputParser(); + const celSummarizeGraph = CEL_QUERY_SUMMARY_PROMPT.pipe(model).pipe(outputParser); + + const apiQuerySummary = await celSummarizeGraph.invoke({ + data_stream_name: state.dataStreamName, + open_api_spec: state.apiDefinition, + }); + + return { + apiQuerySummary, + lastExecutedChain: 'summarizeQuery', + }; +} diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/types.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/types.ts new file mode 100644 index 0000000000000..c0a855754a8b6 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/types.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 { CelInputState, ChatModels } from '../../types'; + +export interface CelInputBaseNodeParams { + state: CelInputState; +} + +export interface CelInputNodeParams extends CelInputBaseNodeParams { + model: ChatModels; +} + +export interface CelInputGraphParams { + model: ChatModels; +} + +export interface CelInputStateDetails { + name: string; + default: string | number | boolean; + redact: boolean; +} diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/util.test.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/util.test.ts new file mode 100644 index 0000000000000..6dfeea7952909 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/util.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { getRedactVariables, getStateVarsAndDefaultValues } from './util'; +import { + celStateDetailsMockedResponse, + celStateSettings, + celRedact, +} from '../../../__jest__/fixtures/cel'; + +describe('getCelInputDetails', () => { + beforeEach(async () => { + jest.clearAllMocks(); + }); + + it('getRedactVariables', () => { + const result = getRedactVariables(celStateDetailsMockedResponse); + expect(result).toStrictEqual(celRedact); + }); + + it('getStateVarsAndDefaultValues', () => { + const result = getStateVarsAndDefaultValues(celStateDetailsMockedResponse); + expect(result).toStrictEqual(celStateSettings); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/graphs/cel/util.ts b/x-pack/plugins/integration_assistant/server/graphs/cel/util.ts new file mode 100644 index 0000000000000..e149e55b47907 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/graphs/cel/util.ts @@ -0,0 +1,32 @@ +/* + * 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 { CelInputStateDetails } from './types'; + +/** + * Gets a list of variables that require redaction from agent logs for the CEL input. + */ +export function getRedactVariables(stateDetails: CelInputStateDetails[]): string[] { + const redact = [] as string[]; + for (const cVar of stateDetails) { + if (cVar.redact) { + redact.push(cVar.name); + } + } + return redact; +} + +/** + * Gets an object containing state variables and their corresponding default values. + */ +export function getStateVarsAndDefaultValues(stateDetails: CelInputStateDetails[]): object { + const defaultStateVarSettings: Record = {}; + for (const stateVar of stateDetails) { + defaultStateVarSettings[stateVar.name] = stateVar.default; + } + return defaultStateVarSettings; +} diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/constants.ts b/x-pack/plugins/integration_assistant/server/integration_builder/constants.ts new file mode 100644 index 0000000000000..7c70cfae756d9 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/integration_builder/constants.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export const DEFAULT_CEL_PROGRAM = `# // Fetch the agent's public IP every minute and note when the last request was made. +# // It does not use the Resource URL configuration value. +# bytes(get("https://api.ipify.org/?format=json").Body).as(body, { +# "events": [body.decode_json().with({ +# "last_requested_at": has(state.cursor) && has(state.cursor.last_requested_at) ? +# state.cursor.last_requested_at +# : +# now +# })], +# "cursor": {"last_requested_at": now} +# })`; diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts index 0a269fa07a1c8..ba6959da00b2a 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts @@ -22,6 +22,10 @@ jest.mock('../util', () => ({ generateFields: jest.fn(), })); +beforeEach(async () => { + jest.clearAllMocks(); +}); + describe('createDataStream', () => { const packageName = 'package'; const dataStreamPath = 'path'; @@ -55,6 +59,22 @@ describe('createDataStream', () => { samplesFormat: { name: 'ndjson', multiline: false }, }; + const celDataStream: DataStream = { + name: firstDatastreamName, + title: 'Datastream_1', + description: 'Datastream_1 description', + inputTypes: ['cel'] as InputType[], + docs: firstDataStreamDocs, + rawSamples: [samples], + pipeline: firstDataStreamPipeline, + samplesFormat: { name: 'ndjson', multiline: false }, + celInput: { + program: 'line1\nline2', + stateSettings: { setting1: 100, setting2: '' }, + redactVars: ['setting2'], + }, + }; + it('Should create expected directories and files', async () => { createDataStream(packageName, dataStreamPath, firstDataStream); @@ -93,4 +113,23 @@ describe('createDataStream', () => { }); }); }); + + it('Should populate expected CEL fields', async () => { + createDataStream(packageName, dataStreamPath, celDataStream); + + const expectedMappedValues = { + data_stream_title: celDataStream.title, + data_stream_description: celDataStream.description, + package_name: packageName, + data_stream_name: firstDatastreamName, + multiline_ndjson: celDataStream.samplesFormat.multiline, + program: celDataStream.celInput?.program.split('\n'), + state: celDataStream.celInput?.stateSettings, + redact: celDataStream.celInput?.redactVars, + }; + + // // Manifest files + expect(createSync).toHaveBeenCalledWith(`${dataStreamPath}/manifest.yml`, undefined); + expect(render).toHaveBeenCalledWith(`cel_manifest.yml.njk`, expectedMappedValues); + }); }); diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts index 6338474fae0ed..8dac7f03c8086 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts @@ -9,6 +9,7 @@ import nunjucks from 'nunjucks'; import { join as joinPath } from 'path'; import { safeLoad } from 'js-yaml'; import type { DataStream } from '../../common'; +import { DEFAULT_CEL_PROGRAM } from './constants'; import { copySync, createSync, ensureDirSync, listDirSync, readSync } from '../util'; import { Field } from '../util/samples'; @@ -30,13 +31,33 @@ export function createDataStream( const dataStreams: string[] = []; for (const inputType of dataStream.inputTypes) { - const mappedValues = { + let mappedValues = { data_stream_title: title, data_stream_description: description, package_name: packageName, data_stream_name: dataStreamName, multiline_ndjson: useMultilineNDJSON, - }; + } as object; + + if (inputType === 'cel') { + if (dataStream.celInput != null) { + // Map the generated CEL config items into the template + const cel = dataStream.celInput; + mappedValues = { + ...mappedValues, + // Ready the program for printing with correct indentation + program: cel.program.split('\n'), + state: cel.stateSettings, + redact: cel.redactVars, + }; + } else { + mappedValues = { + ...mappedValues, + program: DEFAULT_CEL_PROGRAM.split('\n'), + }; + } + } + const dataStreamManifest = nunjucks.render( `${inputType.replaceAll('-', '_')}_manifest.yml.njk`, mappedValues diff --git a/x-pack/plugins/integration_assistant/server/plugin.ts b/x-pack/plugins/integration_assistant/server/plugin.ts index 64989d23e7dd8..bf972f04a61a5 100644 --- a/x-pack/plugins/integration_assistant/server/plugin.ts +++ b/x-pack/plugins/integration_assistant/server/plugin.ts @@ -21,6 +21,8 @@ import type { IntegrationAssistantPluginStart, IntegrationAssistantPluginStartDependencies, } from './types'; +import { parseExperimentalConfigValue } from '../common/experimental_features'; +import { IntegrationAssistantConfigType } from './config'; export type IntegrationAssistantRouteHandlerContext = CustomRequestHandlerContext<{ integrationAssistant: { @@ -36,11 +38,13 @@ export class IntegrationAssistantPlugin implements Plugin { private readonly logger: Logger; + private readonly config: IntegrationAssistantConfigType; private isAvailable: boolean; private hasLicense: boolean; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); + this.config = initializerContext.config.get(); this.isAvailable = true; this.hasLicense = false; } @@ -59,9 +63,11 @@ export class IntegrationAssistantPlugin logger: this.logger, })); const router = core.http.createRouter(); + const experimentalFeatures = parseExperimentalConfigValue(this.config.enableExperimental ?? []); + this.logger.debug('integrationAssistant api: Setup'); - registerRoutes(router); + registerRoutes(router, experimentalFeatures); return { setIsAvailable: (isAvailable: boolean) => { diff --git a/x-pack/plugins/integration_assistant/server/routes/cel_route.test.ts b/x-pack/plugins/integration_assistant/server/routes/cel_route.test.ts new file mode 100644 index 0000000000000..be435aa9866bb --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/routes/cel_route.test.ts @@ -0,0 +1,75 @@ +/* + * 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 { serverMock } from '../__mocks__/mock_server'; +import { requestMock } from '../__mocks__/request'; +import { requestContextMock } from '../__mocks__/request_context'; +import { CEL_INPUT_GRAPH_PATH } from '../../common'; +import { registerCelInputRoutes } from './cel_routes'; + +const mockResult = jest.fn().mockResolvedValue({ + results: { + program: '', + stateSettings: {}, + redactVars: [], + }, +}); + +jest.mock('../graphs/cel', () => { + return { + getCelGraph: jest.fn().mockResolvedValue({ + invoke: () => mockResult(), + }), + }; +}); + +describe('registerCelInputRoute', () => { + let server: ReturnType; + let { context } = requestContextMock.createTools(); + + const req = requestMock.create({ + method: 'post', + path: CEL_INPUT_GRAPH_PATH, + body: { + connectorId: 'testConnector', + dataStreamName: 'testStream', + apiDefinition: 'testApiDefinitionFileContents', + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + server = serverMock.create(); + ({ context } = requestContextMock.createTools()); + registerCelInputRoutes(server.router); + }); + + it('Runs route and gets CelInputResponse', async () => { + const response = await server.inject(req, requestContextMock.convertContext(context)); + expect(response.body).toEqual({ + results: { program: '', stateSettings: {}, redactVars: [] }, + }); + expect(response.status).toEqual(200); + }); + + it('Runs route with badRequest', async () => { + mockResult.mockResolvedValueOnce({}); + const response = await server.inject(req, requestContextMock.convertContext(context)); + expect(response.status).toEqual(400); + }); + + describe('when the integration assistant is not available', () => { + beforeEach(() => { + context.integrationAssistant.isAvailable.mockReturnValue(false); + }); + + it('returns a 404', async () => { + const response = await server.inject(req, requestContextMock.convertContext(context)); + expect(response.status).toEqual(404); + }); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/routes/cel_routes.ts b/x-pack/plugins/integration_assistant/server/routes/cel_routes.ts new file mode 100644 index 0000000000000..ecf012a88cfe5 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/routes/cel_routes.ts @@ -0,0 +1,92 @@ +/* + * 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 type { IKibanaResponse, IRouter } from '@kbn/core/server'; +import { getRequestAbortedSignal } from '@kbn/data-plugin/server'; +import { APMTracer } from '@kbn/langchain/server/tracers/apm'; +import { getLangSmithTracer } from '@kbn/langchain/server/tracers/langsmith'; +import { CEL_INPUT_GRAPH_PATH, CelInputRequestBody, CelInputResponse } from '../../common'; +import { ROUTE_HANDLER_TIMEOUT } from '../constants'; +import { getCelGraph } from '../graphs/cel'; +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'; + +export function registerCelInputRoutes(router: IRouter) { + router.versioned + .post({ + path: CEL_INPUT_GRAPH_PATH, + access: 'internal', + options: { + timeout: { + idleSocket: ROUTE_HANDLER_TIMEOUT, + }, + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: buildRouteValidationWithZod(CelInputRequestBody), + }, + }, + }, + withAvailability(async (context, req, res): Promise> => { + const { dataStreamName, apiDefinition, langSmithOptions } = req.body; + const { getStartServices, logger } = await context.integrationAssistant; + const [, { actions: actionsPlugin }] = await getStartServices(); + + try { + const actionsClient = await actionsPlugin.getActionsClientWithRequest(req); + const connector = await actionsClient.get({ id: req.body.connectorId }); + + const abortSignal = getRequestAbortedSignal(req.events.aborted$); + + const actionTypeId = connector.actionTypeId; + const llmType = getLLMType(actionTypeId); + const llmClass = getLLMClass(llmType); + + const model = new llmClass({ + actionsClient, + connectorId: connector.id, + logger, + llmType, + model: connector.config?.defaultModel, + temperature: 0.05, + maxTokens: 4096, + signal: abortSignal, + streaming: false, + }); + + const parameters = { + dataStreamName, + apiDefinition, + }; + + const options = { + callbacks: [ + new APMTracer({ projectName: langSmithOptions?.projectName ?? 'default' }, logger), + ...getLangSmithTracer({ ...langSmithOptions, logger }), + ], + }; + + const graph = await getCelGraph({ model }); + const results = await graph.invoke(parameters, options); + + return res.ok({ body: CelInputResponse.parse(results) }); + } catch (e) { + if (isErrorThatHandlesItsOwnResponse(e)) { + return e.sendResponse(res); + } + return res.badRequest({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/integration_assistant/server/routes/register_routes.ts b/x-pack/plugins/integration_assistant/server/routes/register_routes.ts index 781010972ddcb..3f8795421a5c0 100644 --- a/x-pack/plugins/integration_assistant/server/routes/register_routes.ts +++ b/x-pack/plugins/integration_assistant/server/routes/register_routes.ts @@ -13,12 +13,21 @@ import { registerRelatedRoutes } from './related_routes'; import { registerPipelineRoutes } from './pipeline_routes'; import type { IntegrationAssistantRouteHandlerContext } from '../plugin'; import { registerAnalyzeLogsRoutes } from './analyze_logs_routes'; +import { registerCelInputRoutes } from './cel_routes'; +import { ExperimentalFeatures } from '../../common/experimental_features'; -export function registerRoutes(router: IRouter) { +export function registerRoutes( + router: IRouter, + experimentalFeatures: ExperimentalFeatures +) { registerAnalyzeLogsRoutes(router); registerEcsRoutes(router); registerIntegrationBuilderRoutes(router); registerCategorizationRoutes(router); registerRelatedRoutes(router); registerPipelineRoutes(router); + + if (experimentalFeatures.generateCel) { + registerCelInputRoutes(router); + } } diff --git a/x-pack/plugins/integration_assistant/server/templates/manifest/cel_manifest.yml.njk b/x-pack/plugins/integration_assistant/server/templates/manifest/cel_manifest.yml.njk index 04db3351691d4..025732305a59d 100644 --- a/x-pack/plugins/integration_assistant/server/templates/manifest/cel_manifest.yml.njk +++ b/x-pack/plugins/integration_assistant/server/templates/manifest/cel_manifest.yml.njk @@ -42,18 +42,8 @@ show_user: true multi: false required: true - default: | - # // Fetch the agent's public IP every minute and note when the last request was made. - # // It does not use the Resource URL configuration value. - # bytes(get("https://api.ipify.org/?format=json").Body).as(body, { - # "events": [body.decode_json().with({ - # "last_requested_at": has(state.cursor) && has(state.cursor.last_requested_at) ? - # state.cursor.last_requested_at - # : - # now - # })], - # "cursor": {"last_requested_at": now} - # }) + default: | {% for programLine in program %} + {{ programLine }}{% endfor %} - name: state type: yaml title: Initial CEL evaluation state @@ -62,7 +52,9 @@ More information can be found in the [documentation](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-cel.html#input-state-cel). show_user: true multi: false - required: false + required: false{% if state %} + default: | {% for key, value in state %} + {{ key }} : {% if value %}{{ value }}{% else %}""{% endif %}{% endfor %}{% endif %} - name: regexp type: yaml title: Defined Regular Expressions @@ -135,7 +127,9 @@ in logs. This may leak secrets, so list sensitive state fields in this configuration. show_user: true multi: true - required: false + required: false{% if redact.length > 0 %} + default: {% for redactVar in redact %} + - {{ redactVar }}{% endfor %}{% endif %} - name: delete_redacted_fields type: bool title: Delete redacted fields diff --git a/x-pack/plugins/integration_assistant/server/types.ts b/x-pack/plugins/integration_assistant/server/types.ts index 454370a02c366..3c17761c495b0 100644 --- a/x-pack/plugins/integration_assistant/server/types.ts +++ b/x-pack/plugins/integration_assistant/server/types.ts @@ -64,6 +64,20 @@ export interface CategorizationState { samplesFormat: SamplesFormat; } +export interface CelInputState { + dataStreamName: string; + apiDefinition: string; + lastExecutedChain: string; + finalized: boolean; + apiQuerySummary: string; + exampleCelPrograms: string[]; + currentProgram: string; + stateVarNames: string[]; + stateSettings: object; + redactVars: string[]; + results: object; +} + export interface EcsMappingState { ecs: string; chunkSize: number;