[Security Solution] [Elastic AI Assistant] LangChain integration (experimental) (#164908)

## [Security Solution] [Elastic AI Assistant] LangChain integration (experimental)

This PR integrates [LangChain](https://www.langchain.com/) with the [Elastic AI Assistant](https://www.elastic.co/blog/introducing-elastic-ai-assistant) as an experimental, alternative execution path.

### How it works

- There are virtually no client side changes to the assistant, apart from a new branch in `x-pack/packages/kbn-elastic-assistant/impl/assistant/api.tsx` that chooses a path based on the value of the `assistantLangChain` flag:

```typescript
    const path = assistantLangChain
      ? `/internal/elastic_assistant/actions/connector/${apiConfig?.connectorId}/_execute`
      : `/api/actions/connector/${apiConfig?.connectorId}/_execute`;
```

Execution of the LangChain chain happens server-side. The new route still executes the request via the `connectorId` in the route, but the connector won't execute the request exactly as it was sent by the client. Instead, the connector will execute one (or more) prompts that are generated by LangChain.

Requests routed to `/internal/elastic_assistant/actions/connector/${apiConfig?.connectorId}/_execute` will be processed by a new Kibana plugin located in:

```
x-pack/plugins/elastic_assistant
```

- Requests are processed in the `postActionsConnectorExecuteRoute` handler in `x-pack/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts`.

The `postActionsConnectorExecuteRoute` route handler:

1. Extracts the chat messages sent by the assistant
2. Converts the extracted messages to the format expected by LangChain
3. Passes the converted messages to `executeCustomLlmChain`

- The `executeCustomLlmChain` function in `x-pack/plugins/elastic_assistant/server/lib/langchain/execute_custom_llm_chain/index.ts`:

1. Splits the messages into `pastMessages` and `latestMessage`, where the latter contains only the last message sent by the user
2. Wraps the conversation history in the `BufferMemory` LangChain abstraction
3. Executes the chain, kicking it off with `latestMessage`

```typescript
  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
};
```

- When LangChain executes the chain, it will invoke `ActionsClientLlm`'s `_call` function in `x-pack/plugins/elastic_assistant/server/lib/langchain/llm/actions_client_llm.ts` one or more times.

The `_call` function's signature is defined by LangChain:

```
async _call(prompt: string): Promise<string>
```

- The contents of `prompt` are completely determined by LangChain.
- The string returned by the promise is the "answer" from the LLM

The `ActionsClientLlm` extends LangChain's LLM interface:

```typescript
export class ActionsClientLlm extends LLM
```

This let's us do additional "work" in the `_call` function:

1. Create a new assistant message using the contents of the `prompt` (`string`) argument to `_call`
2. Create a request body in the format expected by the connector
3. Create an actions client from the authenticated request context
4. Execute the actions client with the request body
5. Save the raw response from the connector, because that's what the assistant expects
6. Return the result as a plain string, as per the contact of `_call`

## Desk testing

This experimental LangChain integration may NOT be enabled via a feature flag (yet).

Set

```typescript
assistantLangChain={true}
```

in `x-pack/plugins/security_solution/public/app/app.tsx` to enable this experimental feature in development environments.
This commit is contained in:
Andrew Macri 2023-08-28 10:30:05 -06:00 committed by GitHub
parent 31e95574ae
commit 3935548f36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1954 additions and 36 deletions

View file

@ -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}',

1
.github/CODEOWNERS vendored
View file

@ -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

View file

@ -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.
|<<enhanced-embeddables-plugin>>
|Enhances Embeddables by registering a custom factory provider. The enhanced factory provider
adds dynamic actions to every embeddables state, in order to support drilldowns.

View file

@ -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",

View file

@ -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

View file

@ -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: [
'<rootDir>/packages/kbn-test/src/jest/setup/after_env.integration.js',
'<rootDir>/packages/kbn-test/src/jest/setup/mocks.moment_timezone.js',

View file

@ -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

View file

@ -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"
]
}
}
}

View file

@ -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');
});
});

View file

@ -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<any>(
`/api/actions/connector/${apiConfig?.connectorId}/_execute`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
signal,
}
);
const response = await http.fetch<any>(path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
signal,
});
const data = response.data;
if (response.status !== 'ok') {

View file

@ -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 };
};

