diff --git a/.eslintrc.js b/.eslintrc.js index ddd39ed00747a..e1153546e8154 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -984,6 +984,7 @@ module.exports = { // front end and common typescript and javascript files only files: [ 'x-pack/plugins/ecs_data_quality_dashboard/common/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/elastic_assistant/common/**/*.{js,mjs,ts,tsx}', 'x-pack/packages/kbn-elastic-assistant/**/*.{js,mjs,ts,tsx}', 'x-pack/packages/security-solution/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/security_solution/public/**/*.{js,mjs,ts,tsx}', @@ -1016,6 +1017,7 @@ module.exports = { // This should be a very small set as most linter rules are useful for tests as well. files: [ 'x-pack/plugins/ecs_data_quality_dashboard/**/*.{ts,tsx}', + 'x-pack/plugins/elastic_assistant/**/*.{ts,tsx}', 'x-pack/packages/kbn-elastic-assistant/**/*.{ts,tsx}', 'x-pack/packages/security-solution/**/*.{ts,tsx}', 'x-pack/plugins/security_solution/**/*.{ts,tsx}', @@ -1026,6 +1028,7 @@ module.exports = { ], excludedFiles: [ 'x-pack/plugins/ecs_data_quality_dashboard/**/*.{test,mock,test_helper}.{ts,tsx}', + 'x-pack/plugins/elastic_assistant/**/*.{test,mock,test_helper}.{ts,tsx}', 'x-pack/packages/kbn-elastic-assistant/**/*.{test,mock,test_helper}.{ts,tsx}', 'x-pack/packages/security-solution/**/*.{test,mock,test_helper}.{ts,tsx}', 'x-pack/plugins/security_solution/**/*.{test,mock,test_helper}.{ts,tsx}', @@ -1042,6 +1045,7 @@ module.exports = { // typescript only for front and back end files: [ 'x-pack/plugins/ecs_data_quality_dashboard/**/*.{ts,tsx}', + 'x-pack/plugins/elastic_assistant/**/*.{ts,tsx}', 'x-pack/packages/kbn-elastic-assistant/**/*.{ts,tsx}', 'x-pack/packages/security-solution/**/*.{ts,tsx}', 'x-pack/plugins/security_solution/**/*.{ts,tsx}', @@ -1077,6 +1081,7 @@ module.exports = { // typescript and javascript for front and back end files: [ 'x-pack/plugins/ecs_data_quality_dashboard/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/elastic_assistant/**/*.{js,mjs,ts,tsx}', 'x-pack/packages/kbn-elastic-assistant/**/*.{js,mjs,ts,tsx}', 'x-pack/packages/security-solution/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6189270654c7b..f23d2df2fb092 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -340,6 +340,7 @@ packages/kbn-ecs @elastic/kibana-core @elastic/security-threat-hunting-investiga x-pack/packages/security-solution/ecs_data_quality_dashboard @elastic/security-threat-hunting-investigations x-pack/plugins/ecs_data_quality_dashboard @elastic/security-threat-hunting-investigations x-pack/packages/kbn-elastic-assistant @elastic/security-solution +x-pack/plugins/elastic_assistant @elastic/security-solution test/plugin_functional/plugins/elasticsearch_client_plugin @elastic/kibana-core x-pack/test/plugin_api_integration/plugins/elasticsearch_client @elastic/kibana-core x-pack/plugins/embeddable_enhanced @elastic/kibana-presentation diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 41aecaccf554c..632bce5883c28 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -528,6 +528,10 @@ Plugin server-side only. Plugin has three main functions: |This plugin implements (server) APIs used to render the content of the Data Quality dashboard. +|{kib-repo}blob/{branch}/x-pack/plugins/elastic_assistant/README.md[elasticAssistant] +|This plugin implements (only) server APIs for the Elastic AI Assistant. + + |<> |Enhances Embeddables by registering a custom factory provider. The enhanced factory provider adds dynamic actions to every embeddables state, in order to support drilldowns. diff --git a/package.json b/package.json index 657329a4bb6a8..fc076f17b7407 100644 --- a/package.json +++ b/package.json @@ -383,6 +383,7 @@ "@kbn/ecs-data-quality-dashboard": "link:x-pack/packages/security-solution/ecs_data_quality_dashboard", "@kbn/ecs-data-quality-dashboard-plugin": "link:x-pack/plugins/ecs_data_quality_dashboard", "@kbn/elastic-assistant": "link:x-pack/packages/kbn-elastic-assistant", + "@kbn/elastic-assistant-plugin": "link:x-pack/plugins/elastic_assistant", "@kbn/elasticsearch-client-plugin": "link:test/plugin_functional/plugins/elasticsearch_client_plugin", "@kbn/elasticsearch-client-xpack-plugin": "link:x-pack/test/plugin_api_integration/plugins/elasticsearch_client", "@kbn/embeddable-enhanced-plugin": "link:x-pack/plugins/embeddable_enhanced", @@ -897,6 +898,7 @@ "jsonwebtoken": "^9.0.0", "jsts": "^1.6.2", "kea": "^2.4.2", + "langchain": "^0.0.132", "launchdarkly-js-client-sdk": "^2.22.1", "launchdarkly-node-server-sdk": "^6.4.2", "load-json-file": "^6.2.0", @@ -1559,6 +1561,7 @@ "val-loader": "^1.1.1", "vinyl-fs": "^4.0.0", "watchpack": "^1.6.0", + "web-streams-polyfill": "^3.2.1", "webpack": "^4.41.5", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0", diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index f80a5e103c16f..bbe3cb2923280 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -105,8 +105,10 @@ module.exports = { transformIgnorePatterns: [ // ignore all node_modules except monaco-editor and react-monaco-editor which requires babel transforms to handle dynamic import() // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) - '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|vscode-languageserver-types|react-monaco-editor|d3-interpolate|d3-color))[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](byte-size|monaco-editor|monaco-yaml|vscode-languageserver-types|react-monaco-editor|d3-interpolate|d3-color|langchain|langsmith))[/\\\\].+\\.js$', 'packages/kbn-pm/dist/index.js', + '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/util/[/\\\\].+\\.js$', ], // An array of regexp pattern strings that are matched against all source file paths, matched files to include/exclude for code coverage diff --git a/packages/kbn-test/jest_integration_node/jest-preset.js b/packages/kbn-test/jest_integration_node/jest-preset.js index 43373e41db5c1..92b8aedb5ee88 100644 --- a/packages/kbn-test/jest_integration_node/jest-preset.js +++ b/packages/kbn-test/jest_integration_node/jest-preset.js @@ -19,6 +19,13 @@ module.exports = { testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), + // An array of regexp pattern strings that are matched against, matched files will skip transformation: + transformIgnorePatterns: [ + // since ESM modules are not natively supported in Jest yet (https://github.com/facebook/jest/issues/4842) + '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/[/\\\\].+\\.js$', + '[/\\\\]node_modules(?![\\/\\\\](langchain|langsmith))/dist/util/[/\\\\].+\\.js$', + ], setupFilesAfterEnv: [ '/packages/kbn-test/src/jest/setup/after_env.integration.js', '/packages/kbn-test/src/jest/setup/mocks.moment_timezone.js', diff --git a/packages/kbn-test/src/jest/setup/setup_test.js b/packages/kbn-test/src/jest/setup/setup_test.js index b0038daf196c9..ee386f894a71e 100644 --- a/packages/kbn-test/src/jest/setup/setup_test.js +++ b/packages/kbn-test/src/jest/setup/setup_test.js @@ -13,6 +13,7 @@ import 'jest-styled-components'; import '@testing-library/jest-dom'; +import 'web-streams-polyfill/es6'; // ReadableStream polyfill /** * Removed in Jest 27/jsdom, used in some transitive dependencies diff --git a/tsconfig.base.json b/tsconfig.base.json index 2efdd91c53d3b..a347a249b68ca 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -674,6 +674,8 @@ "@kbn/ecs-data-quality-dashboard-plugin/*": ["x-pack/plugins/ecs_data_quality_dashboard/*"], "@kbn/elastic-assistant": ["x-pack/packages/kbn-elastic-assistant"], "@kbn/elastic-assistant/*": ["x-pack/packages/kbn-elastic-assistant/*"], + "@kbn/elastic-assistant-plugin": ["x-pack/plugins/elastic_assistant"], + "@kbn/elastic-assistant-plugin/*": ["x-pack/plugins/elastic_assistant/*"], "@kbn/elasticsearch-client-plugin": ["test/plugin_functional/plugins/elasticsearch_client_plugin"], "@kbn/elasticsearch-client-plugin/*": ["test/plugin_functional/plugins/elasticsearch_client_plugin/*"], "@kbn/elasticsearch-client-xpack-plugin": ["x-pack/test/plugin_api_integration/plugins/elasticsearch_client"], @@ -1645,4 +1647,5 @@ "@kbn/ambient-storybook-types" ] } -} \ No newline at end of file +} + diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx new file mode 100644 index 0000000000000..65b8183b60a0b --- /dev/null +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.test.tsx @@ -0,0 +1,129 @@ +/* + * 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 { HttpSetup } from '@kbn/core-http-browser'; +import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common'; + +import { fetchConnectorExecuteAction, FetchConnectorExecuteAction } from './api'; +import type { Conversation, Message } from '../assistant_context/types'; +import { API_ERROR } from './translations'; + +jest.mock('@kbn/core-http-browser'); + +const mockHttp = { + fetch: jest.fn(), +} as unknown as HttpSetup; + +const apiConfig: Conversation['apiConfig'] = { + connectorId: 'foo', + model: 'gpt-4', + provider: OpenAiProviderType.OpenAi, +}; + +const messages: Message[] = [ + { content: 'This is a test', role: 'user', timestamp: new Date().toLocaleString() }, +]; + +describe('fetchConnectorExecuteAction', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls the internal assistant API when assistantLangChain is true', async () => { + const testProps: FetchConnectorExecuteAction = { + assistantLangChain: true, + http: mockHttp, + messages, + apiConfig, + }; + + await fetchConnectorExecuteAction(testProps); + + expect(mockHttp.fetch).toHaveBeenCalledWith( + '/internal/elastic_assistant/actions/connector/foo/_execute', + { + body: '{"params":{"subActionParams":{"body":"{\\"model\\":\\"gpt-4\\",\\"messages\\":[{\\"role\\":\\"user\\",\\"content\\":\\"This is a test\\"}],\\"n\\":1,\\"stop\\":null,\\"temperature\\":0.2}"},"subAction":"test"}}', + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + signal: undefined, + } + ); + }); + + it('calls the actions connector api when assistantLangChain is false', async () => { + const testProps: FetchConnectorExecuteAction = { + assistantLangChain: false, + http: mockHttp, + messages, + apiConfig, + }; + + await fetchConnectorExecuteAction(testProps); + + expect(mockHttp.fetch).toHaveBeenCalledWith('/api/actions/connector/foo/_execute', { + body: '{"params":{"subActionParams":{"body":"{\\"model\\":\\"gpt-4\\",\\"messages\\":[{\\"role\\":\\"user\\",\\"content\\":\\"This is a test\\"}],\\"n\\":1,\\"stop\\":null,\\"temperature\\":0.2}"},"subAction":"test"}}', + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + signal: undefined, + }); + }); + + it('returns API_ERROR when the response status is not ok', async () => { + (mockHttp.fetch as jest.Mock).mockResolvedValue({ status: 'error' }); + + const testProps: FetchConnectorExecuteAction = { + assistantLangChain: false, + http: mockHttp, + messages, + apiConfig, + }; + + const result = await fetchConnectorExecuteAction(testProps); + + expect(result).toBe(API_ERROR); + }); + + it('returns API_ERROR when there are no choices', async () => { + (mockHttp.fetch as jest.Mock).mockResolvedValue({ status: 'ok', data: {} }); + const testProps: FetchConnectorExecuteAction = { + assistantLangChain: false, + http: mockHttp, + messages, + apiConfig, + }; + + const result = await fetchConnectorExecuteAction(testProps); + + expect(result).toBe(API_ERROR); + }); + + it('return the trimmed first `choices` `message` `content` when the API call is successful', async () => { + (mockHttp.fetch as jest.Mock).mockResolvedValue({ + status: 'ok', + data: { + choices: [ + { + message: { + content: ' Test response ', // leading and trailing whitespace + }, + }, + ], + }, + }); + + const testProps: FetchConnectorExecuteAction = { + assistantLangChain: false, + http: mockHttp, + messages, + apiConfig, + }; + + const result = await fetchConnectorExecuteAction(testProps); + + expect(result).toBe('Test response'); + }); +}); diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx index 0bb68409caf91..c8624e365419d 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx @@ -14,6 +14,7 @@ import { API_ERROR } from './translations'; import { MODEL_GPT_3_5_TURBO } from '../connectorland/models/model_selector/model_selector'; export interface FetchConnectorExecuteAction { + assistantLangChain: boolean; apiConfig: Conversation['apiConfig']; http: HttpSetup; messages: Message[]; @@ -21,6 +22,7 @@ export interface FetchConnectorExecuteAction { } export const fetchConnectorExecuteAction = async ({ + assistantLangChain, http, messages, apiConfig, @@ -54,19 +56,20 @@ export const fetchConnectorExecuteAction = async ({ }; try { + const path = assistantLangChain + ? `/internal/elastic_assistant/actions/connector/${apiConfig?.connectorId}/_execute` + : `/api/actions/connector/${apiConfig?.connectorId}/_execute`; + // TODO: Find return type for this API // eslint-disable-next-line @typescript-eslint/no-explicit-any - const response = await http.fetch( - `/api/actions/connector/${apiConfig?.connectorId}/_execute`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - signal, - } - ); + const response = await http.fetch(path, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + signal, + }); const data = response.data; if (response.status !== 'ok') { diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_send_messages/index.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_send_messages/index.tsx index 3a3b72c253862..c68d82d99b9ac 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_send_messages/index.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant/use_send_messages/index.tsx @@ -8,6 +8,8 @@ import { useCallback, useState } from 'react'; import { HttpSetup } from '@kbn/core-http-browser'; + +import { useAssistantContext } from '../../assistant_context'; import { Conversation, Message } from '../../assistant_context/types'; import { fetchConnectorExecuteAction } from '../api'; @@ -23,20 +25,25 @@ interface UseSendMessages { } export const useSendMessages = (): UseSendMessages => { + const { assistantLangChain } = useAssistantContext(); const [isLoading, setIsLoading] = useState(false); - const sendMessages = useCallback(async ({ apiConfig, http, messages }: SendMessagesProps) => { - setIsLoading(true); - try { - return await fetchConnectorExecuteAction({ - http, - messages, - apiConfig, - }); - } finally { - setIsLoading(false); - } - }, []); + const sendMessages = useCallback( + async ({ apiConfig, http, messages }: SendMessagesProps) => { + setIsLoading(true); + try { + return await fetchConnectorExecuteAction({ + assistantLangChain, + http, + messages, + apiConfig, + }); + } finally { + setIsLoading(false); + } + }, + [assistantLangChain] + ); return { isLoading, sendMessages }; }; diff --git a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx index 466ccbb83cb0f..6e39da055043a 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/assistant_context/index.test.tsx @@ -28,6 +28,7 @@ const ContextWrapper: React.FC = ({ children }) => ( CodeBlockDetails[][]; baseAllow: string[]; @@ -85,6 +86,7 @@ export interface UseAssistantContext { augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][]; allQuickPrompts: QuickPrompt[]; allSystemPrompts: Prompt[]; + assistantLangChain: boolean; baseAllow: string[]; baseAllowReplacement: string[]; docLinks: Omit; @@ -129,6 +131,7 @@ const AssistantContext = React.createContext(un export const AssistantProvider: React.FC = ({ actionTypeRegistry, assistantAvailability, + assistantLangChain, assistantTelemetry, augmentMessageCodeBlocks, baseAllow, @@ -248,6 +251,7 @@ export const AssistantProvider: React.FC = ({ () => ({ actionTypeRegistry, assistantAvailability, + assistantLangChain, assistantTelemetry, augmentMessageCodeBlocks, allQuickPrompts: localStorageQuickPrompts ?? [], @@ -284,6 +288,7 @@ export const AssistantProvider: React.FC = ({ [ actionTypeRegistry, assistantAvailability, + assistantLangChain, assistantTelemetry, augmentMessageCodeBlocks, baseAllow, diff --git a/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx b/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx index aec3a6262ba4c..484dd316cc0ac 100644 --- a/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx +++ b/x-pack/packages/kbn-elastic-assistant/impl/mock/test_providers/test_providers.tsx @@ -66,6 +66,7 @@ export const TestProvidersComponent: React.FC = ({ = ({ children, isILMAvailab /x-pack/plugins/elastic_assistant/{common,lib,server}/**/*.{ts,tsx}', + ], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/elastic_assistant', + coverageReporters: ['text', 'html'], + rootDir: '../../..', + roots: ['/x-pack/plugins/elastic_assistant'], + preset: '@kbn/test', +}; diff --git a/x-pack/plugins/elastic_assistant/kibana.jsonc b/x-pack/plugins/elastic_assistant/kibana.jsonc new file mode 100644 index 0000000000000..d7518cf600983 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/kibana.jsonc @@ -0,0 +1,15 @@ +{ + "type": "plugin", + "id": "@kbn/elastic-assistant-plugin", + "owner": "@elastic/security-solution", + "description": "Server APIs for the Elastic AI Assistant", + "plugin": { + "id": "elasticAssistant", + "server": true, + "browser": false, + "requiredPlugins": [ + "actions", + "data" + ] + } +} diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/action_result_data.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/action_result_data.ts new file mode 100644 index 0000000000000..280a86d2ac326 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/action_result_data.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. + */ + +/** + * A mock `data` property from an `actionResult` response, which is returned + * from the `execute` method of the Actions plugin. + * + * Given the following example: + * + * ```ts + * const actionResult = await actionsClient.execute(requestBody); + * ``` + * + * In the above example, `actionResult.data` would be this mock data. + */ +export const mockActionResultData = { + id: 'chatcmpl-7sFVvksgFtMUac3pY5bTypFAKaGX1', + object: 'chat.completion', + created: 1693163703, + model: 'gpt-4', + choices: [ + { + index: 0, + finish_reason: 'stop', + message: { + role: 'assistant', + content: 'Yes, your name is Andrew. How can I assist you further, Andrew?', + }, + }, + ], + usage: { completion_tokens: 16, prompt_tokens: 140, total_tokens: 156 }, +}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/lang_chain_messages.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/lang_chain_messages.ts new file mode 100644 index 0000000000000..bbf32c714065f --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/lang_chain_messages.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 { AIMessage, BaseMessage, HumanMessage } from 'langchain/schema'; + +export const langChainMessages: BaseMessage[] = [ + new HumanMessage('What is my name?'), + new AIMessage( + "I'm sorry, but I am not able to answer questions unrelated to Elastic Security. If you have any questions about Elastic Security, please feel free to ask." + ), + new HumanMessage('\n\nMy name is Andrew'), + new AIMessage( + "Hello Andrew! If you have any questions about Elastic Security, feel free to ask, and I'll do my best to help you." + ), + new HumanMessage('\n\nDo you know my name?'), +]; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request.ts new file mode 100644 index 0000000000000..827f08683e0b8 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request.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 { httpServerMock } from '@kbn/core/server/mocks'; + +export const requestMock = { + create: httpServerMock.createKibanaRequest, +}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts new file mode 100644 index 0000000000000..19fb44f7f8bac --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/request_context.ts @@ -0,0 +1,56 @@ +/* + * 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 { coreMock } from '@kbn/core/server/mocks'; +import { licensingMock } from '@kbn/licensing-plugin/server/mocks'; + +export const createMockClients = () => { + const core = coreMock.createRequestHandlerContext(); + const license = licensingMock.createLicenseMock(); + + return { + core, + clusterClient: core.elasticsearch.client, + savedObjectsClient: core.savedObjects.client, + + licensing: { + ...licensingMock.createRequestHandlerContext({ license }), + license, + }, + + config: createMockConfig(), + appClient: createAppClientMock(), + }; +}; + +type MockClients = ReturnType; + +const convertRequestContextMock = (context: T) => { + return coreMock.createCustomRequestHandlerContext(context); +}; + +const createMockConfig = () => ({}); + +const createAppClientMock = () => ({}); + +const createRequestContextMock = (clients: MockClients = createMockClients()) => { + return { + core: clients.core, + }; +}; + +const createTools = () => { + const clients = createMockClients(); + const context = createRequestContextMock(clients); + + return { clients, context }; +}; + +export const requestContextMock = { + create: createRequestContextMock, + convertContext: convertRequestContextMock, + createTools, +}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts new file mode 100644 index 0000000000000..8efe2407f2245 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/response.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServerMock } from '@kbn/core/server/mocks'; + +export const responseMock = { + create: httpServerMock.createResponseFactory, +}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/server.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/server.ts new file mode 100644 index 0000000000000..7ac44e1beedf1 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/server.ts @@ -0,0 +1,95 @@ +/* + * 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 { httpServiceMock } from '@kbn/core/server/mocks'; +import type { RequestHandler, RouteConfig, KibanaRequest } from '@kbn/core/server'; +import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; + +import { requestMock } from './request'; +import { responseMock as responseFactoryMock } from './response'; +import { requestContextMock } from './request_context'; +import { responseAdapter } from './test_adapters'; + +interface Route { + config: RouteConfig; + handler: RequestHandler; +} + +const getRoute = (routerMock: MockServer['router']): Route => { + const routeCalls = [ + ...routerMock.get.mock.calls, + ...routerMock.post.mock.calls, + ...routerMock.put.mock.calls, + ...routerMock.patch.mock.calls, + ...routerMock.delete.mock.calls, + ]; + + const [route] = routeCalls; + if (!route) { + throw new Error('No route registered!'); + } + + const [config, handler] = route; + return { config, handler }; +}; + +const buildResultMock = () => ({ ok: jest.fn((x) => x), badRequest: jest.fn((x) => x) }); + +class MockServer { + constructor( + public readonly router = httpServiceMock.createRouter(), + private responseMock = responseFactoryMock.create(), + private contextMock = requestContextMock.convertContext(requestContextMock.create()), + private resultMock = buildResultMock() + ) {} + + public validate(request: KibanaRequest) { + this.validateRequest(request); + return this.resultMock; + } + + public async inject(request: KibanaRequest, context: RequestHandlerContext = this.contextMock) { + const validatedRequest = this.validateRequest(request); + const [rejection] = this.resultMock.badRequest.mock.calls; + if (rejection) { + throw new Error(`Request was rejected with message: '${rejection}'`); + } + + await this.getRoute().handler(context, validatedRequest, this.responseMock); + return responseAdapter(this.responseMock); + } + + private getRoute(): Route { + return getRoute(this.router); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private maybeValidate(part: any, validator?: any): any { + return typeof validator === 'function' ? validator(part, this.resultMock) : part; + } + + private validateRequest(request: KibanaRequest): KibanaRequest { + const validations = this.getRoute().config.validate; + if (!validations) { + return request; + } + + const validatedRequest = requestMock.create({ + path: request.route.path, + method: request.route.method, + body: this.maybeValidate(request.body, validations.body), + query: this.maybeValidate(request.query, validations.query), + params: this.maybeValidate(request.params, validations.params), + }); + + return validatedRequest; + } +} +const createMockServer = () => new MockServer(); + +export const serverMock = { + create: createMockServer, +}; diff --git a/x-pack/plugins/elastic_assistant/server/__mocks__/test_adapters.ts b/x-pack/plugins/elastic_assistant/server/__mocks__/test_adapters.ts new file mode 100644 index 0000000000000..4de81ca931692 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/__mocks__/test_adapters.ts @@ -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 type { responseMock } from './response'; + +type ResponseMock = ReturnType; +type Method = keyof ResponseMock; + +type MockCall = any; // eslint-disable-line @typescript-eslint/no-explicit-any + +interface ResponseCall { + body: any; // eslint-disable-line @typescript-eslint/no-explicit-any + status: number; +} + +/** + * @internal + */ +export interface Response extends ResponseCall { + calls: ResponseCall[]; +} + +const buildResponses = (method: Method, calls: MockCall[]): ResponseCall[] => { + if (!calls.length) return []; + + switch (method) { + case 'ok': + return calls.map(([call]) => ({ status: 200, body: call.body })); + case 'custom': + return calls.map(([call]) => ({ + status: call.statusCode, + body: JSON.parse(call.body), + })); + case 'customError': + return calls.map(([call]) => ({ + status: call.statusCode, + body: call.body, + })); + default: + throw new Error(`Encountered unexpected call to response.${method}`); + } +}; + +export const responseAdapter = (response: ResponseMock): Response => { + const methods = Object.keys(response) as Method[]; + const calls = methods + .reduce((responses, method) => { + const methodMock = response[method]; + return [...responses, ...buildResponses(method, methodMock.mock.calls)]; + }, []) + .sort((call, other) => other.status - call.status); + + const [{ body, status }] = calls; + + return { + body, + status, + calls, + }; +}; diff --git a/x-pack/plugins/elastic_assistant/server/index.ts b/x-pack/plugins/elastic_assistant/server/index.ts new file mode 100755 index 0000000000000..a375e036d8238 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/index.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. + */ + +import { PluginInitializerContext } from '@kbn/core/server'; +import { ElasticAssistantPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new ElasticAssistantPlugin(initializerContext); +} + +export type { + ElasticAssistantPluginSetup as EcsDataQualityDashboardPluginSetup, + ElasticAssistantPluginStart as EcsDataQualityDashboardPluginStart, +} from './types'; diff --git a/x-pack/plugins/elastic_assistant/server/lib/build_response/index.ts b/x-pack/plugins/elastic_assistant/server/lib/build_response/index.ts new file mode 100644 index 0000000000000..bbec702c74915 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/build_response/index.ts @@ -0,0 +1,68 @@ +/* + * 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 { CustomHttpResponseOptions, KibanaResponseFactory } from '@kbn/core-http-server'; + +const statusToErrorMessage = ( + statusCode: number +): + | 'Bad Request' + | 'Unauthorized' + | 'Forbidden' + | 'Not Found' + | 'Conflict' + | 'Internal Error' + | '(unknown error)' => { + switch (statusCode) { + case 400: + return 'Bad Request'; + case 401: + return 'Unauthorized'; + case 403: + return 'Forbidden'; + case 404: + return 'Not Found'; + case 409: + return 'Conflict'; + case 500: + return 'Internal Error'; + default: + return '(unknown error)'; + } +}; + +/** Creates responses */ +export class ResponseFactory { + /** constructor */ + constructor(private response: KibanaResponseFactory) {} + + /** error */ + error({ statusCode, body, headers }: CustomHttpResponseOptions) { + const contentType: CustomHttpResponseOptions['headers'] = { + 'content-type': 'application/json', + }; + const defaultedHeaders: CustomHttpResponseOptions['headers'] = { + ...contentType, + ...(headers ?? {}), + }; + + return this.response.custom({ + body: Buffer.from( + JSON.stringify({ + message: body ?? statusToErrorMessage(statusCode), + status_code: statusCode, + }) + ), + headers: defaultedHeaders, + statusCode, + }); + } +} + +/** builds a response */ +export const buildResponse = (response: KibanaResponseFactory): ResponseFactory => + new ResponseFactory(response); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/execute_custom_llm_chain/index.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/execute_custom_llm_chain/index.test.ts new file mode 100644 index 0000000000000..b6c4dd3917585 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/execute_custom_llm_chain/index.test.ts @@ -0,0 +1,105 @@ +/* + * 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 { KibanaRequest } from '@kbn/core/server'; +import { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; + +import { ResponseBody } from '../helpers'; +import { ActionsClientLlm } from '../llm/actions_client_llm'; +import { mockActionResultData } from '../../../__mocks__/action_result_data'; +import { langChainMessages } from '../../../__mocks__/lang_chain_messages'; +import { executeCustomLlmChain } from '.'; + +jest.mock('../llm/actions_client_llm'); + +const mockConversationChain = { + call: jest.fn(), +}; + +jest.mock('langchain/chains', () => ({ + ConversationChain: jest.fn().mockImplementation(() => mockConversationChain), +})); + +const mockConnectorId = 'mock-connector-id'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockRequest: KibanaRequest = {} as KibanaRequest< + unknown, + unknown, + any, // eslint-disable-line @typescript-eslint/no-explicit-any + any // eslint-disable-line @typescript-eslint/no-explicit-any +>; + +const mockActions: ActionsPluginStart = {} as ActionsPluginStart; + +describe('executeCustomLlmChain', () => { + beforeEach(() => { + jest.clearAllMocks(); + + ActionsClientLlm.prototype.getActionResultData = jest + .fn() + .mockReturnValueOnce(mockActionResultData); + }); + + it('creates an instance of ActionsClientLlm with the expected context from the request', async () => { + await executeCustomLlmChain({ + actions: mockActions, + connectorId: mockConnectorId, + langChainMessages, + request: mockRequest, + }); + + expect(ActionsClientLlm).toHaveBeenCalledWith({ + actions: mockActions, + connectorId: mockConnectorId, + request: mockRequest, + }); + }); + + it('kicks off the chain with (only) the last message', async () => { + await executeCustomLlmChain({ + actions: mockActions, + connectorId: mockConnectorId, + langChainMessages, + request: mockRequest, + }); + + expect(mockConversationChain.call).toHaveBeenCalledWith({ + input: '\n\nDo you know my name?', + }); + }); + + it('kicks off the chain with the expected message when langChainMessages has only one entry', async () => { + const onlyOneMessage = [langChainMessages[0]]; + + await executeCustomLlmChain({ + actions: mockActions, + connectorId: mockConnectorId, + langChainMessages: onlyOneMessage, + request: mockRequest, + }); + + expect(mockConversationChain.call).toHaveBeenCalledWith({ + input: 'What is my name?', + }); + }); + + it('returns the expected response body', async () => { + const result: ResponseBody = await executeCustomLlmChain({ + actions: mockActions, + connectorId: mockConnectorId, + langChainMessages, + request: mockRequest, + }); + + expect(result).toEqual({ + connector_id: 'mock-connector-id', + data: mockActionResultData, + status: 'ok', + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/execute_custom_llm_chain/index.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/execute_custom_llm_chain/index.ts new file mode 100644 index 0000000000000..ee7b6820c983d --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/execute_custom_llm_chain/index.ts @@ -0,0 +1,51 @@ +/* + * 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 { KibanaRequest } from '@kbn/core/server'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; +import { ConversationChain } from 'langchain/chains'; +import { BufferMemory, ChatMessageHistory } from 'langchain/memory'; +import { BaseMessage } from 'langchain/schema'; + +import { ActionsClientLlm } from '../llm/actions_client_llm'; +import { ResponseBody } from '../helpers'; + +export const executeCustomLlmChain = async ({ + actions, + connectorId, + langChainMessages, + request, +}: { + actions: ActionsPluginStart; + connectorId: string; + langChainMessages: BaseMessage[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: KibanaRequest; +}): Promise => { + const llm = new ActionsClientLlm({ actions, connectorId, request }); + + const pastMessages = langChainMessages.slice(0, -1); // all but the last message + const latestMessage = langChainMessages.slice(-1); // the last message + + const memory = new BufferMemory({ + chatHistory: new ChatMessageHistory(pastMessages), + }); + + const chain = new ConversationChain({ llm, memory }); + + await chain.call({ input: latestMessage[0].content }); // kick off the chain with the last message + + // The assistant (on the client side) expects the same response returned + // from the actions framework, so we need to return the same shape of data: + const responseBody = { + connector_id: connectorId, + data: llm.getActionResultData(), // the response from the actions framework + status: 'ok', + }; + + return responseBody; +}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.test.ts new file mode 100644 index 0000000000000..1c62fab9df6cc --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.test.ts @@ -0,0 +1,185 @@ +/* + * 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 { Message } from '@kbn/elastic-assistant'; +import { AIMessage, BaseMessage, HumanMessage, SystemMessage } from 'langchain/schema'; + +import { + getLangChainMessage, + getLangChainMessages, + getMessageContentAndRole, + unsafeGetAssistantMessagesFromRequest, +} from './helpers'; +import { langChainMessages } from '../../__mocks__/lang_chain_messages'; + +describe('helpers', () => { + describe('getLangChainMessage', () => { + const testCases: Array<[Pick, typeof BaseMessage]> = [ + [ + { + role: 'system', + content: 'System message', + }, + SystemMessage, + ], + [ + { + role: 'user', + content: 'User message', + }, + HumanMessage, + ], + [ + { + role: 'assistant', + content: 'Assistant message', + }, + AIMessage, + ], + [ + { + role: 'unknown' as Message['role'], + content: 'Unknown message', + }, + HumanMessage, + ], + ]; + + testCases.forEach(([testCase, expectedClass]) => { + it(`returns the expected content when role is ${testCase.role}`, () => { + const result = getLangChainMessage(testCase); + + expect(result.content).toEqual(testCase.content); + }); + + it(`returns the expected BaseMessage instance when role is ${testCase.role}`, () => { + const result = getLangChainMessage(testCase); + + expect(result instanceof expectedClass).toBeTruthy(); + }); + }); + }); + + describe('getLangChainMessages', () => { + const assistantMessages: Array> = [ + { + content: 'What is my name?', + role: 'user', + }, + { + content: + "I'm sorry, but I am not able to answer questions unrelated to Elastic Security. If you have any questions about Elastic Security, please feel free to ask.", + role: 'assistant', + }, + { + content: '\n\nMy name is Andrew', + role: 'user', + }, + { + content: + "Hello Andrew! If you have any questions about Elastic Security, feel free to ask, and I'll do my best to help you.", + role: 'assistant', + }, + { + content: '\n\nDo you know my name?', + role: 'user', + }, + ]; + + it('returns the expected BaseMessage instances', () => { + expect(getLangChainMessages(assistantMessages)).toEqual(langChainMessages); + }); + }); + + describe('getMessageContentAndRole', () => { + const testCases: Array<[string, Pick]> = [ + ['Prompt 1', { content: 'Prompt 1', role: 'user' }], + ['Prompt 2', { content: 'Prompt 2', role: 'user' }], + ['', { content: '', role: 'user' }], + ]; + + testCases.forEach(([prompt, expectedOutput]) => { + test(`Given the prompt "${prompt}", it returns the prompt as content with a "user" role`, () => { + const result = getMessageContentAndRole(prompt); + + expect(result).toEqual(expectedOutput); + }); + }); + }); + + describe('unsafeGetAssistantMessagesFromRequest', () => { + const rawSubActionParamsBody = { + messages: [ + { role: 'user', content: '\n\n\n\nWhat is my name?' }, + { + role: 'assistant', + content: + "Hello! Since we are communicating through text, I do not have the information about your name. Please feel free to share your name with me, if you'd like.", + }, + { role: 'user', content: '\n\nMy name is Andrew' }, + { + role: 'assistant', + content: + "Hi, Andrew! It's nice to meet you. How can I help you or what would you like to talk about today?", + }, + { role: 'user', content: '\n\nDo you know my name?' }, + ], + }; + + it('returns the expected assistant messages from a conversation', () => { + const result = unsafeGetAssistantMessagesFromRequest(JSON.stringify(rawSubActionParamsBody)); + + const expected = [ + { role: 'user', content: '\n\n\n\nWhat is my name?' }, + { + role: 'assistant', + content: + "Hello! Since we are communicating through text, I do not have the information about your name. Please feel free to share your name with me, if you'd like.", + }, + { role: 'user', content: '\n\nMy name is Andrew' }, + { + role: 'assistant', + content: + "Hi, Andrew! It's nice to meet you. How can I help you or what would you like to talk about today?", + }, + { role: 'user', content: '\n\nDo you know my name?' }, + ]; + + expect(result).toEqual(expected); + }); + + it('returns an empty array when the rawSubActionParamsBody is undefined', () => { + const result = unsafeGetAssistantMessagesFromRequest(undefined); + + expect(result).toEqual([]); + }); + + it('returns an empty array when the rawSubActionParamsBody messages[] array is empty', () => { + const hasEmptyMessages = { + messages: [], + }; + + const result = unsafeGetAssistantMessagesFromRequest(JSON.stringify(hasEmptyMessages)); + + expect(result).toEqual([]); + }); + + it('returns an empty array when the rawSubActionParamsBody shape is unexpected', () => { + const unexpected = { invalidKey: 'some_value' }; + + const result = unsafeGetAssistantMessagesFromRequest(JSON.stringify(unexpected)); + + expect(result).toEqual([]); + }); + + it('returns an empty array when the rawSubActionParamsBody is invalid JSON', () => { + const result = unsafeGetAssistantMessagesFromRequest('[]'); + + expect(result).toEqual([]); + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.ts new file mode 100644 index 0000000000000..90364dcfe75db --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/helpers.ts @@ -0,0 +1,57 @@ +/* + * 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 { Message } from '@kbn/elastic-assistant'; +import { AIMessage, BaseMessage, HumanMessage, SystemMessage } from 'langchain/schema'; + +export const getLangChainMessage = ( + assistantMessage: Pick +): BaseMessage => { + switch (assistantMessage.role) { + case 'system': + return new SystemMessage(assistantMessage.content); + case 'user': + return new HumanMessage(assistantMessage.content); + case 'assistant': + return new AIMessage(assistantMessage.content); + default: + return new HumanMessage(assistantMessage.content); + } +}; + +export const getLangChainMessages = ( + assistantMessages: Array> +): BaseMessage[] => assistantMessages.map(getLangChainMessage); + +export const getMessageContentAndRole = (prompt: string): Pick => ({ + content: prompt, + role: 'user', +}); + +export interface ResponseBody { + status: string; + data: Record; + connector_id: string; +} + +/** An unsafe, temporary stub that parses assistant messages from the request with no validation */ +export const unsafeGetAssistantMessagesFromRequest = ( + rawSubActionParamsBody: string | undefined +): Array> => { + try { + if (rawSubActionParamsBody == null) { + return []; + } + + const subActionParamsBody = JSON.parse(rawSubActionParamsBody); // TODO: unsafe, no validation + const messages = subActionParamsBody?.messages; + + return Array.isArray(messages) ? messages : []; + } catch { + return []; + } +}; diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.test.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.test.ts new file mode 100644 index 0000000000000..289793de859c0 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.test.ts @@ -0,0 +1,172 @@ +/* + * 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 { KibanaRequest } from '@kbn/core/server'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; + +import { ActionsClientLlm } from './actions_client_llm'; +import { mockActionResultData } from '../../../__mocks__/action_result_data'; + +const connectorId = 'mock-connector-id'; + +const mockExecute = jest.fn().mockImplementation(() => ({ + data: mockActionResultData, + status: 'ok', +})); + +const mockActions = { + getActionsClientWithRequest: jest.fn().mockImplementation(() => ({ + execute: mockExecute, + })), +} as unknown as ActionsPluginStart; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockRequest: KibanaRequest = { + params: { connectorId }, + body: { + params: { + subActionParams: { + body: '{"messages":[{"role":"user","content":"\\n\\n\\n\\nWhat is my name?"},{"role":"assistant","content":"I\'m sorry, but I don\'t have the information about your name. You can tell me your name if you\'d like, and we can continue our conversation from there."},{"role":"user","content":"\\n\\nMy name is Andrew"},{"role":"assistant","content":"Hello, Andrew! It\'s nice to meet you. What would you like to talk about today?"},{"role":"user","content":"\\n\\nDo you know my name?"}]}', + }, + subAction: 'test', + }, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} as KibanaRequest; + +const prompt = 'Do you know my name?'; + +describe('ActionsClientLlm', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getActionResultData', () => { + it('returns the expected data', async () => { + const actionsClientLlm = new ActionsClientLlm({ + actions: mockActions, + connectorId, + request: mockRequest, + }); + + await actionsClientLlm._call(prompt); // ignore the result + + expect(actionsClientLlm.getActionResultData()).toEqual(mockActionResultData); + }); + }); + + describe('_llmType', () => { + it('returns the expected LLM type', () => { + const actionsClientLlm = new ActionsClientLlm({ + actions: mockActions, + connectorId, + request: mockRequest, + }); + + expect(actionsClientLlm._llmType()).toEqual('ActionsClientLlm'); + }); + }); + + describe('_call', () => { + it('returns the expected content when _call is invoked', async () => { + const actionsClientLlm = new ActionsClientLlm({ + actions: mockActions, + connectorId, + request: mockRequest, + }); + + const result = await actionsClientLlm._call(prompt); + + expect(result).toEqual('Yes, your name is Andrew. How can I assist you further, Andrew?'); + }); + + it('rejects with the expected error when the action result status is error', async () => { + const hasErrorStatus = jest.fn().mockImplementation(() => ({ + message: 'action-result-message', + serviceMessage: 'action-result-service-message', + status: 'error', // <-- error status + })); + + const badActions = { + getActionsClientWithRequest: jest.fn().mockImplementation(() => ({ + execute: hasErrorStatus, + })), + } as unknown as ActionsPluginStart; + + const actionsClientLlm = new ActionsClientLlm({ + actions: badActions, + connectorId, + request: mockRequest, + }); + + expect(actionsClientLlm._call(prompt)).rejects.toThrowError( + 'ActionsClientLlm: action result status is error: action-result-message - action-result-service-message' + ); + }); + + it('rejects with the expected error the message has invalid content', async () => { + const invalidContent = { + id: 'chatcmpl-7sFVvksgFtMUac3pY5bTypFAKaGX1', + object: 'chat.completion', + created: 1693163703, + model: 'gpt-4', + choices: [ + { + index: 0, + finish_reason: 'stop', + message: { + role: 'assistant', + content: 1234, // <-- invalid content + }, + }, + ], + usage: { completion_tokens: 16, prompt_tokens: 140, total_tokens: 156 }, + }; + + mockExecute.mockImplementation(() => ({ + data: invalidContent, + status: 'ok', + })); + + const actionsClientLlm = new ActionsClientLlm({ + actions: mockActions, + connectorId, + request: mockRequest, + }); + + expect(actionsClientLlm._call(prompt)).rejects.toThrowError( + 'ActionsClientLlm: choices[0] message content should be a string, but it had an unexpected type: number' + ); + }); + + it('rejects with the expected error when choices is empty', async () => { + const invalidContent = { + id: 'chatcmpl-7sFVvksgFtMUac3pY5bTypFAKaGX1', + object: 'chat.completion', + created: 1693163703, + model: 'gpt-4', + choices: [], // <-- empty choices + usage: { completion_tokens: 16, prompt_tokens: 140, total_tokens: 156 }, + }; + + mockExecute.mockImplementation(() => ({ + data: invalidContent, + status: 'ok', + })); + + const actionsClientLlm = new ActionsClientLlm({ + actions: mockActions, + connectorId, + request: mockRequest, + }); + + expect(actionsClientLlm._call(prompt)).rejects.toThrowError( + 'ActionsClientLlm: choices is expected to be an non-empty array' + ); + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts new file mode 100644 index 0000000000000..00d78dc6cb309 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.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 { KibanaRequest } from '@kbn/core/server'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; +import { LLM } from 'langchain/llms/base'; +import { get } from 'lodash/fp'; + +import { getMessageContentAndRole } from '../helpers'; + +const LLM_TYPE = 'ActionsClientLlm'; + +export class ActionsClientLlm extends LLM { + #actions: ActionsPluginStart; + #connectorId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #request: KibanaRequest; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #actionResultData: Record; + + constructor({ + actions, + connectorId, + request, + }: { + actions: ActionsPluginStart; + connectorId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: KibanaRequest; + }) { + super({}); + + this.#actions = actions; + this.#connectorId = connectorId; + this.#request = request; + this.#actionResultData = {}; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getActionResultData(): Record { + return this.#actionResultData; + } + + _llmType() { + return LLM_TYPE; + } + + async _call(prompt: string): Promise { + // convert the Langchain prompt to an assistant message: + const assistantMessage = getMessageContentAndRole(prompt); + + // create a new connector request body with the assistant message: + const requestBody = { + actionId: this.#connectorId, + params: { + ...this.#request.body.params, // the original request body params + subActionParams: { + ...this.#request.body.params.subActionParams, // the original request body params.subActionParams + body: JSON.stringify({ messages: [assistantMessage] }), + }, + }, + }; + + // create an actions client from the authenticated request context: + const actionsClient = await this.#actions.getActionsClientWithRequest(this.#request); + + const actionResult = await actionsClient.execute(requestBody); + + if (actionResult.status === 'error') { + throw new Error( + `${LLM_TYPE}: action result status is error: ${actionResult?.message} - ${actionResult?.serviceMessage}` + ); + } + + const choices = get('data.choices', actionResult); + + if (Array.isArray(choices) && choices.length > 0) { + // get the raw content from the first choice, because _call must return a string + const content: string | undefined = choices[0]?.message?.content; + + if (typeof content !== 'string') { + throw new Error( + `${LLM_TYPE}: choices[0] message content should be a string, but it had an unexpected type: ${typeof content}` + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.#actionResultData = actionResult.data as Record; // save the raw response from the connector, because that's what the assistant expects + + return content; // per the contact of _call, return a string + } else { + throw new Error(`${LLM_TYPE}: choices is expected to be an non-empty array`); + } + } +} diff --git a/x-pack/plugins/elastic_assistant/server/plugin.ts b/x-pack/plugins/elastic_assistant/server/plugin.ts new file mode 100755 index 0000000000000..4e277132e8da0 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/plugin.ts @@ -0,0 +1,80 @@ +/* + * 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 { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, + IContextProvider, +} from '@kbn/core/server'; + +import { + ElasticAssistantPluginSetup, + ElasticAssistantPluginSetupDependencies, + ElasticAssistantPluginStart, + ElasticAssistantPluginStartDependencies, + ElasticAssistantRequestHandlerContext, +} from './types'; +import { postActionsConnectorExecuteRoute } from './routes'; + +export class ElasticAssistantPlugin + implements + Plugin< + ElasticAssistantPluginSetup, + ElasticAssistantPluginStart, + ElasticAssistantPluginSetupDependencies, + ElasticAssistantPluginStartDependencies + > +{ + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + private createRouteHandlerContext = ( + core: CoreSetup + ): IContextProvider => { + return async function elasticAssistantRouteHandlerContext(context, request) { + const [_, pluginsStart] = await core.getStartServices(); + + return { + actions: pluginsStart.actions, + }; + }; + }; + + public setup(core: CoreSetup, plugins: ElasticAssistantPluginSetupDependencies) { + this.logger.debug('elasticAssistant: Setup'); + const router = core.http.createRouter(); + + core.http.registerRouteHandlerContext< + ElasticAssistantRequestHandlerContext, + 'elasticAssistant' + >( + 'elasticAssistant', + this.createRouteHandlerContext(core as CoreSetup) + ); + + postActionsConnectorExecuteRoute(router); + return { + actions: plugins.actions, + }; + } + + public start(core: CoreStart, plugins: ElasticAssistantPluginStartDependencies) { + this.logger.debug('elasticAssistant: Started'); + + return { + actions: plugins.actions, + }; + } + + public stop() {} +} diff --git a/x-pack/plugins/elastic_assistant/server/routes/index.ts b/x-pack/plugins/elastic_assistant/server/routes/index.ts new file mode 100644 index 0000000000000..b6a53787763a0 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/routes/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 { postActionsConnectorExecuteRoute } from './post_actions_connector_execute'; diff --git a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts new file mode 100644 index 0000000000000..a5934ffb8a7a7 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { IRouter, KibanaRequest } from '@kbn/core/server'; +import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server'; +import { BaseMessage } from 'langchain/schema'; + +import { mockActionResultData } from '../__mocks__/action_result_data'; +import { postActionsConnectorExecuteRoute } from './post_actions_connector_execute'; +import { ElasticAssistantRequestHandlerContext } from '../types'; + +jest.mock('../lib/build_response', () => ({ + buildResponse: jest.fn().mockImplementation((x) => x), +})); + +jest.mock('../lib/langchain/execute_custom_llm_chain', () => ({ + executeCustomLlmChain: jest.fn().mockImplementation( + async ({ + connectorId, + }: { + actions: ActionsPluginStart; + connectorId: string; + langChainMessages: BaseMessage[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request: KibanaRequest; + }) => { + if (connectorId === 'mock-connector-id') { + return { + connector_id: 'mock-connector-id', + data: mockActionResultData, + status: 'ok', + }; + } else { + throw new Error('simulated error'); + } + } + ), +})); + +const mockContext = { + elasticAssistant: async () => ({ + actions: jest.fn(), + }), +}; + +const mockRequest = { + params: { connectorId: 'mock-connector-id' }, + body: { + params: { + subActionParams: { + body: '{"messages":[{"role":"user","content":"\\n\\n\\n\\nWhat is my name?"},{"role":"assistant","content":"I\'m sorry, but I don\'t have the information about your name. You can tell me your name if you\'d like, and we can continue our conversation from there."},{"role":"user","content":"\\n\\nMy name is Andrew"},{"role":"assistant","content":"Hello, Andrew! It\'s nice to meet you. What would you like to talk about today?"},{"role":"user","content":"\\n\\nDo you know my name?"}]}', + }, + subAction: 'test', + }, + }, +}; + +const mockResponse = { + ok: jest.fn().mockImplementation((x) => x), + error: jest.fn().mockImplementation((x) => x), +}; + +describe('postActionsConnectorExecuteRoute', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns the expected response', async () => { + const mockRouter = { + post: jest.fn().mockImplementation(async (_, handler) => { + const result = await handler(mockContext, mockRequest, mockResponse); + + expect(result).toEqual({ + body: { + connector_id: 'mock-connector-id', + data: mockActionResultData, + status: 'ok', + }, + }); + }), + }; + + await postActionsConnectorExecuteRoute( + mockRouter as unknown as IRouter + ); + }); + + it('returns the expected error when executeCustomLlmChain fails', async () => { + const requestWithBadConnectorId = { + ...mockRequest, + params: { connectorId: 'bad-connector-id' }, + }; + + const mockRouter = { + post: jest.fn().mockImplementation(async (_, handler) => { + const result = await handler(mockContext, requestWithBadConnectorId, mockResponse); + + expect(result).toEqual({ + body: 'simulated error', + statusCode: 500, + }); + }), + }; + + await postActionsConnectorExecuteRoute( + mockRouter as unknown as IRouter + ); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts new file mode 100644 index 0000000000000..be4468587bdd9 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts @@ -0,0 +1,72 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import { transformError } from '@kbn/securitysolution-es-utils'; + +import { POST_ACTIONS_CONNECTOR_EXECUTE } from '../../common/constants'; +import { + getLangChainMessages, + unsafeGetAssistantMessagesFromRequest, +} from '../lib/langchain/helpers'; +import { buildResponse } from '../lib/build_response'; +import { buildRouteValidation } from '../schemas/common'; +import { + PostActionsConnectorExecuteBody, + PostActionsConnectorExecutePathParams, +} from '../schemas/post_actions_connector_execute'; +import { ElasticAssistantRequestHandlerContext } from '../types'; +import { executeCustomLlmChain } from '../lib/langchain/execute_custom_llm_chain'; + +export const postActionsConnectorExecuteRoute = ( + router: IRouter +) => { + router.post( + { + path: POST_ACTIONS_CONNECTOR_EXECUTE, + validate: { + body: buildRouteValidation(PostActionsConnectorExecuteBody), + params: buildRouteValidation(PostActionsConnectorExecutePathParams), + }, + }, + async (context, request, response) => { + const resp = buildResponse(response); + + try { + const connectorId = decodeURIComponent(request.params.connectorId); + const rawSubActionParamsBody = request.body.params.subActionParams.body; + + // get the actions plugin start contract from the request context: + const actions = (await context.elasticAssistant).actions; + + // get the assistant messages from the request body: + const assistantMessages = unsafeGetAssistantMessagesFromRequest(rawSubActionParamsBody); + + // convert the assistant messages to LangChain messages: + const langChainMessages = getLangChainMessages(assistantMessages); + + const langChainResponseBody = await executeCustomLlmChain({ + actions, + connectorId, + langChainMessages, + request, + }); + + return response.ok({ + body: langChainResponseBody, + }); + } catch (err) { + const error = transformError(err); + + return resp.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/elastic_assistant/server/schemas/common.ts b/x-pack/plugins/elastic_assistant/server/schemas/common.ts new file mode 100644 index 0000000000000..00e97a9326c5e --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/schemas/common.ts @@ -0,0 +1,38 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import type * as rt from 'io-ts'; +import { exactCheck, formatErrors } from '@kbn/securitysolution-io-ts-utils'; +import type { + RouteValidationFunction, + RouteValidationResultFactory, + RouteValidationError, +} from '@kbn/core/server'; + +type RequestValidationResult = + | { + value: T; + error?: undefined; + } + | { + value?: undefined; + error: RouteValidationError; + }; + +export const buildRouteValidation = + >(schema: T): RouteValidationFunction => + (inputValue: unknown, validationResult: RouteValidationResultFactory) => + pipe( + schema.decode(inputValue), + (decoded) => exactCheck(inputValue, decoded), + fold>( + (errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()), + (validatedInput: A) => validationResult.ok(validatedInput) + ) + ); diff --git a/x-pack/plugins/elastic_assistant/server/schemas/post_actions_connector_execute.ts b/x-pack/plugins/elastic_assistant/server/schemas/post_actions_connector_execute.ts new file mode 100644 index 0000000000000..0aae23ed7512d --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/schemas/post_actions_connector_execute.ts @@ -0,0 +1,27 @@ +/* + * 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 * as t from 'io-ts'; + +/** Validates the URL path of a POST request to the `/actions/connector/{connector_id}/_execute` endpoint */ +export const PostActionsConnectorExecutePathParams = t.type({ + connectorId: t.string, +}); + +/** Validates the body of a POST request to the `/actions/connector/{connector_id}/_execute` endpoint */ +export const PostActionsConnectorExecuteBody = t.type({ + params: t.type({ + subActionParams: t.type({ + body: t.string, + }), + subAction: t.string, + }), +}); + +export type PostActionsConnectorExecuteBodyInputs = t.TypeOf< + typeof PostActionsConnectorExecuteBody +>; diff --git a/x-pack/plugins/elastic_assistant/server/types.ts b/x-pack/plugins/elastic_assistant/server/types.ts new file mode 100755 index 0000000000000..cbe7e096b4eb3 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/types.ts @@ -0,0 +1,40 @@ +/* + * 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 { + PluginSetupContract as ActionsPluginSetup, + PluginStartContract as ActionsPluginStart, +} from '@kbn/actions-plugin/server'; +import { CustomRequestHandlerContext } from '@kbn/core/server'; + +/** The plugin setup interface */ +export interface ElasticAssistantPluginSetup { + actions: ActionsPluginSetup; +} + +/** The plugin start interface */ +export interface ElasticAssistantPluginStart { + actions: ActionsPluginStart; +} + +export interface ElasticAssistantPluginSetupDependencies { + actions: ActionsPluginSetup; +} +export interface ElasticAssistantPluginStartDependencies { + actions: ActionsPluginStart; +} + +export interface ElasticAssistantApiRequestHandlerContext { + actions: ActionsPluginStart; +} + +/** + * @internal + */ +export type ElasticAssistantRequestHandlerContext = CustomRequestHandlerContext<{ + elasticAssistant: ElasticAssistantApiRequestHandlerContext; +}>; diff --git a/x-pack/plugins/elastic_assistant/tsconfig.json b/x-pack/plugins/elastic_assistant/tsconfig.json new file mode 100644 index 0000000000000..99119202376ec --- /dev/null +++ b/x-pack/plugins/elastic_assistant/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + }, + "include": [ + "common/**/*", + "server/lib/**/*", + "server/**/*", + // must declare *.json explicitly per https://github.com/microsoft/TypeScript/issues/25636 + "server/**/*.json", + "../../../typings/**/*" + ], + "kbn_references": [ + "@kbn/core", + "@kbn/core-http-server", + "@kbn/licensing-plugin", + "@kbn/core-http-request-handler-context-server", + "@kbn/securitysolution-es-utils", + "@kbn/securitysolution-io-ts-utils", + "@kbn/actions-plugin", + "@kbn/elastic-assistant", + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/x-pack/plugins/security_solution/kibana.jsonc b/x-pack/plugins/security_solution/kibana.jsonc index f4716adfca310..91f366b5dd94f 100644 --- a/x-pack/plugins/security_solution/kibana.jsonc +++ b/x-pack/plugins/security_solution/kibana.jsonc @@ -22,6 +22,7 @@ "dataViews", "discover", "ecsDataQualityDashboard", + "elasticAssistant", "embeddable", "eventLog", "features", diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index 3e172207fb4bd..8bbf2c3a425ec 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -99,6 +99,7 @@ const StartAppComponent: FC = ({ actionTypeRegistry={actionTypeRegistry} augmentMessageCodeBlocks={augmentMessageCodeBlocks} assistantAvailability={assistantAvailability} + assistantLangChain={false} assistantTelemetry={assistantTelemetry} defaultAllow={defaultAllow} defaultAllowReplacement={defaultAllowReplacement} diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx b/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx index f36b776b72b04..90c64ec496c78 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/mock_assistant_provider.tsx @@ -34,6 +34,7 @@ export const MockAssistantProviderComponent: React.FC = ({ children }) => [])} baseAllow={[]} baseAllowReplacement={[]} diff --git a/yarn.lock b/yarn.lock index b3a342acb0eef..7264182e33b4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,20 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" +"@anthropic-ai/sdk@^0.5.7": + version "0.5.10" + resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.5.10.tgz#8cd0b68ac32c71e579b466a89ea30338f2165a32" + integrity sha512-P8xrIuTUO/6wDzcjQRUROXp4WSqtngbXaE4GpEu0PhEmnq/1Q8vbF1s0o7W07EV3j8zzRoyJxAKovUJtNXH7ew== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + digest-fetch "^1.3.0" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + "@apidevtools/json-schema-ref-parser@^9.0.6": version "9.0.9" resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz#d720f9256e3609621280584f2b47ae165359268b" @@ -4250,6 +4264,10 @@ version "0.0.0" uid "" +"@kbn/elastic-assistant-plugin@link:x-pack/plugins/elastic_assistant": + version "0.0.0" + uid "" + "@kbn/elastic-assistant@link:x-pack/packages/kbn-elastic-assistant": version "0.0.0" uid "" @@ -9245,7 +9263,7 @@ dependencies: "@types/node" "*" -"@types/node-fetch@2.6.4": +"@types/node-fetch@2.6.4", "@types/node-fetch@^2.6.4": version "2.6.4" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" integrity sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg== @@ -9275,7 +9293,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@18.17.1", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0", "@types/node@^14.14.31": +"@types/node@*", "@types/node@18.17.1", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0", "@types/node@^14.14.31", "@types/node@^18.11.18": version "18.17.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.1.tgz#84c32903bf3a09f7878c391d31ff08f6fe7d8335" integrity sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw== @@ -9609,7 +9627,7 @@ dependencies: "@types/node" "*" -"@types/retry@^0.12.0": +"@types/retry@0.12.0", "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== @@ -9828,6 +9846,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== +"@types/uuid@^9.0.1": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b" + integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ== + "@types/vfile-message@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5" @@ -10409,6 +10432,13 @@ abbrev@1, abbrev@^1.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -11569,12 +11599,17 @@ balanced-match@^2.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== + base64-js@1.3.1, base64-js@^1.0.2, base64-js@^1.2.0, base64-js@^1.3.0, base64-js@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64-js@^1.1.2: +base64-js@^1.1.2, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -11667,7 +11702,12 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -binary-search@^1.3.3: +binary-extensions@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +binary-search@^1.3.3, binary-search@^1.3.5: version "1.3.6" resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== @@ -12291,6 +12331,11 @@ camelcase@5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA== +camelcase@6: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + camelcase@^2.0.0, camelcase@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -12479,7 +12524,7 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -charenc@~0.0.1: +charenc@0.0.2, charenc@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= @@ -13405,7 +13450,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypt@~0.0.1: +crypt@0.0.2, crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= @@ -14648,6 +14693,14 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +digest-fetch@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/digest-fetch/-/digest-fetch-1.3.0.tgz#898e69264d00012a23cf26e8a3e40320143fc661" + integrity sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA== + dependencies: + base-64 "^0.1.0" + md5 "^2.3.0" + dir-glob@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" @@ -15945,6 +15998,11 @@ event-emitter@^0.3.5, event-emitter@~0.3.5: d "1" es5-ext "~0.10.14" +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter-asyncresource@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" @@ -15955,7 +16013,7 @@ eventemitter2@6.4.7: resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.7.tgz#a7f6c4d7abf28a14c1ef3442f21cb306a054271d" integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -16123,6 +16181,11 @@ expose-loader@^0.7.5: resolved "https://registry.yarnpkg.com/expose-loader/-/expose-loader-0.7.5.tgz#e29ea2d9aeeed3254a3faa1b35f502db9f9c3f6f" integrity sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw== +expr-eval@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expr-eval/-/expr-eval-2.0.2.tgz#fa6f044a7b0c93fde830954eb9c5b0f7fbc7e201" + integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg== + express@^4.17.1, express@^4.17.3: version "4.17.3" resolved "https://registry.yarnpkg.com/express/-/express-4.17.3.tgz#f6c7302194a4fb54271b73a1fe7a06478c8f85a1" @@ -16780,6 +16843,11 @@ fork-ts-checker-webpack-plugin@^6.0.4: semver "^7.3.2" tapable "^1.0.0" +form-data-encoder@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" + integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -16812,6 +16880,14 @@ format@^0.2.0: resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= +formdata-node@^4.3.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" + integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.3" + formdata-polyfill@^4.0.10: version "4.0.10" resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" @@ -18647,6 +18723,11 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" +is-any-array@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-any-array/-/is-any-array-2.0.1.tgz#9233242a9c098220290aa2ec28f82ca7fa79899e" + integrity sha512-UtilS7hLRu++wb/WBAw9bNuP1Eg04Ivn1vERJck8zJthEvXCBEBpGR/33u/xLKWEQf95803oalHrVDptcAvFdQ== + is-arguments@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" @@ -18690,7 +18771,7 @@ is-boolean-object@^1.0.1, is-boolean-object@^1.1.0: dependencies: call-bind "^1.0.0" -is-buffer@^1.1.5, is-buffer@~1.1.1: +is-buffer@^1.1.5, is-buffer@~1.1.1, is-buffer@~1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -20091,6 +20172,13 @@ js-string-escape@^1.0.1: resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8= +js-tiktoken@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.7.tgz#56933fcd2093e8304060dfde3071bda91812e6f5" + integrity sha512-biba8u/clw7iesNEWLOLwrNGoBP2lA+hTaBLs/D45pJdUPFXyxD6nhcDVtADChghv4GgyAiMKYMiRx7x6h7Biw== + dependencies: + base64-js "^1.5.1" + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -20287,6 +20375,11 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= +jsonpointer@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== + jsonwebtoken@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" @@ -20456,6 +20549,44 @@ kuler@^2.0.0: resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== +langchain@^0.0.132: + version "0.0.132" + resolved "https://registry.yarnpkg.com/langchain/-/langchain-0.0.132.tgz#2cdcc5d7078c70aa403f7eaeff3556c50a485632" + integrity sha512-gXnuiAhsQQqXheKQiaSmFa9s3S/Yhkkb9OCytu04OE0ecttvVvfjjqIoNVS9vor8V7kRUgYPKHJsMz2UFDoJNw== + dependencies: + "@anthropic-ai/sdk" "^0.5.7" + ansi-styles "^5.0.0" + binary-extensions "^2.2.0" + camelcase "6" + decamelize "^1.2.0" + expr-eval "^2.0.2" + flat "^5.0.2" + js-tiktoken "^1.0.7" + js-yaml "^4.1.0" + jsonpointer "^5.0.1" + langsmith "~0.0.16" + ml-distance "^4.0.0" + object-hash "^3.0.0" + openai "^3.3.0" + openapi-types "^12.1.3" + p-queue "^6.6.2" + p-retry "4" + uuid "^9.0.0" + yaml "^2.2.1" + zod "^3.21.4" + zod-to-json-schema "^3.20.4" + +langsmith@~0.0.16: + version "0.0.26" + resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.0.26.tgz#a63f911a3113860de5488392a46468d1b482e3ef" + integrity sha512-TecBjdgYGMxNaWp2L2X0OVgu8lge2WeQ5UpDXluwF3x+kH/WHFVSuR1RCuP+k2628GSVFvXxVIyXvzrHYxrZSw== + dependencies: + "@types/uuid" "^9.0.1" + commander "^10.0.1" + p-queue "^6.6.2" + p-retry "4" + uuid "^9.0.0" + language-subtag-registry@~0.3.2: version "0.3.21" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz#04ac218bea46f04cb039084602c6da9e788dd45a" @@ -21341,6 +21472,15 @@ md5@^2.1.0: crypt "~0.0.1" is-buffer "~1.1.1" +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + mdast-squeeze-paragraphs@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-4.0.0.tgz#7c4c114679c3bee27ef10b58e2e015be79f1ef97" @@ -21991,6 +22131,42 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +ml-array-mean@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/ml-array-mean/-/ml-array-mean-1.1.6.tgz#d951a700dc8e3a17b3e0a583c2c64abd0c619c56" + integrity sha512-MIdf7Zc8HznwIisyiJGRH9tRigg3Yf4FldW8DxKxpCCv/g5CafTw0RRu51nojVEOXuCQC7DRVVu5c7XXO/5joQ== + dependencies: + ml-array-sum "^1.1.6" + +ml-array-sum@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/ml-array-sum/-/ml-array-sum-1.1.6.tgz#d1d89c20793cd29c37b09d40e85681aa4515a955" + integrity sha512-29mAh2GwH7ZmiRnup4UyibQZB9+ZLyMShvt4cH4eTK+cL2oEMIZFnSyB3SS8MlsTh6q/w/yh48KmqLxmovN4Dw== + dependencies: + is-any-array "^2.0.0" + +ml-distance-euclidean@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ml-distance-euclidean/-/ml-distance-euclidean-2.0.0.tgz#3a668d236649d1b8fec96380b9435c6f42c9a817" + integrity sha512-yC9/2o8QF0A3m/0IXqCTXCzz2pNEzvmcE/9HFKOZGnTjatvBbsn4lWYJkxENkA4Ug2fnYl7PXQxnPi21sgMy/Q== + +ml-distance@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/ml-distance/-/ml-distance-4.0.1.tgz#4741d17a1735888c5388823762271dfe604bd019" + integrity sha512-feZ5ziXs01zhyFUUUeZV5hwc0f5JW0Sh0ckU1koZe/wdVkJdGxcP06KNQuF0WBTj8FttQUzcvQcpcrOp/XrlEw== + dependencies: + ml-array-mean "^1.1.6" + ml-distance-euclidean "^2.0.0" + ml-tree-similarity "^1.0.0" + +ml-tree-similarity@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ml-tree-similarity/-/ml-tree-similarity-1.0.0.tgz#24705a107e32829e24d945e87219e892159c53f0" + integrity sha512-XJUyYqjSuUQkNQHMscr6tcjldsOoAekxADTplt40QKfwW6nd++1wHWV9AArl0Zvw/TIHgNaZZNvr8QGvE8wLRg== + dependencies: + binary-search "^1.3.5" + num-sort "^2.0.0" + mocha-junit-reporter@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.0.2.tgz#d521689b651dc52f52044739f8ffb368be415731" @@ -22458,7 +22634,7 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-domexception@^1.0.0: +node-domexception@1.0.0, node-domexception@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== @@ -22764,6 +22940,11 @@ null-loader@^3.0.0: loader-utils "^1.2.3" schema-utils "^1.0.0" +num-sort@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/num-sort/-/num-sort-2.1.0.tgz#1cbb37aed071329fdf41151258bc011898577a9b" + integrity sha512-1MQz1Ed8z2yckoBeSfkQHHO9K1yDRxxtotKSJ9yvcTUUxSvfvzEq5GwBrjjHEpMlq/k5gvXdmJ1SbYxWtpNoVg== + num2fraction@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" @@ -22841,6 +23022,11 @@ object-hash@^1.3.0, object-hash@^1.3.1: resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-identity-map@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/object-identity-map/-/object-identity-map-1.0.2.tgz#2b4213a4285ca3a8cd2e696782c9964f887524e7" @@ -23047,6 +23233,11 @@ openapi-types@^10.0.0: resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-10.0.0.tgz#0debbf663b2feed0322030b5b7c9080804076934" integrity sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ== +openapi-types@^12.1.3: + version "12.1.3" + resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3" + integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== + opener@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -23274,11 +23465,27 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + p-reflect@2.1.0, p-reflect@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-reflect/-/p-reflect-2.1.0.tgz#5d67c7b3c577c4e780b9451fc9129675bd99fe67" integrity sha512-paHV8NUz8zDHu5lhr/ngGWQiW067DK/+IbJ+RfZ4k+s8y4EKyYCz8pGYWjxCg35eHztpJAt+NUgvN4L+GCbPlg== +p-retry@4: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + p-retry@^4.2.0, p-retry@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.5.0.tgz#6685336b3672f9ee8174d3769a660cb5e488521d" @@ -23302,6 +23509,13 @@ p-timeout@^2.0.1: dependencies: p-finally "^1.0.0" +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -26201,6 +26415,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -30062,11 +30281,21 @@ web-namespaces@^1.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +web-streams-polyfill@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" + integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== + web-streams-polyfill@^3.0.3, web-streams-polyfill@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965" integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA== +web-streams-polyfill@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + web-streams-polyfill@~3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.0.3.tgz#f49e487eedeca47a207c1aee41ee5578f884b42f" @@ -30726,7 +30955,7 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.2.2: +yaml@^2.2.1, yaml@^2.2.2: version "2.3.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== @@ -30886,6 +31115,11 @@ zip-stream@^4.1.0: compress-commons "^4.1.0" readable-stream "^3.6.0" +zod-to-json-schema@^3.20.4: + version "3.21.4" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.21.4.tgz#de97c5b6d4a25e9d444618486cb55c0c7fb949fd" + integrity sha512-fjUZh4nQ1s6HMccgIeE0VP4QG/YRGPmyjO9sAh890aQKPEk3nqbfUXhMFaC+Dr5KvYBm8BCyvfpZf2jY9aGSsw== + zod@^3.21.4: version "3.21.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"