View file

@ -28,6 +28,7 @@ const ContextWrapper: React.FC = ({ children }) => (
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -49,6 +49,7 @@ type ShowAssistantOverlay = ({
export interface AssistantProviderProps {
actionTypeRegistry: ActionTypeRegistryContract;
assistantAvailability: AssistantAvailability;
assistantLangChain: boolean;
assistantTelemetry?: AssistantTelemetry;
augmentMessageCodeBlocks: (currentConversation: Conversation) => 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<DocLinksStart, 'links'>;
@ -129,6 +131,7 @@ const AssistantContext = React.createContext<UseAssistantContext | undefined>(un
export const AssistantProvider: React.FC<AssistantProviderProps> = ({
actionTypeRegistry,
assistantAvailability,
assistantLangChain,
assistantTelemetry,
augmentMessageCodeBlocks,
baseAllow,
@ -248,6 +251,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
() => ({
actionTypeRegistry,
assistantAvailability,
assistantLangChain,
assistantTelemetry,
augmentMessageCodeBlocks,
allQuickPrompts: localStorageQuickPrompts ?? [],
@ -284,6 +288,7 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
[
actionTypeRegistry,
assistantAvailability,
assistantLangChain,
assistantTelemetry,
augmentMessageCodeBlocks,
baseAllow,

View file

@ -66,6 +66,7 @@ export const TestProvidersComponent: React.FC<Props> = ({
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={assistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn().mockReturnValue([])}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -46,6 +46,7 @@ export const TestProvidersComponent: React.FC<Props> = ({ children, isILMAvailab
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn()}
baseAllow={[]}
baseAllowReplacement={[]}

View file

@ -0,0 +1,9 @@
# Elastic AI Assistant
This plugin implements (only) server APIs for the `Elastic AI Assistant`.
This plugin does NOT contain UI components. See `x-pack/packages/kbn-elastic-assistant` for React components.
## Maintainers
Maintained by the Security Solution team

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const PLUGIN_ID = 'elasticAssistant';
export const PLUGIN_NAME = 'elasticAssistant';
export const BASE_PATH = '/internal/elastic_assistant';
export const POST_ACTIONS_CONNECTOR_EXECUTE = `${BASE_PATH}/actions/connector/{connectorId}/_execute`;

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
module.exports = {
collectCoverageFrom: [
'<rootDir>/x-pack/plugins/elastic_assistant/{common,lib,server}/**/*.{ts,tsx}',
],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/elastic_assistant',
coverageReporters: ['text', 'html'],
rootDir: '../../..',
roots: ['<rootDir>/x-pack/plugins/elastic_assistant'],
preset: '@kbn/test',
};

View file

@ -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"
]
}
}

View file

@ -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 },
};

View file

@ -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?'),
];

View file

@ -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,
};

View file

@ -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<typeof createMockClients>;
const convertRequestContextMock = <T>(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,
};

View file

@ -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,
};

View file

@ -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<unknown, unknown, unknown, 'get' | 'post' | 'delete' | 'patch' | 'put'>;
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,
};

View file

@ -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<typeof responseMock.create>;
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<Response['calls']>((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,
};
};

View file

@ -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';

View file

@ -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<T>({ statusCode, body, headers }: CustomHttpResponseOptions<T>) {
const contentType: CustomHttpResponseOptions<T>['headers'] = {
'content-type': 'application/json',
};
const defaultedHeaders: CustomHttpResponseOptions<T>['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);

View file

@ -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<unknown, unknown, any, any> = {} 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',
});
});
});

View file

@ -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<unknown, unknown, any, any>;
}): Promise<ResponseBody> => {
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;
};

View file

@ -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<Message, 'content' | 'role'>, 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<Pick<Message, 'content' | 'role'>> = [
{
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<Message, 'content' | 'role'>]> = [
['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([]);
});
});
});

View file

@ -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<Message, 'content' | 'role'>
): 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<Pick<Message, 'content' | 'role'>>
): BaseMessage[] => assistantMessages.map(getLangChainMessage);
export const getMessageContentAndRole = (prompt: string): Pick<Message, 'content' | 'role'> => ({
content: prompt,
role: 'user',
});
export interface ResponseBody {
status: string;
data: Record<string, unknown>;
connector_id: string;
}
/** An unsafe, temporary stub that parses assistant messages from the request with no validation */
export const unsafeGetAssistantMessagesFromRequest = (
rawSubActionParamsBody: string | undefined
): Array<Pick<Message, 'content' | 'role'>> => {
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 [];
}
};

View file

@ -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<unknown, unknown, any, any> = {
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<unknown, unknown, any, any>;
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'
);
});
});
});

View file

@ -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<unknown, unknown, any, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#actionResultData: Record<string, any>;
constructor({
actions,
connectorId,
request,
}: {
actions: ActionsPluginStart;
connectorId: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request: KibanaRequest<unknown, unknown, any, any>;
}) {
super({});
this.#actions = actions;
this.#connectorId = connectorId;
this.#request = request;
this.#actionResultData = {};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getActionResultData(): Record<string, any> {
return this.#actionResultData;
}
_llmType() {
return LLM_TYPE;
}
async _call(prompt: string): Promise<string> {
// 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<string, any>; // 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`);
}
}
}

View file

@ -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<ElasticAssistantPluginStart, unknown>
): IContextProvider<ElasticAssistantRequestHandlerContext, 'elasticAssistant'> => {
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<ElasticAssistantRequestHandlerContext>();
core.http.registerRouteHandlerContext<
ElasticAssistantRequestHandlerContext,
'elasticAssistant'
>(
'elasticAssistant',
this.createRouteHandlerContext(core as CoreSetup<ElasticAssistantPluginStart, unknown>)
);
postActionsConnectorExecuteRoute(router);
return {
actions: plugins.actions,
};
}
public start(core: CoreStart, plugins: ElasticAssistantPluginStartDependencies) {
this.logger.debug('elasticAssistant: Started');
return {
actions: plugins.actions,
};
}
public stop() {}
}

View file

@ -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';

View file

@ -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<unknown, unknown, any, any>;
}) => {
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<ElasticAssistantRequestHandlerContext>
);
});
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<ElasticAssistantRequestHandlerContext>
);
});
});

View file

@ -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<ElasticAssistantRequestHandlerContext>
) => {
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,
});
}
}
);
};

View file

@ -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<T> =
| {
value: T;
error?: undefined;
}
| {
value?: undefined;
error: RouteValidationError;
};
export const buildRouteValidation =
<T extends rt.Mixed, A = rt.TypeOf<T>>(schema: T): RouteValidationFunction<A> =>
(inputValue: unknown, validationResult: RouteValidationResultFactory) =>
pipe(
schema.decode(inputValue),
(decoded) => exactCheck(inputValue, decoded),
fold<rt.Errors, A, RequestValidationResult<A>>(
(errors: rt.Errors) => validationResult.badRequest(formatErrors(errors).join()),
(validatedInput: A) => validationResult.ok(validatedInput)
)
);

View file

@ -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
>;

View file

@ -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;
}>;

View file

@ -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/**/*",
]
}

View file

@ -22,6 +22,7 @@
"dataViews",
"discover",
"ecsDataQualityDashboard",
"elasticAssistant",
"embeddable",
"eventLog",
"features",

View file

@ -99,6 +99,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
actionTypeRegistry={actionTypeRegistry}
augmentMessageCodeBlocks={augmentMessageCodeBlocks}
assistantAvailability={assistantAvailability}
assistantLangChain={false}
assistantTelemetry={assistantTelemetry}
defaultAllow={defaultAllow}
defaultAllowReplacement={defaultAllowReplacement}

View file

@ -34,6 +34,7 @@ export const MockAssistantProviderComponent: React.FC<Props> = ({ children }) =>
<AssistantProvider
actionTypeRegistry={actionTypeRegistry}
assistantAvailability={mockAssistantAvailability}
assistantLangChain={false}
augmentMessageCodeBlocks={jest.fn(() => [])}
baseAllow={[]}
baseAllowReplacement={[]}

256
yarn.lock
View file

@ -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"