mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] Security AI Assistant persistent storage. (#173487)
## Summary This PR including both new APIs and client side changes to use data stream for Security Solution AI Assistant conversations persistence storage. Issue https://github.com/elastic/security-team/issues/7810 ## Extended description #### elastic-assistant plugin All API changes are introduced in elastic-assistant plugin server - `RequestContextFactory ` - this class helps to provide the needed context for each API request for routes handler context. - `AIAssistantService` - This service instance is created on the plugin setup and included to the request context factory. It is responsible for the needed conversations storage resources initialization and installation. It uses DataStreamAdapter from `packages/kbn-data-stream-adapter`. Conversations fieldMap definition [here](https://github.com/elastic/kibana/pull/173487/files#diff-c4fdbd4023c6ebc0c0bb04a32314ce8ea614f2d0916afac5e366a71122687d54) - `AIAssistantConversationsDataClient` - data client which has a set of methods to interact with conversation storage on behalf of the current user and space. - `ConversationDataWriter` - is a helper class which implements a bulk method to interact with esClient - Added new routes using versioned router and o[penAPI code generator](https://github.com/elastic/kibana/blob/main/packages/kbn-openapi-generator/README.md) schemas: `createConversationRoute` `readConversationRoute` `updateConversationRoute` `deleteConversationRoute` `appendMessagesRoute` `findUserConversationsRoute` `bulkActionsRoute` - Migrated existing `knowledge_base`, `evaluate` and `post_actions_connector_execute` routes to versioned routing and openAPI code generator schemas. #### kbn-elastic-assistant package - removed local storage persistency logic for assistantConversations. - added API requests definition to communicate to server side. - #### kbn-elastic-assistant-common package - Changed `transformsRowData` function to use async add replacements API. - Exposed routing URLs with constants file to be available for server and client. #### security_solution plugin - Added `migrateConversationsFromLocalStorage` for existing conversations in the local storage. This migration happening only for the first time when user doesn't have any conversations persisted in the current space. After mirgation complete, the old local storage key `securitySolution.assistantConversation` will be removed. - Passing security related `baseConversation` as a property to `ElasticAssistantProvider` - Changed `useAssistantTelemetry` to fetch information about the conversation from the conversations API - Modified `useConversationStore` to fetch the data from the conversations API `/api/elastic_assistant/conversations/current_user/_find` and merge with security predefined `baseConversations` if they are not used(persisted) yet. - Extracted `AssistantTab` to a separate lazy loaded file to avoid unnecessary rendering/requests till this tab will be shown in Timeline. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
This commit is contained in:
parent
1bc8c16574
commit
0631172a68
232 changed files with 17152 additions and 3084 deletions
26
x-pack/packages/kbn-elastic-assistant-common/constants.ts
Executable file
26
x-pack/packages/kbn-elastic-assistant-common/constants.ts
Executable file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION = '2023-10-31';
|
||||
export const ELASTIC_AI_ASSISTANT_INTERNAL_API_VERSION = '1';
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_URL = '/api/elastic_assistant';
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL = `${ELASTIC_AI_ASSISTANT_URL}/current_user/conversations`;
|
||||
export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/{id}`;
|
||||
export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID_MESSAGES = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BY_ID}/messages`;
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/_bulk_action`;
|
||||
export const ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND = `${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/_find`;
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_PROMPTS_URL = `${ELASTIC_AI_ASSISTANT_URL}/prompts`;
|
||||
export const ELASTIC_AI_ASSISTANT_PROMPTS_URL_BULK_ACTION = `${ELASTIC_AI_ASSISTANT_PROMPTS_URL}/_bulk_action`;
|
||||
export const ELASTIC_AI_ASSISTANT_PROMPTS_URL_FIND = `${ELASTIC_AI_ASSISTANT_PROMPTS_URL}/_find`;
|
||||
|
||||
export const ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL = `${ELASTIC_AI_ASSISTANT_URL}/anonymization_fields`;
|
||||
export const ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_BULK_ACTION = `${ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL}/_bulk_action`;
|
||||
export const ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL_FIND = `${ELASTIC_AI_ASSISTANT_ANONYMIZATION_FIELDS_URL}/_find`;
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacement } from '../../schemas';
|
||||
|
||||
export const getIsDataAnonymizable = (rawData: string | Record<string, string[]>): boolean =>
|
||||
typeof rawData !== 'string';
|
||||
|
||||
|
@ -21,3 +23,31 @@ export const isAnonymized = ({
|
|||
allowReplacementSet: Set<string>;
|
||||
field: string;
|
||||
}): boolean => allowReplacementSet.has(field);
|
||||
|
||||
export const replaceAnonymizedValuesWithOriginalValues = ({
|
||||
messageContent,
|
||||
replacements,
|
||||
}: {
|
||||
messageContent: string;
|
||||
replacements: Replacement[];
|
||||
}): string =>
|
||||
replacements != null
|
||||
? replacements.reduce((acc, replacement) => {
|
||||
const value = replacement.value;
|
||||
return replacement.uuid && value ? acc.replaceAll(replacement.uuid, value) : acc;
|
||||
}, messageContent)
|
||||
: messageContent;
|
||||
|
||||
export const replaceOriginalValuesWithUuidValues = ({
|
||||
messageContent,
|
||||
replacements,
|
||||
}: {
|
||||
messageContent: string;
|
||||
replacements: Replacement[];
|
||||
}): string =>
|
||||
replacements != null
|
||||
? replacements.reduce((acc, replacement) => {
|
||||
const value = replacement.value;
|
||||
return replacement.uuid && value ? acc.replaceAll(value, replacement.uuid) : acc;
|
||||
}, messageContent)
|
||||
: messageContent;
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('transformRawData', () => {
|
|||
const result = transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements: () => {},
|
||||
rawData: inputRawData.rawData,
|
||||
|
@ -42,13 +42,13 @@ describe('transformRawData', () => {
|
|||
transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements,
|
||||
rawData: inputRawData.rawData,
|
||||
});
|
||||
|
||||
expect(onNewReplacements).toHaveBeenCalledWith({ '1eulav': 'value1' });
|
||||
expect(onNewReplacements).toHaveBeenCalledWith([{ uuid: '1eulav', value: 'value1' }]);
|
||||
});
|
||||
|
||||
it('returns the expected mix of anonymized and non-anonymized data as a CSV string', () => {
|
||||
|
@ -62,7 +62,7 @@ describe('transformRawData', () => {
|
|||
const result = transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements: () => {},
|
||||
rawData: inputRawData.rawData,
|
||||
|
@ -86,7 +86,7 @@ describe('transformRawData', () => {
|
|||
const result = transformRawData({
|
||||
allow: inputRawData.allow,
|
||||
allowReplacement: inputRawData.allowReplacement,
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
onNewReplacements: () => {},
|
||||
rawData: inputRawData.rawData,
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Replacement } from '../../schemas';
|
||||
import { getAnonymizedData } from '../get_anonymized_data';
|
||||
import { getAnonymizedValues } from '../get_anonymized_values';
|
||||
import { getCsvFromData } from '../get_csv_from_data';
|
||||
|
@ -19,7 +20,7 @@ export const transformRawData = ({
|
|||
}: {
|
||||
allow: string[];
|
||||
allowReplacement: string[];
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacement[] | undefined;
|
||||
getAnonymizedValue: ({
|
||||
currentReplacements,
|
||||
rawValue,
|
||||
|
@ -27,7 +28,7 @@ export const transformRawData = ({
|
|||
currentReplacements: Record<string, string> | undefined;
|
||||
rawValue: string;
|
||||
}) => string;
|
||||
onNewReplacements?: (replacements: Record<string, string>) => void;
|
||||
onNewReplacements?: (replacements: Replacement[]) => void;
|
||||
rawData: string | Record<string, unknown[]>;
|
||||
}): string => {
|
||||
if (typeof rawData === 'string') {
|
||||
|
@ -37,14 +38,22 @@ export const transformRawData = ({
|
|||
const anonymizedData = getAnonymizedData({
|
||||
allow,
|
||||
allowReplacement,
|
||||
currentReplacements,
|
||||
currentReplacements: currentReplacements?.reduce((acc: Record<string, string>, r) => {
|
||||
acc[r.uuid] = r.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
rawData,
|
||||
getAnonymizedValue,
|
||||
getAnonymizedValues,
|
||||
});
|
||||
|
||||
if (onNewReplacements != null) {
|
||||
onNewReplacements(anonymizedData.replacements);
|
||||
onNewReplacements(
|
||||
Object.keys(anonymizedData.replacements).map((key) => ({
|
||||
uuid: key,
|
||||
value: anonymizedData.replacements[key],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return getCsvFromData(anonymizedData.anonymizedData);
|
||||
|
|
|
@ -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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Execute Connector API endpoint
|
||||
* version: 1
|
||||
*/
|
||||
|
||||
import { UUID, Replacement } from '../conversations/common_attributes.gen';
|
||||
|
||||
export type ExecuteConnectorRequestParams = z.infer<typeof ExecuteConnectorRequestParams>;
|
||||
export const ExecuteConnectorRequestParams = z.object({
|
||||
/**
|
||||
* The connector's `id` value.
|
||||
*/
|
||||
connectorId: z.string(),
|
||||
});
|
||||
export type ExecuteConnectorRequestParamsInput = z.input<typeof ExecuteConnectorRequestParams>;
|
||||
|
||||
export type ExecuteConnectorRequestBody = z.infer<typeof ExecuteConnectorRequestBody>;
|
||||
export const ExecuteConnectorRequestBody = z.object({
|
||||
conversationId: UUID.optional(),
|
||||
message: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
subAction: z.enum(['invokeAI', 'invokeStream']),
|
||||
alertsIndexPattern: z.string().optional(),
|
||||
allow: z.array(z.string()).optional(),
|
||||
allowReplacement: z.array(z.string()).optional(),
|
||||
isEnabledKnowledgeBase: z.boolean().optional(),
|
||||
isEnabledRAGAlerts: z.boolean().optional(),
|
||||
replacements: z.array(Replacement),
|
||||
size: z.number().optional(),
|
||||
llmType: z.enum(['bedrock', 'openai']),
|
||||
});
|
||||
export type ExecuteConnectorRequestBodyInput = z.input<typeof ExecuteConnectorRequestBody>;
|
||||
|
||||
export type ExecuteConnectorResponse = z.infer<typeof ExecuteConnectorResponse>;
|
||||
export const ExecuteConnectorResponse = z.object({
|
||||
data: z.string().optional(),
|
||||
connector_id: z.string().optional(),
|
||||
replacements: z.array(Replacement).optional(),
|
||||
status: z.string().optional(),
|
||||
/**
|
||||
* Trace Data
|
||||
*/
|
||||
trace_data: z
|
||||
.object({
|
||||
/**
|
||||
* Could be any string, not necessarily a UUID
|
||||
*/
|
||||
transactionId: z.string().optional(),
|
||||
/**
|
||||
* Could be any string, not necessarily a UUID
|
||||
*/
|
||||
traceId: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Execute Connector API endpoint
|
||||
version: '1'
|
||||
paths:
|
||||
/internal/elastic_assistant/actions/connector/{connectorId}/_execute:
|
||||
post:
|
||||
operationId: ExecuteConnector
|
||||
x-codegen-enabled: true
|
||||
description: Execute Elastic Assistant connector by id
|
||||
summary: Execute Elastic Assistant connector
|
||||
tags:
|
||||
- Connector API
|
||||
parameters:
|
||||
- name: connectorId
|
||||
in: path
|
||||
required: true
|
||||
description: The connector's `id` value.
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- params
|
||||
- llmType
|
||||
- replacements
|
||||
- subAction
|
||||
properties:
|
||||
conversationId:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/UUID'
|
||||
message:
|
||||
type: string
|
||||
model:
|
||||
type: string
|
||||
subAction:
|
||||
type: string
|
||||
enum:
|
||||
- invokeAI
|
||||
- invokeStream
|
||||
alertsIndexPattern:
|
||||
type: string
|
||||
allow:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
allowReplacement:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
isEnabledKnowledgeBase:
|
||||
type: boolean
|
||||
isEnabledRAGAlerts:
|
||||
type: boolean
|
||||
replacements:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacement'
|
||||
size:
|
||||
type: number
|
||||
llmType:
|
||||
type: string
|
||||
enum:
|
||||
- bedrock
|
||||
- openai
|
||||
responses:
|
||||
'200':
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: string
|
||||
connector_id:
|
||||
type: string
|
||||
replacements:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/Replacement'
|
||||
status:
|
||||
type: string
|
||||
trace_data:
|
||||
type: object
|
||||
description: Trace Data
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
description: Could be any string, not necessarily a UUID
|
||||
traceId:
|
||||
type: string
|
||||
description: Could be any string, not necessarily a UUID
|
||||
'400':
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Bulk Actions API endpoint
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { UUID, NonEmptyString, User } from '../conversations/common_attributes.gen';
|
||||
|
||||
export type BulkActionSkipReason = z.infer<typeof BulkActionSkipReason>;
|
||||
export const BulkActionSkipReason = z.literal('ANONYMIZATION_FIELD_NOT_MODIFIED');
|
||||
|
||||
export type BulkActionSkipResult = z.infer<typeof BulkActionSkipResult>;
|
||||
export const BulkActionSkipResult = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
skip_reason: BulkActionSkipReason,
|
||||
});
|
||||
|
||||
export type AnonymizationFieldDetailsInError = z.infer<typeof AnonymizationFieldDetailsInError>;
|
||||
export const AnonymizationFieldDetailsInError = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
export type NormalizedAnonymizationFieldError = z.infer<typeof NormalizedAnonymizationFieldError>;
|
||||
export const NormalizedAnonymizationFieldError = z.object({
|
||||
message: z.string(),
|
||||
status_code: z.number().int(),
|
||||
err_code: z.string().optional(),
|
||||
anonymization_fields: z.array(AnonymizationFieldDetailsInError),
|
||||
});
|
||||
|
||||
export type AnonymizationFieldResponse = z.infer<typeof AnonymizationFieldResponse>;
|
||||
export const AnonymizationFieldResponse = z.object({
|
||||
id: UUID,
|
||||
timestamp: NonEmptyString.optional(),
|
||||
field: z.string(),
|
||||
defaultAllow: z.boolean().optional(),
|
||||
defaultAllowReplacement: z.boolean().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
updatedBy: z.string().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
createdBy: z.string().optional(),
|
||||
users: z.array(User).optional(),
|
||||
/**
|
||||
* Kibana space
|
||||
*/
|
||||
namespace: z.string().optional(),
|
||||
});
|
||||
|
||||
export type BulkCrudActionResults = z.infer<typeof BulkCrudActionResults>;
|
||||
export const BulkCrudActionResults = z.object({
|
||||
updated: z.array(AnonymizationFieldResponse),
|
||||
created: z.array(AnonymizationFieldResponse),
|
||||
deleted: z.array(z.string()),
|
||||
skipped: z.array(BulkActionSkipResult),
|
||||
});
|
||||
|
||||
export type BulkCrudActionSummary = z.infer<typeof BulkCrudActionSummary>;
|
||||
export const BulkCrudActionSummary = z.object({
|
||||
failed: z.number().int(),
|
||||
skipped: z.number().int(),
|
||||
succeeded: z.number().int(),
|
||||
total: z.number().int(),
|
||||
});
|
||||
|
||||
export type BulkCrudActionResponse = z.infer<typeof BulkCrudActionResponse>;
|
||||
export const BulkCrudActionResponse = z.object({
|
||||
success: z.boolean().optional(),
|
||||
status_code: z.number().int().optional(),
|
||||
message: z.string().optional(),
|
||||
anonymization_fields_count: z.number().int().optional(),
|
||||
attributes: z.object({
|
||||
results: BulkCrudActionResults,
|
||||
summary: BulkCrudActionSummary,
|
||||
errors: z.array(NormalizedAnonymizationFieldError).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type BulkActionBase = z.infer<typeof BulkActionBase>;
|
||||
export const BulkActionBase = z.object({
|
||||
/**
|
||||
* Query to filter anonymization fields
|
||||
*/
|
||||
query: z.string().optional(),
|
||||
/**
|
||||
* Array of anonymization fields IDs
|
||||
*/
|
||||
ids: z.array(z.string()).min(1).optional(),
|
||||
});
|
||||
|
||||
export type AnonymizationFieldCreateProps = z.infer<typeof AnonymizationFieldCreateProps>;
|
||||
export const AnonymizationFieldCreateProps = z.object({
|
||||
field: z.string(),
|
||||
defaultAllow: z.boolean().optional(),
|
||||
defaultAllowReplacement: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type AnonymizationFieldUpdateProps = z.infer<typeof AnonymizationFieldUpdateProps>;
|
||||
export const AnonymizationFieldUpdateProps = z.object({
|
||||
id: z.string(),
|
||||
defaultAllow: z.boolean().optional(),
|
||||
defaultAllowReplacement: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type PerformBulkActionRequestBody = z.infer<typeof PerformBulkActionRequestBody>;
|
||||
export const PerformBulkActionRequestBody = z.object({
|
||||
delete: BulkActionBase.optional(),
|
||||
create: z.array(AnonymizationFieldCreateProps).optional(),
|
||||
update: z.array(AnonymizationFieldUpdateProps).optional(),
|
||||
});
|
||||
export type PerformBulkActionRequestBodyInput = z.input<typeof PerformBulkActionRequestBody>;
|
||||
|
||||
export type PerformBulkActionResponse = z.infer<typeof PerformBulkActionResponse>;
|
||||
export const PerformBulkActionResponse = BulkCrudActionResponse;
|
|
@ -0,0 +1,239 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Bulk Actions API endpoint
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/elastic_assistant/anonymization_fields/_bulk_action:
|
||||
post:
|
||||
operationId: PerformBulkAction
|
||||
x-codegen-enabled: true
|
||||
summary: Applies a bulk action to multiple anonymization fields
|
||||
description: The bulk action is applied to all anonymization fields that match the filter or to the list of anonymization fields by their IDs.
|
||||
tags:
|
||||
- Bulk API
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
delete:
|
||||
$ref: '#/components/schemas/BulkActionBase'
|
||||
create:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AnonymizationFieldCreateProps'
|
||||
update:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AnonymizationFieldUpdateProps'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BulkCrudActionResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
BulkActionSkipReason:
|
||||
type: string
|
||||
enum:
|
||||
- ANONYMIZATION_FIELD_NOT_MODIFIED
|
||||
|
||||
BulkActionSkipResult:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
skip_reason:
|
||||
$ref: '#/components/schemas/BulkActionSkipReason'
|
||||
required:
|
||||
- id
|
||||
- skip_reason
|
||||
|
||||
AnonymizationFieldDetailsInError:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
|
||||
NormalizedAnonymizationFieldError:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
err_code:
|
||||
type: string
|
||||
anonymization_fields:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AnonymizationFieldDetailsInError'
|
||||
required:
|
||||
- message
|
||||
- status_code
|
||||
- anonymization_fields
|
||||
|
||||
AnonymizationFieldResponse:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- field
|
||||
properties:
|
||||
id:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/UUID'
|
||||
'timestamp':
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/NonEmptyString'
|
||||
field:
|
||||
type: string
|
||||
defaultAllow:
|
||||
type: boolean
|
||||
defaultAllowReplacement:
|
||||
type: boolean
|
||||
updatedAt:
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
createdAt:
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/User'
|
||||
namespace:
|
||||
type: string
|
||||
description: Kibana space
|
||||
|
||||
BulkCrudActionResults:
|
||||
type: object
|
||||
properties:
|
||||
updated:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AnonymizationFieldResponse'
|
||||
created:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AnonymizationFieldResponse'
|
||||
deleted:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
skipped:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BulkActionSkipResult'
|
||||
required:
|
||||
- updated
|
||||
- created
|
||||
- deleted
|
||||
- skipped
|
||||
|
||||
BulkCrudActionSummary:
|
||||
type: object
|
||||
properties:
|
||||
failed:
|
||||
type: integer
|
||||
skipped:
|
||||
type: integer
|
||||
succeeded:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
required:
|
||||
- failed
|
||||
- skipped
|
||||
- succeeded
|
||||
- total
|
||||
|
||||
BulkCrudActionResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
status_code:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
anonymization_fields_count:
|
||||
type: integer
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
$ref: '#/components/schemas/BulkCrudActionResults'
|
||||
summary:
|
||||
$ref: '#/components/schemas/BulkCrudActionSummary'
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/NormalizedAnonymizationFieldError'
|
||||
required:
|
||||
- results
|
||||
- summary
|
||||
required:
|
||||
- attributes
|
||||
|
||||
|
||||
BulkActionBase:
|
||||
x-inline: true
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: Query to filter anonymization fields
|
||||
ids:
|
||||
type: array
|
||||
description: Array of anonymization fields IDs
|
||||
minItems: 1
|
||||
items:
|
||||
type: string
|
||||
|
||||
AnonymizationFieldCreateProps:
|
||||
type: object
|
||||
required:
|
||||
- field
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
defaultAllow:
|
||||
type: boolean
|
||||
defaultAllowReplacement:
|
||||
type: boolean
|
||||
|
||||
AnonymizationFieldUpdateProps:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
defaultAllow:
|
||||
type: boolean
|
||||
defaultAllowReplacement:
|
||||
type: boolean
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { ArrayFromString } from '@kbn/zod-helpers';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Find AnonymizationFields API endpoint
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { AnonymizationFieldResponse } from './bulk_crud_anonymization_fields_route.gen';
|
||||
|
||||
export type FindAnonymizationFieldsSortField = z.infer<typeof FindAnonymizationFieldsSortField>;
|
||||
export const FindAnonymizationFieldsSortField = z.enum([
|
||||
'created_at',
|
||||
'is_default',
|
||||
'title',
|
||||
'updated_at',
|
||||
]);
|
||||
export type FindAnonymizationFieldsSortFieldEnum = typeof FindAnonymizationFieldsSortField.enum;
|
||||
export const FindAnonymizationFieldsSortFieldEnum = FindAnonymizationFieldsSortField.enum;
|
||||
|
||||
export type SortOrder = z.infer<typeof SortOrder>;
|
||||
export const SortOrder = z.enum(['asc', 'desc']);
|
||||
export type SortOrderEnum = typeof SortOrder.enum;
|
||||
export const SortOrderEnum = SortOrder.enum;
|
||||
|
||||
export type FindAnonymizationFieldsRequestQuery = z.infer<
|
||||
typeof FindAnonymizationFieldsRequestQuery
|
||||
>;
|
||||
export const FindAnonymizationFieldsRequestQuery = z.object({
|
||||
fields: ArrayFromString(z.string()).optional(),
|
||||
/**
|
||||
* Search query
|
||||
*/
|
||||
filter: z.string().optional(),
|
||||
/**
|
||||
* Field to sort by
|
||||
*/
|
||||
sort_field: FindAnonymizationFieldsSortField.optional(),
|
||||
/**
|
||||
* Sort order
|
||||
*/
|
||||
sort_order: SortOrder.optional(),
|
||||
/**
|
||||
* Page number
|
||||
*/
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
/**
|
||||
* AnonymizationFields per page
|
||||
*/
|
||||
per_page: z.coerce.number().int().min(0).optional().default(20),
|
||||
});
|
||||
export type FindAnonymizationFieldsRequestQueryInput = z.input<
|
||||
typeof FindAnonymizationFieldsRequestQuery
|
||||
>;
|
||||
|
||||
export type FindAnonymizationFieldsResponse = z.infer<typeof FindAnonymizationFieldsResponse>;
|
||||
export const FindAnonymizationFieldsResponse = z.object({
|
||||
page: z.number().int(),
|
||||
perPage: z.number().int(),
|
||||
total: z.number().int(),
|
||||
data: z.array(AnonymizationFieldResponse),
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Find AnonymizationFields API endpoint
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/elastic_assistant/anonymization_fields/_find:
|
||||
get:
|
||||
operationId: FindAnonymizationFields
|
||||
x-codegen-enabled: true
|
||||
description: Finds anonymization fields that match the given query.
|
||||
summary: Finds anonymization fields that match the given query.
|
||||
tags:
|
||||
- AnonymizationFields API
|
||||
parameters:
|
||||
- name: 'fields'
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: 'filter'
|
||||
in: query
|
||||
description: Search query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: 'sort_field'
|
||||
in: query
|
||||
description: Field to sort by
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/FindAnonymizationFieldsSortField'
|
||||
- name: 'sort_order'
|
||||
in: query
|
||||
description: Sort order
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/SortOrder'
|
||||
- name: 'page'
|
||||
in: query
|
||||
description: Page number
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: 'per_page'
|
||||
in: query
|
||||
description: AnonymizationFields per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 20
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
perPage:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './bulk_crud_anonymization_fields_route.schema.yaml#/components/schemas/AnonymizationFieldResponse'
|
||||
required:
|
||||
- page
|
||||
- perPage
|
||||
- total
|
||||
- data
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
FindAnonymizationFieldsSortField:
|
||||
type: string
|
||||
enum:
|
||||
- 'created_at'
|
||||
- 'is_default'
|
||||
- 'title'
|
||||
- 'updated_at'
|
||||
|
||||
SortOrder:
|
||||
type: string
|
||||
enum:
|
||||
- 'asc'
|
||||
- 'desc'
|
|
@ -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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Bulk Actions API endpoint
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import {
|
||||
ConversationCreateProps,
|
||||
ConversationUpdateProps,
|
||||
ConversationResponse,
|
||||
} from './common_attributes.gen';
|
||||
|
||||
export type BulkActionSkipReason = z.infer<typeof BulkActionSkipReason>;
|
||||
export const BulkActionSkipReason = z.literal('CONVERSATION_NOT_MODIFIED');
|
||||
|
||||
export type BulkActionSkipResult = z.infer<typeof BulkActionSkipResult>;
|
||||
export const BulkActionSkipResult = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
skip_reason: BulkActionSkipReason,
|
||||
});
|
||||
|
||||
export type ConversationDetailsInError = z.infer<typeof ConversationDetailsInError>;
|
||||
export const ConversationDetailsInError = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
export type NormalizedConversationError = z.infer<typeof NormalizedConversationError>;
|
||||
export const NormalizedConversationError = z.object({
|
||||
message: z.string(),
|
||||
status_code: z.number().int(),
|
||||
err_code: z.string().optional(),
|
||||
conversations: z.array(ConversationDetailsInError),
|
||||
});
|
||||
|
||||
export type BulkCrudActionResults = z.infer<typeof BulkCrudActionResults>;
|
||||
export const BulkCrudActionResults = z.object({
|
||||
updated: z.array(ConversationResponse),
|
||||
created: z.array(ConversationResponse),
|
||||
deleted: z.array(z.string()),
|
||||
skipped: z.array(BulkActionSkipResult),
|
||||
});
|
||||
|
||||
export type BulkCrudActionSummary = z.infer<typeof BulkCrudActionSummary>;
|
||||
export const BulkCrudActionSummary = z.object({
|
||||
failed: z.number().int(),
|
||||
skipped: z.number().int(),
|
||||
succeeded: z.number().int(),
|
||||
total: z.number().int(),
|
||||
});
|
||||
|
||||
export type BulkCrudActionResponse = z.infer<typeof BulkCrudActionResponse>;
|
||||
export const BulkCrudActionResponse = z.object({
|
||||
success: z.boolean().optional(),
|
||||
status_code: z.number().int().optional(),
|
||||
message: z.string().optional(),
|
||||
conversations_count: z.number().int().optional(),
|
||||
attributes: z.object({
|
||||
results: BulkCrudActionResults,
|
||||
summary: BulkCrudActionSummary,
|
||||
errors: z.array(NormalizedConversationError).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type BulkActionBase = z.infer<typeof BulkActionBase>;
|
||||
export const BulkActionBase = z.object({
|
||||
/**
|
||||
* Query to filter conversations
|
||||
*/
|
||||
query: z.string().optional(),
|
||||
/**
|
||||
* Array of conversation IDs
|
||||
*/
|
||||
ids: z.array(z.string()).min(1).optional(),
|
||||
});
|
||||
|
||||
export type PerformBulkActionRequestBody = z.infer<typeof PerformBulkActionRequestBody>;
|
||||
export const PerformBulkActionRequestBody = z.object({
|
||||
delete: BulkActionBase.optional(),
|
||||
create: z.array(ConversationCreateProps).optional(),
|
||||
update: z.array(ConversationUpdateProps).optional(),
|
||||
});
|
||||
export type PerformBulkActionRequestBodyInput = z.input<typeof PerformBulkActionRequestBody>;
|
||||
|
||||
export type PerformBulkActionResponse = z.infer<typeof PerformBulkActionResponse>;
|
||||
export const PerformBulkActionResponse = BulkCrudActionResponse;
|
|
@ -0,0 +1,183 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Bulk Actions API endpoint
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/elastic_assistant/conversations/_bulk_action:
|
||||
post:
|
||||
operationId: PerformBulkAction
|
||||
x-codegen-enabled: true
|
||||
summary: Applies a bulk action to multiple conversations
|
||||
description: The bulk action is applied to all conversations that match the filter or to the list of conversations by their IDs.
|
||||
tags:
|
||||
- Bulk API
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
delete:
|
||||
$ref: '#/components/schemas/BulkActionBase'
|
||||
create:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationCreateProps'
|
||||
update:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationUpdateProps'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BulkCrudActionResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
BulkActionSkipReason:
|
||||
type: string
|
||||
enum:
|
||||
- CONVERSATION_NOT_MODIFIED
|
||||
|
||||
BulkActionSkipResult:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
skip_reason:
|
||||
$ref: '#/components/schemas/BulkActionSkipReason'
|
||||
required:
|
||||
- id
|
||||
- skip_reason
|
||||
|
||||
ConversationDetailsInError:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
|
||||
NormalizedConversationError:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
err_code:
|
||||
type: string
|
||||
conversations:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ConversationDetailsInError'
|
||||
required:
|
||||
- message
|
||||
- status_code
|
||||
- conversations
|
||||
|
||||
BulkCrudActionResults:
|
||||
type: object
|
||||
properties:
|
||||
updated:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
created:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
deleted:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
skipped:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BulkActionSkipResult'
|
||||
required:
|
||||
- updated
|
||||
- created
|
||||
- deleted
|
||||
- skipped
|
||||
|
||||
BulkCrudActionSummary:
|
||||
type: object
|
||||
properties:
|
||||
failed:
|
||||
type: integer
|
||||
skipped:
|
||||
type: integer
|
||||
succeeded:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
required:
|
||||
- failed
|
||||
- skipped
|
||||
- succeeded
|
||||
- total
|
||||
|
||||
BulkCrudActionResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
status_code:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
conversations_count:
|
||||
type: integer
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
$ref: '#/components/schemas/BulkCrudActionResults'
|
||||
summary:
|
||||
$ref: '#/components/schemas/BulkCrudActionSummary'
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/NormalizedConversationError'
|
||||
required:
|
||||
- results
|
||||
- summary
|
||||
required:
|
||||
- attributes
|
||||
|
||||
|
||||
BulkActionBase:
|
||||
x-inline: true
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: Query to filter conversations
|
||||
ids:
|
||||
type: array
|
||||
description: Array of conversation IDs
|
||||
minItems: 1
|
||||
items:
|
||||
type: string
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* 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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Common Conversation Attributes
|
||||
* version: not applicable
|
||||
*/
|
||||
|
||||
/**
|
||||
* A string that is not empty and does not contain only whitespace
|
||||
*/
|
||||
export type NonEmptyString = z.infer<typeof NonEmptyString>;
|
||||
export const NonEmptyString = z
|
||||
.string()
|
||||
.min(1)
|
||||
.regex(/^(?! *$).+$/);
|
||||
|
||||
/**
|
||||
* A universally unique identifier
|
||||
*/
|
||||
export type UUID = z.infer<typeof UUID>;
|
||||
export const UUID = z.string().uuid();
|
||||
|
||||
/**
|
||||
* Could be any string, not necessarily a UUID
|
||||
*/
|
||||
export type User = z.infer<typeof User>;
|
||||
export const User = z.object({
|
||||
/**
|
||||
* User id.
|
||||
*/
|
||||
id: z.string().optional(),
|
||||
/**
|
||||
* User name.
|
||||
*/
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* trace Data
|
||||
*/
|
||||
export type TraceData = z.infer<typeof TraceData>;
|
||||
export const TraceData = z.object({
|
||||
/**
|
||||
* Could be any string, not necessarily a UUID
|
||||
*/
|
||||
transactionId: z.string().optional(),
|
||||
/**
|
||||
* Could be any string, not necessarily a UUID
|
||||
*/
|
||||
traceId: z.string().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Replacements object used to anonymize/deanomymize messsages
|
||||
*/
|
||||
export type Replacement = z.infer<typeof Replacement>;
|
||||
export const Replacement = z.object({
|
||||
/**
|
||||
* Actual value was anonymized.
|
||||
*/
|
||||
value: z.string(),
|
||||
uuid: UUID,
|
||||
});
|
||||
|
||||
export type Reader = z.infer<typeof Reader>;
|
||||
export const Reader = z.object({}).catchall(z.unknown());
|
||||
|
||||
/**
|
||||
* Provider
|
||||
*/
|
||||
export type Provider = z.infer<typeof Provider>;
|
||||
export const Provider = z.enum(['OpenAI', 'Azure OpenAI']);
|
||||
export type ProviderEnum = typeof Provider.enum;
|
||||
export const ProviderEnum = Provider.enum;
|
||||
|
||||
/**
|
||||
* Message role.
|
||||
*/
|
||||
export type MessageRole = z.infer<typeof MessageRole>;
|
||||
export const MessageRole = z.enum(['system', 'user', 'assistant']);
|
||||
export type MessageRoleEnum = typeof MessageRole.enum;
|
||||
export const MessageRoleEnum = MessageRole.enum;
|
||||
|
||||
/**
|
||||
* The conversation category.
|
||||
*/
|
||||
export type ConversationCategory = z.infer<typeof ConversationCategory>;
|
||||
export const ConversationCategory = z.enum(['assistant', 'insights']);
|
||||
export type ConversationCategoryEnum = typeof ConversationCategory.enum;
|
||||
export const ConversationCategoryEnum = ConversationCategory.enum;
|
||||
|
||||
/**
|
||||
* The conversation confidence.
|
||||
*/
|
||||
export type ConversationConfidence = z.infer<typeof ConversationConfidence>;
|
||||
export const ConversationConfidence = z.enum(['low', 'medium', 'high']);
|
||||
export type ConversationConfidenceEnum = typeof ConversationConfidence.enum;
|
||||
export const ConversationConfidenceEnum = ConversationConfidence.enum;
|
||||
|
||||
/**
|
||||
* AI assistant conversation message.
|
||||
*/
|
||||
export type Message = z.infer<typeof Message>;
|
||||
export const Message = z.object({
|
||||
/**
|
||||
* Message content.
|
||||
*/
|
||||
content: z.string(),
|
||||
/**
|
||||
* Message content.
|
||||
*/
|
||||
reader: Reader.optional(),
|
||||
/**
|
||||
* Message role.
|
||||
*/
|
||||
role: MessageRole,
|
||||
/**
|
||||
* The timestamp message was sent or received.
|
||||
*/
|
||||
timestamp: NonEmptyString,
|
||||
/**
|
||||
* Is error message.
|
||||
*/
|
||||
isError: z.boolean().optional(),
|
||||
/**
|
||||
* trace Data
|
||||
*/
|
||||
traceData: TraceData.optional(),
|
||||
});
|
||||
|
||||
export type ApiConfig = z.infer<typeof ApiConfig>;
|
||||
export const ApiConfig = z.object({
|
||||
/**
|
||||
* connector Id
|
||||
*/
|
||||
connectorId: z.string(),
|
||||
/**
|
||||
* connector Type Title
|
||||
*/
|
||||
connectorTypeTitle: z.string(),
|
||||
/**
|
||||
* defaultSystemPromptId
|
||||
*/
|
||||
defaultSystemPromptId: z.string().optional(),
|
||||
/**
|
||||
* Provider
|
||||
*/
|
||||
provider: Provider.optional(),
|
||||
/**
|
||||
* model
|
||||
*/
|
||||
model: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ConversationSummary = z.infer<typeof ConversationSummary>;
|
||||
export const ConversationSummary = z.object({
|
||||
/**
|
||||
* Summary text of the conversation over time.
|
||||
*/
|
||||
content: z.string().optional(),
|
||||
/**
|
||||
* The timestamp summary was updated.
|
||||
*/
|
||||
timestamp: NonEmptyString.optional(),
|
||||
/**
|
||||
* Define if summary is marked as publicly available.
|
||||
*/
|
||||
public: z.boolean().optional(),
|
||||
/**
|
||||
* How confident you are about this being a correct and useful learning.
|
||||
*/
|
||||
confidence: ConversationConfidence.optional(),
|
||||
});
|
||||
|
||||
export type ErrorSchema = z.infer<typeof ErrorSchema>;
|
||||
export const ErrorSchema = z
|
||||
.object({
|
||||
id: UUID.optional(),
|
||||
error: z.object({
|
||||
status_code: z.number().int().min(400),
|
||||
message: z.string(),
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type ConversationResponse = z.infer<typeof ConversationResponse>;
|
||||
export const ConversationResponse = z.object({
|
||||
id: z.union([UUID, NonEmptyString]),
|
||||
/**
|
||||
* The conversation title.
|
||||
*/
|
||||
title: z.string(),
|
||||
/**
|
||||
* The conversation category.
|
||||
*/
|
||||
category: ConversationCategory,
|
||||
summary: ConversationSummary.optional(),
|
||||
timestamp: NonEmptyString.optional(),
|
||||
/**
|
||||
* The last time conversation was updated.
|
||||
*/
|
||||
updatedAt: z.string().optional(),
|
||||
/**
|
||||
* The last time conversation was updated.
|
||||
*/
|
||||
createdAt: z.string(),
|
||||
replacements: z.array(Replacement).optional(),
|
||||
users: z.array(User),
|
||||
/**
|
||||
* The conversation messages.
|
||||
*/
|
||||
messages: z.array(Message).optional(),
|
||||
/**
|
||||
* LLM API configuration.
|
||||
*/
|
||||
apiConfig: ApiConfig.optional(),
|
||||
/**
|
||||
* Is default conversation.
|
||||
*/
|
||||
isDefault: z.boolean().optional(),
|
||||
/**
|
||||
* excludeFromLastConversationStorage.
|
||||
*/
|
||||
excludeFromLastConversationStorage: z.boolean().optional(),
|
||||
/**
|
||||
* Kibana space
|
||||
*/
|
||||
namespace: z.string(),
|
||||
});
|
||||
|
||||
export type ConversationUpdateProps = z.infer<typeof ConversationUpdateProps>;
|
||||
export const ConversationUpdateProps = z.object({
|
||||
id: z.union([UUID, NonEmptyString]),
|
||||
/**
|
||||
* The conversation title.
|
||||
*/
|
||||
title: z.string().optional(),
|
||||
/**
|
||||
* The conversation category.
|
||||
*/
|
||||
category: ConversationCategory.optional(),
|
||||
/**
|
||||
* The conversation messages.
|
||||
*/
|
||||
messages: z.array(Message).optional(),
|
||||
/**
|
||||
* LLM API configuration.
|
||||
*/
|
||||
apiConfig: ApiConfig.optional(),
|
||||
summary: ConversationSummary.optional(),
|
||||
/**
|
||||
* excludeFromLastConversationStorage.
|
||||
*/
|
||||
excludeFromLastConversationStorage: z.boolean().optional(),
|
||||
replacements: z.array(Replacement).optional(),
|
||||
});
|
||||
|
||||
export type ConversationCreateProps = z.infer<typeof ConversationCreateProps>;
|
||||
export const ConversationCreateProps = z.object({
|
||||
/**
|
||||
* The conversation title.
|
||||
*/
|
||||
title: z.string(),
|
||||
/**
|
||||
* The conversation category.
|
||||
*/
|
||||
category: ConversationCategory.optional(),
|
||||
/**
|
||||
* The conversation messages.
|
||||
*/
|
||||
messages: z.array(Message).optional(),
|
||||
/**
|
||||
* LLM API configuration.
|
||||
*/
|
||||
apiConfig: ApiConfig.optional(),
|
||||
/**
|
||||
* Is default conversation.
|
||||
*/
|
||||
isDefault: z.boolean().optional(),
|
||||
/**
|
||||
* excludeFromLastConversationStorage.
|
||||
*/
|
||||
excludeFromLastConversationStorage: z.boolean().optional(),
|
||||
replacements: z.array(Replacement).optional(),
|
||||
});
|
||||
|
||||
export type ConversationMessageCreateProps = z.infer<typeof ConversationMessageCreateProps>;
|
||||
export const ConversationMessageCreateProps = z.object({
|
||||
/**
|
||||
* The conversation messages.
|
||||
*/
|
||||
messages: z.array(Message),
|
||||
});
|
|
@ -0,0 +1,303 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Common Conversation Attributes
|
||||
version: 'not applicable'
|
||||
paths: {}
|
||||
components:
|
||||
x-codegen-enabled: true
|
||||
schemas:
|
||||
NonEmptyString:
|
||||
type: string
|
||||
pattern: ^(?! *$).+$
|
||||
minLength: 1
|
||||
description: A string that is not empty and does not contain only whitespace
|
||||
|
||||
UUID:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A universally unique identifier
|
||||
|
||||
User:
|
||||
type: object
|
||||
description: Could be any string, not necessarily a UUID
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: User id.
|
||||
name:
|
||||
type: string
|
||||
description: User name.
|
||||
|
||||
TraceData:
|
||||
type: object
|
||||
description: trace Data
|
||||
properties:
|
||||
transactionId:
|
||||
type: string
|
||||
description: Could be any string, not necessarily a UUID
|
||||
traceId:
|
||||
type: string
|
||||
description: Could be any string, not necessarily a UUID
|
||||
|
||||
Replacement:
|
||||
type: object
|
||||
required:
|
||||
- 'value'
|
||||
- 'uuid'
|
||||
description: Replacements object used to anonymize/deanomymize messsages
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
description: Actual value was anonymized.
|
||||
uuid:
|
||||
$ref: '#/components/schemas/UUID'
|
||||
|
||||
Reader:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
|
||||
Provider:
|
||||
type: string
|
||||
description: Provider
|
||||
enum:
|
||||
- OpenAI
|
||||
- Azure OpenAI
|
||||
|
||||
MessageRole:
|
||||
type: string
|
||||
description: Message role.
|
||||
enum:
|
||||
- system
|
||||
- user
|
||||
- assistant
|
||||
|
||||
ConversationCategory:
|
||||
type: string
|
||||
description: The conversation category.
|
||||
enum:
|
||||
- assistant
|
||||
- insights
|
||||
|
||||
ConversationConfidence:
|
||||
type: string
|
||||
description: The conversation confidence.
|
||||
enum:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
|
||||
Message:
|
||||
type: object
|
||||
description: AI assistant conversation message.
|
||||
required:
|
||||
- 'timestamp'
|
||||
- 'content'
|
||||
- 'role'
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
description: Message content.
|
||||
reader:
|
||||
$ref: '#/components/schemas/Reader'
|
||||
description: Message content.
|
||||
role:
|
||||
$ref: '#/components/schemas/MessageRole'
|
||||
description: Message role.
|
||||
timestamp:
|
||||
$ref: '#/components/schemas/NonEmptyString'
|
||||
description: The timestamp message was sent or received.
|
||||
isError:
|
||||
type: boolean
|
||||
description: Is error message.
|
||||
traceData:
|
||||
$ref: '#/components/schemas/TraceData'
|
||||
description: trace Data
|
||||
|
||||
ApiConfig:
|
||||
type: object
|
||||
required:
|
||||
- connectorId
|
||||
- connectorTypeTitle
|
||||
properties:
|
||||
connectorId:
|
||||
type: string
|
||||
description: connector Id
|
||||
connectorTypeTitle:
|
||||
type: string
|
||||
description: connector Type Title
|
||||
defaultSystemPromptId:
|
||||
type: string
|
||||
description: defaultSystemPromptId
|
||||
provider:
|
||||
$ref: '#/components/schemas/Provider'
|
||||
description: Provider
|
||||
model:
|
||||
type: string
|
||||
description: model
|
||||
|
||||
ConversationSummary:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
description: Summary text of the conversation over time.
|
||||
timestamp:
|
||||
$ref: '#/components/schemas/NonEmptyString'
|
||||
description: The timestamp summary was updated.
|
||||
public:
|
||||
type: boolean
|
||||
description: Define if summary is marked as publicly available.
|
||||
confidence:
|
||||
$ref: '#/components/schemas/ConversationConfidence'
|
||||
description: How confident you are about this being a correct and useful learning.
|
||||
|
||||
ErrorSchema:
|
||||
type: object
|
||||
required:
|
||||
- error
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/UUID'
|
||||
error:
|
||||
type: object
|
||||
required:
|
||||
- status_code
|
||||
- message
|
||||
properties:
|
||||
status_code:
|
||||
type: integer
|
||||
minimum: 400
|
||||
message:
|
||||
type: string
|
||||
|
||||
ConversationResponse:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- title
|
||||
- createdAt
|
||||
- users
|
||||
- namespace
|
||||
- category
|
||||
properties:
|
||||
id:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/UUID'
|
||||
- $ref: '#/components/schemas/NonEmptyString'
|
||||
title:
|
||||
type: string
|
||||
description: The conversation title.
|
||||
category:
|
||||
$ref: '#/components/schemas/ConversationCategory'
|
||||
description: The conversation category.
|
||||
summary:
|
||||
$ref: '#/components/schemas/ConversationSummary'
|
||||
'timestamp':
|
||||
$ref: '#/components/schemas/NonEmptyString'
|
||||
updatedAt:
|
||||
description: The last time conversation was updated.
|
||||
type: string
|
||||
createdAt:
|
||||
description: The last time conversation was updated.
|
||||
type: string
|
||||
replacements:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Replacement'
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
description: The conversation messages.
|
||||
apiConfig:
|
||||
$ref: '#/components/schemas/ApiConfig'
|
||||
description: LLM API configuration.
|
||||
isDefault:
|
||||
description: Is default conversation.
|
||||
type: boolean
|
||||
excludeFromLastConversationStorage:
|
||||
description: excludeFromLastConversationStorage.
|
||||
type: boolean
|
||||
namespace:
|
||||
type: string
|
||||
description: Kibana space
|
||||
|
||||
ConversationUpdateProps:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/UUID'
|
||||
- $ref: '#/components/schemas/NonEmptyString'
|
||||
title:
|
||||
type: string
|
||||
description: The conversation title.
|
||||
category:
|
||||
$ref: '#/components/schemas/ConversationCategory'
|
||||
description: The conversation category.
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
description: The conversation messages.
|
||||
apiConfig:
|
||||
$ref: '#/components/schemas/ApiConfig'
|
||||
description: LLM API configuration.
|
||||
summary:
|
||||
$ref: '#/components/schemas/ConversationSummary'
|
||||
excludeFromLastConversationStorage:
|
||||
description: excludeFromLastConversationStorage.
|
||||
type: boolean
|
||||
replacements:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Replacement'
|
||||
|
||||
ConversationCreateProps:
|
||||
type: object
|
||||
required:
|
||||
- title
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: The conversation title.
|
||||
category:
|
||||
$ref: '#/components/schemas/ConversationCategory'
|
||||
description: The conversation category.
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
description: The conversation messages.
|
||||
apiConfig:
|
||||
$ref: '#/components/schemas/ApiConfig'
|
||||
description: LLM API configuration.
|
||||
isDefault:
|
||||
description: Is default conversation.
|
||||
type: boolean
|
||||
excludeFromLastConversationStorage:
|
||||
description: excludeFromLastConversationStorage.
|
||||
type: boolean
|
||||
replacements:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Replacement'
|
||||
|
||||
ConversationMessageCreateProps:
|
||||
type: object
|
||||
required:
|
||||
- messages
|
||||
properties:
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Message'
|
||||
description: The conversation messages.
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Create Conversation API endpoint
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import {
|
||||
ConversationCreateProps,
|
||||
ConversationResponse,
|
||||
UUID,
|
||||
ConversationUpdateProps,
|
||||
ConversationMessageCreateProps,
|
||||
} from './common_attributes.gen';
|
||||
|
||||
export type AppendConversationMessageRequestParams = z.infer<
|
||||
typeof AppendConversationMessageRequestParams
|
||||
>;
|
||||
export const AppendConversationMessageRequestParams = z.object({
|
||||
/**
|
||||
* The conversation's `id` value.
|
||||
*/
|
||||
id: UUID,
|
||||
});
|
||||
export type AppendConversationMessageRequestParamsInput = z.input<
|
||||
typeof AppendConversationMessageRequestParams
|
||||
>;
|
||||
|
||||
export type AppendConversationMessageRequestBody = z.infer<
|
||||
typeof AppendConversationMessageRequestBody
|
||||
>;
|
||||
export const AppendConversationMessageRequestBody = ConversationMessageCreateProps;
|
||||
export type AppendConversationMessageRequestBodyInput = z.input<
|
||||
typeof AppendConversationMessageRequestBody
|
||||
>;
|
||||
|
||||
export type AppendConversationMessageResponse = z.infer<typeof AppendConversationMessageResponse>;
|
||||
export const AppendConversationMessageResponse = ConversationResponse;
|
||||
|
||||
export type CreateConversationRequestBody = z.infer<typeof CreateConversationRequestBody>;
|
||||
export const CreateConversationRequestBody = ConversationCreateProps;
|
||||
export type CreateConversationRequestBodyInput = z.input<typeof CreateConversationRequestBody>;
|
||||
|
||||
export type CreateConversationResponse = z.infer<typeof CreateConversationResponse>;
|
||||
export const CreateConversationResponse = ConversationResponse;
|
||||
|
||||
export type DeleteConversationRequestParams = z.infer<typeof DeleteConversationRequestParams>;
|
||||
export const DeleteConversationRequestParams = z.object({
|
||||
/**
|
||||
* The conversation's `id` value.
|
||||
*/
|
||||
id: UUID,
|
||||
});
|
||||
export type DeleteConversationRequestParamsInput = z.input<typeof DeleteConversationRequestParams>;
|
||||
|
||||
export type DeleteConversationResponse = z.infer<typeof DeleteConversationResponse>;
|
||||
export const DeleteConversationResponse = ConversationResponse;
|
||||
|
||||
export type ReadConversationRequestParams = z.infer<typeof ReadConversationRequestParams>;
|
||||
export const ReadConversationRequestParams = z.object({
|
||||
/**
|
||||
* The conversation's `id` value.
|
||||
*/
|
||||
id: UUID,
|
||||
});
|
||||
export type ReadConversationRequestParamsInput = z.input<typeof ReadConversationRequestParams>;
|
||||
|
||||
export type ReadConversationResponse = z.infer<typeof ReadConversationResponse>;
|
||||
export const ReadConversationResponse = ConversationResponse;
|
||||
|
||||
export type UpdateConversationRequestParams = z.infer<typeof UpdateConversationRequestParams>;
|
||||
export const UpdateConversationRequestParams = z.object({
|
||||
/**
|
||||
* The conversation's `id` value.
|
||||
*/
|
||||
id: UUID,
|
||||
});
|
||||
export type UpdateConversationRequestParamsInput = z.input<typeof UpdateConversationRequestParams>;
|
||||
|
||||
export type UpdateConversationRequestBody = z.infer<typeof UpdateConversationRequestBody>;
|
||||
export const UpdateConversationRequestBody = ConversationUpdateProps;
|
||||
export type UpdateConversationRequestBodyInput = z.input<typeof UpdateConversationRequestBody>;
|
||||
|
||||
export type UpdateConversationResponse = z.infer<typeof UpdateConversationResponse>;
|
||||
export const UpdateConversationResponse = ConversationResponse;
|
|
@ -0,0 +1,191 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Create Conversation API endpoint
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/elastic_assistant/conversations:
|
||||
post:
|
||||
operationId: CreateConversation
|
||||
x-codegen-enabled: true
|
||||
description: Create a conversation
|
||||
summary: Create a conversation
|
||||
tags:
|
||||
- Conversation API
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationCreateProps'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
/api/elastic_assistant/conversations/{id}:
|
||||
get:
|
||||
operationId: ReadConversation
|
||||
x-codegen-enabled: true
|
||||
description: Read a single conversation
|
||||
summary: Read a single conversation
|
||||
tags:
|
||||
- Conversations API
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The conversation's `id` value.
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/UUID'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
put:
|
||||
operationId: UpdateConversation
|
||||
x-codegen-enabled: true
|
||||
description: Update a single conversation
|
||||
summary: Update a conversation
|
||||
tags:
|
||||
- Conversation API
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The conversation's `id` value.
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/UUID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationUpdateProps'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
delete:
|
||||
operationId: DeleteConversation
|
||||
x-codegen-enabled: true
|
||||
description: Deletes a single conversation using the `id` field.
|
||||
summary: Deletes a single conversation using the `id` field.
|
||||
tags:
|
||||
- Conversation API
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The conversation's `id` value.
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/UUID'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
/api/elastic_assistant/conversations/{id}/messages:
|
||||
post:
|
||||
operationId: AppendConversationMessage
|
||||
x-codegen-enabled: true
|
||||
description: Append a message to the conversation
|
||||
summary: Append a message to the conversation
|
||||
tags:
|
||||
- Conversation API
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: The conversation's `id` value.
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/UUID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationMessageCreateProps'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { z } from 'zod';
|
||||
import { ArrayFromString } from '@kbn/zod-helpers';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Find Conversations API endpoint
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { ConversationResponse } from './common_attributes.gen';
|
||||
|
||||
export type FindConversationsSortField = z.infer<typeof FindConversationsSortField>;
|
||||
export const FindConversationsSortField = z.enum([
|
||||
'created_at',
|
||||
'is_default',
|
||||
'title',
|
||||
'updated_at',
|
||||
]);
|
||||
export type FindConversationsSortFieldEnum = typeof FindConversationsSortField.enum;
|
||||
export const FindConversationsSortFieldEnum = FindConversationsSortField.enum;
|
||||
|
||||
export type SortOrder = z.infer<typeof SortOrder>;
|
||||
export const SortOrder = z.enum(['asc', 'desc']);
|
||||
export type SortOrderEnum = typeof SortOrder.enum;
|
||||
export const SortOrderEnum = SortOrder.enum;
|
||||
|
||||
export type FindConversationsRequestQuery = z.infer<typeof FindConversationsRequestQuery>;
|
||||
export const FindConversationsRequestQuery = z.object({
|
||||
fields: ArrayFromString(z.string()).optional(),
|
||||
/**
|
||||
* Search query
|
||||
*/
|
||||
filter: z.string().optional(),
|
||||
/**
|
||||
* Field to sort by
|
||||
*/
|
||||
sort_field: FindConversationsSortField.optional(),
|
||||
/**
|
||||
* Sort order
|
||||
*/
|
||||
sort_order: SortOrder.optional(),
|
||||
/**
|
||||
* Page number
|
||||
*/
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
/**
|
||||
* Conversations per page
|
||||
*/
|
||||
per_page: z.coerce.number().int().min(0).optional().default(20),
|
||||
});
|
||||
export type FindConversationsRequestQueryInput = z.input<typeof FindConversationsRequestQuery>;
|
||||
|
||||
export type FindConversationsResponse = z.infer<typeof FindConversationsResponse>;
|
||||
export const FindConversationsResponse = z.object({
|
||||
page: z.number().int(),
|
||||
perPage: z.number().int(),
|
||||
total: z.number().int(),
|
||||
data: z.array(ConversationResponse),
|
||||
});
|
||||
export type FindCurrentUserConversationsRequestQuery = z.infer<
|
||||
typeof FindCurrentUserConversationsRequestQuery
|
||||
>;
|
||||
export const FindCurrentUserConversationsRequestQuery = z.object({
|
||||
fields: ArrayFromString(z.string()).optional(),
|
||||
/**
|
||||
* Search query
|
||||
*/
|
||||
filter: z.string().optional(),
|
||||
/**
|
||||
* Field to sort by
|
||||
*/
|
||||
sort_field: FindConversationsSortField.optional(),
|
||||
/**
|
||||
* Sort order
|
||||
*/
|
||||
sort_order: SortOrder.optional(),
|
||||
/**
|
||||
* Page number
|
||||
*/
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
/**
|
||||
* Conversations per page
|
||||
*/
|
||||
per_page: z.coerce.number().int().min(0).optional().default(20),
|
||||
});
|
||||
export type FindCurrentUserConversationsRequestQueryInput = z.input<
|
||||
typeof FindCurrentUserConversationsRequestQuery
|
||||
>;
|
||||
|
||||
export type FindCurrentUserConversationsResponse = z.infer<
|
||||
typeof FindCurrentUserConversationsResponse
|
||||
>;
|
||||
export const FindCurrentUserConversationsResponse = z.object({
|
||||
page: z.number().int(),
|
||||
perPage: z.number().int(),
|
||||
total: z.number().int(),
|
||||
data: z.array(ConversationResponse),
|
||||
});
|
|
@ -0,0 +1,196 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Find Conversations API endpoint
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/elastic_assistant/conversations/_find:
|
||||
get:
|
||||
operationId: FindConversations
|
||||
x-codegen-enabled: true
|
||||
description: Finds conversations that match the given query.
|
||||
summary: Finds conversations that match the given query.
|
||||
tags:
|
||||
- Conversations API
|
||||
parameters:
|
||||
- name: 'fields'
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: 'filter'
|
||||
in: query
|
||||
description: Search query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: 'sort_field'
|
||||
in: query
|
||||
description: Field to sort by
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/FindConversationsSortField'
|
||||
- name: 'sort_order'
|
||||
in: query
|
||||
description: Sort order
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/SortOrder'
|
||||
- name: 'page'
|
||||
in: query
|
||||
description: Page number
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: 'per_page'
|
||||
in: query
|
||||
description: Conversations per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 20
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
perPage:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
required:
|
||||
- page
|
||||
- perPage
|
||||
- total
|
||||
- data
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
/api/elastic_assistant/conversations/current_user/_find:
|
||||
get:
|
||||
operationId: FindCurrentUserConversations
|
||||
x-codegen-enabled: true
|
||||
description: Finds current user conversations that match the given query.
|
||||
summary: Finds current user conversations that match the given query.
|
||||
tags:
|
||||
- Conversations API
|
||||
parameters:
|
||||
- name: 'fields'
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: 'filter'
|
||||
in: query
|
||||
description: Search query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: 'sort_field'
|
||||
in: query
|
||||
description: Field to sort by
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/FindConversationsSortField'
|
||||
- name: 'sort_order'
|
||||
in: query
|
||||
description: Sort order
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/SortOrder'
|
||||
- name: 'page'
|
||||
in: query
|
||||
description: Page number
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: 'per_page'
|
||||
in: query
|
||||
description: Conversations per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 20
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
perPage:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './common_attributes.schema.yaml#/components/schemas/ConversationResponse'
|
||||
required:
|
||||
- page
|
||||
- perPage
|
||||
- total
|
||||
- data
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
FindConversationsSortField:
|
||||
type: string
|
||||
enum:
|
||||
- 'created_at'
|
||||
- 'is_default'
|
||||
- 'title'
|
||||
- 'updated_at'
|
||||
|
||||
SortOrder:
|
||||
type: string
|
||||
enum:
|
||||
- 'asc'
|
||||
- 'desc'
|
|
@ -24,3 +24,15 @@ export * from './evaluation/get_evaluate_route.gen';
|
|||
|
||||
// Capabilities Schemas
|
||||
export * from './capabilities/get_capabilities_route.gen';
|
||||
|
||||
// Conversations Schemas
|
||||
export * from './conversations/bulk_crud_conversations_route.gen';
|
||||
export * from './conversations/common_attributes.gen';
|
||||
export * from './conversations/crud_conversation_route.gen';
|
||||
export * from './conversations/find_conversations_route.gen';
|
||||
|
||||
// Actions Connector Schemas
|
||||
export * from './actions_connector/post_actions_connector_execute_route.gen';
|
||||
|
||||
// KB Schemas
|
||||
export * from './knowledge_base/crud_kb_route.gen';
|
||||
|
|
|
@ -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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: KnowledgeBase API endpoints
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI assistant KnowledgeBase.
|
||||
*/
|
||||
export type KnowledgeBaseResponse = z.infer<typeof KnowledgeBaseResponse>;
|
||||
export const KnowledgeBaseResponse = z.object({
|
||||
/**
|
||||
* Identify the success of the method execution.
|
||||
*/
|
||||
success: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateKnowledgeBaseRequestParams = z.infer<typeof CreateKnowledgeBaseRequestParams>;
|
||||
export const CreateKnowledgeBaseRequestParams = z.object({
|
||||
/**
|
||||
* The KnowledgeBase `resource` value.
|
||||
*/
|
||||
resource: z.string().optional(),
|
||||
});
|
||||
export type CreateKnowledgeBaseRequestParamsInput = z.input<
|
||||
typeof CreateKnowledgeBaseRequestParams
|
||||
>;
|
||||
|
||||
export type CreateKnowledgeBaseResponse = z.infer<typeof CreateKnowledgeBaseResponse>;
|
||||
export const CreateKnowledgeBaseResponse = KnowledgeBaseResponse;
|
||||
|
||||
export type DeleteKnowledgeBaseRequestParams = z.infer<typeof DeleteKnowledgeBaseRequestParams>;
|
||||
export const DeleteKnowledgeBaseRequestParams = z.object({
|
||||
/**
|
||||
* The KnowledgeBase `resource` value.
|
||||
*/
|
||||
resource: z.string().optional(),
|
||||
});
|
||||
export type DeleteKnowledgeBaseRequestParamsInput = z.input<
|
||||
typeof DeleteKnowledgeBaseRequestParams
|
||||
>;
|
||||
|
||||
export type DeleteKnowledgeBaseResponse = z.infer<typeof DeleteKnowledgeBaseResponse>;
|
||||
export const DeleteKnowledgeBaseResponse = KnowledgeBaseResponse;
|
||||
|
||||
export type ReadKnowledgeBaseRequestParams = z.infer<typeof ReadKnowledgeBaseRequestParams>;
|
||||
export const ReadKnowledgeBaseRequestParams = z.object({
|
||||
/**
|
||||
* The KnowledgeBase `resource` value.
|
||||
*/
|
||||
resource: z.string().optional(),
|
||||
});
|
||||
export type ReadKnowledgeBaseRequestParamsInput = z.input<typeof ReadKnowledgeBaseRequestParams>;
|
||||
|
||||
export type ReadKnowledgeBaseResponse = z.infer<typeof ReadKnowledgeBaseResponse>;
|
||||
export const ReadKnowledgeBaseResponse = z.object({
|
||||
elser_exists: z.boolean().optional(),
|
||||
index_exists: z.boolean().optional(),
|
||||
pipeline_exists: z.boolean().optional(),
|
||||
});
|
|
@ -0,0 +1,122 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: KnowledgeBase API endpoints
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/internal/elastic_assistant/knowledge_base/{resource}:
|
||||
post:
|
||||
operationId: CreateKnowledgeBase
|
||||
x-codegen-enabled: true
|
||||
summary: Create a KnowledgeBase
|
||||
description: Create a KnowledgeBase
|
||||
tags:
|
||||
- KnowledgeBase API
|
||||
parameters:
|
||||
- name: resource
|
||||
in: path
|
||||
description: The KnowledgeBase `resource` value.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgeBaseResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
get:
|
||||
operationId: ReadKnowledgeBase
|
||||
x-codegen-enabled: true
|
||||
description: Read a single KB
|
||||
summary: Read a KnowledgeBase
|
||||
tags:
|
||||
- KnowledgeBase API
|
||||
parameters:
|
||||
- name: resource
|
||||
in: path
|
||||
description: The KnowledgeBase `resource` value.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
elser_exists:
|
||||
type: boolean
|
||||
index_exists:
|
||||
type: boolean
|
||||
pipeline_exists:
|
||||
type: boolean
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
delete:
|
||||
operationId: DeleteKnowledgeBase
|
||||
x-codegen-enabled: true
|
||||
description: Deletes KnowledgeBase with the `resource` field.
|
||||
summary: Deletes a KnowledgeBase
|
||||
tags:
|
||||
- KnowledgeBase API
|
||||
parameters:
|
||||
- name: resource
|
||||
in: path
|
||||
description: The KnowledgeBase `resource` value.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/KnowledgeBaseResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
KnowledgeBaseResponse:
|
||||
type: object
|
||||
description: AI assistant KnowledgeBase.
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Identify the success of the method execution.
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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 { z } from 'zod';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Bulk Actions API endpoint
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { UUID, NonEmptyString, User } from '../conversations/common_attributes.gen';
|
||||
|
||||
export type BulkActionSkipReason = z.infer<typeof BulkActionSkipReason>;
|
||||
export const BulkActionSkipReason = z.literal('PROMPT_FIELD_NOT_MODIFIED');
|
||||
|
||||
export type BulkActionSkipResult = z.infer<typeof BulkActionSkipResult>;
|
||||
export const BulkActionSkipResult = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
skip_reason: BulkActionSkipReason,
|
||||
});
|
||||
|
||||
export type PromptDetailsInError = z.infer<typeof PromptDetailsInError>;
|
||||
export const PromptDetailsInError = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
export type NormalizedPromptError = z.infer<typeof NormalizedPromptError>;
|
||||
export const NormalizedPromptError = z.object({
|
||||
message: z.string(),
|
||||
status_code: z.number().int(),
|
||||
err_code: z.string().optional(),
|
||||
prompts: z.array(PromptDetailsInError),
|
||||
});
|
||||
|
||||
export type PromptResponse = z.infer<typeof PromptResponse>;
|
||||
export const PromptResponse = z.object({
|
||||
id: UUID,
|
||||
timestamp: NonEmptyString.optional(),
|
||||
name: z.string(),
|
||||
promptType: z.string(),
|
||||
content: z.string(),
|
||||
isNewConversationDefault: z.boolean().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
isShared: z.boolean().optional(),
|
||||
updatedAt: z.string().optional(),
|
||||
updatedBy: z.string().optional(),
|
||||
createdAt: z.string().optional(),
|
||||
createdBy: z.string().optional(),
|
||||
users: z.array(User).optional(),
|
||||
/**
|
||||
* Kibana space
|
||||
*/
|
||||
namespace: z.string().optional(),
|
||||
});
|
||||
|
||||
export type BulkCrudActionResults = z.infer<typeof BulkCrudActionResults>;
|
||||
export const BulkCrudActionResults = z.object({
|
||||
updated: z.array(PromptResponse),
|
||||
created: z.array(PromptResponse),
|
||||
deleted: z.array(z.string()),
|
||||
skipped: z.array(BulkActionSkipResult),
|
||||
});
|
||||
|
||||
export type BulkCrudActionSummary = z.infer<typeof BulkCrudActionSummary>;
|
||||
export const BulkCrudActionSummary = z.object({
|
||||
failed: z.number().int(),
|
||||
skipped: z.number().int(),
|
||||
succeeded: z.number().int(),
|
||||
total: z.number().int(),
|
||||
});
|
||||
|
||||
export type BulkCrudActionResponse = z.infer<typeof BulkCrudActionResponse>;
|
||||
export const BulkCrudActionResponse = z.object({
|
||||
success: z.boolean().optional(),
|
||||
status_code: z.number().int().optional(),
|
||||
message: z.string().optional(),
|
||||
prompts_count: z.number().int().optional(),
|
||||
attributes: z.object({
|
||||
results: BulkCrudActionResults,
|
||||
summary: BulkCrudActionSummary,
|
||||
errors: z.array(NormalizedPromptError).optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type BulkActionBase = z.infer<typeof BulkActionBase>;
|
||||
export const BulkActionBase = z.object({
|
||||
/**
|
||||
* Query to filter promps
|
||||
*/
|
||||
query: z.string().optional(),
|
||||
/**
|
||||
* Array of prompts IDs
|
||||
*/
|
||||
ids: z.array(z.string()).min(1).optional(),
|
||||
});
|
||||
|
||||
export type PromptCreateProps = z.infer<typeof PromptCreateProps>;
|
||||
export const PromptCreateProps = z.object({
|
||||
name: z.string(),
|
||||
promptType: z.string(),
|
||||
content: z.string(),
|
||||
isNewConversationDefault: z.boolean().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
isShared: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type PromptUpdateProps = z.infer<typeof PromptUpdateProps>;
|
||||
export const PromptUpdateProps = z.object({
|
||||
id: z.string(),
|
||||
content: z.string().optional(),
|
||||
isNewConversationDefault: z.boolean().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
isShared: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type PerformBulkActionRequestBody = z.infer<typeof PerformBulkActionRequestBody>;
|
||||
export const PerformBulkActionRequestBody = z.object({
|
||||
delete: BulkActionBase.optional(),
|
||||
create: z.array(PromptCreateProps).optional(),
|
||||
update: z.array(PromptUpdateProps).optional(),
|
||||
});
|
||||
export type PerformBulkActionRequestBodyInput = z.input<typeof PerformBulkActionRequestBody>;
|
||||
|
||||
export type PerformBulkActionResponse = z.infer<typeof PerformBulkActionResponse>;
|
||||
export const PerformBulkActionResponse = BulkCrudActionResponse;
|
|
@ -0,0 +1,259 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Bulk Actions API endpoint
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/elastic_assistant/prompts/_bulk_action:
|
||||
post:
|
||||
operationId: PerformBulkAction
|
||||
x-codegen-enabled: true
|
||||
summary: Applies a bulk action to multiple prompts
|
||||
description: The bulk action is applied to all prompts that match the filter or to the list of prompts by their IDs.
|
||||
tags:
|
||||
- Bulk API
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
delete:
|
||||
$ref: '#/components/schemas/BulkActionBase'
|
||||
create:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PromptCreateProps'
|
||||
update:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PromptUpdateProps'
|
||||
responses:
|
||||
200:
|
||||
description: Indicates a successful call.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BulkCrudActionResponse'
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
BulkActionSkipReason:
|
||||
type: string
|
||||
enum:
|
||||
- PROMPT_FIELD_NOT_MODIFIED
|
||||
|
||||
BulkActionSkipResult:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
skip_reason:
|
||||
$ref: '#/components/schemas/BulkActionSkipReason'
|
||||
required:
|
||||
- id
|
||||
- skip_reason
|
||||
|
||||
PromptDetailsInError:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
|
||||
NormalizedPromptError:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
err_code:
|
||||
type: string
|
||||
prompts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PromptDetailsInError'
|
||||
required:
|
||||
- message
|
||||
- status_code
|
||||
- prompts
|
||||
|
||||
PromptResponse:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- promptType
|
||||
- content
|
||||
properties:
|
||||
id:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/UUID'
|
||||
'timestamp':
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/NonEmptyString'
|
||||
name:
|
||||
type: string
|
||||
promptType:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
isNewConversationDefault:
|
||||
type: boolean
|
||||
isDefault:
|
||||
type: boolean
|
||||
isShared:
|
||||
type: boolean
|
||||
updatedAt:
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
createdAt:
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: '../conversations/common_attributes.schema.yaml#/components/schemas/User'
|
||||
namespace:
|
||||
type: string
|
||||
description: Kibana space
|
||||
|
||||
BulkCrudActionResults:
|
||||
type: object
|
||||
properties:
|
||||
updated:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PromptResponse'
|
||||
created:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/PromptResponse'
|
||||
deleted:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
skipped:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BulkActionSkipResult'
|
||||
required:
|
||||
- updated
|
||||
- created
|
||||
- deleted
|
||||
- skipped
|
||||
|
||||
BulkCrudActionSummary:
|
||||
type: object
|
||||
properties:
|
||||
failed:
|
||||
type: integer
|
||||
skipped:
|
||||
type: integer
|
||||
succeeded:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
required:
|
||||
- failed
|
||||
- skipped
|
||||
- succeeded
|
||||
- total
|
||||
|
||||
BulkCrudActionResponse:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
status_code:
|
||||
type: integer
|
||||
message:
|
||||
type: string
|
||||
prompts_count:
|
||||
type: integer
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
$ref: '#/components/schemas/BulkCrudActionResults'
|
||||
summary:
|
||||
$ref: '#/components/schemas/BulkCrudActionSummary'
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/NormalizedPromptError'
|
||||
required:
|
||||
- results
|
||||
- summary
|
||||
required:
|
||||
- attributes
|
||||
|
||||
|
||||
BulkActionBase:
|
||||
x-inline: true
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: Query to filter promps
|
||||
ids:
|
||||
type: array
|
||||
description: Array of prompts IDs
|
||||
minItems: 1
|
||||
items:
|
||||
type: string
|
||||
|
||||
PromptCreateProps:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- content
|
||||
- promptType
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
promptType:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
isNewConversationDefault:
|
||||
type: boolean
|
||||
isDefault:
|
||||
type: boolean
|
||||
isShared:
|
||||
type: boolean
|
||||
|
||||
PromptUpdateProps:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
isNewConversationDefault:
|
||||
type: boolean
|
||||
isDefault:
|
||||
type: boolean
|
||||
isShared:
|
||||
type: boolean
|
||||
|
|
@ -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 { z } from 'zod';
|
||||
import { ArrayFromString } from '@kbn/zod-helpers';
|
||||
|
||||
/*
|
||||
* NOTICE: Do not edit this file manually.
|
||||
* This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator.
|
||||
*
|
||||
* info:
|
||||
* title: Find Prompts API endpoint
|
||||
* version: 2023-10-31
|
||||
*/
|
||||
|
||||
import { PromptResponse } from './bulk_crud_prompts_route.gen';
|
||||
|
||||
export type FindPromptsSortField = z.infer<typeof FindPromptsSortField>;
|
||||
export const FindPromptsSortField = z.enum(['created_at', 'is_default', 'name', 'updated_at']);
|
||||
export type FindPromptsSortFieldEnum = typeof FindPromptsSortField.enum;
|
||||
export const FindPromptsSortFieldEnum = FindPromptsSortField.enum;
|
||||
|
||||
export type SortOrder = z.infer<typeof SortOrder>;
|
||||
export const SortOrder = z.enum(['asc', 'desc']);
|
||||
export type SortOrderEnum = typeof SortOrder.enum;
|
||||
export const SortOrderEnum = SortOrder.enum;
|
||||
|
||||
export type FindPromptsRequestQuery = z.infer<typeof FindPromptsRequestQuery>;
|
||||
export const FindPromptsRequestQuery = z.object({
|
||||
fields: ArrayFromString(z.string()).optional(),
|
||||
/**
|
||||
* Search query
|
||||
*/
|
||||
filter: z.string().optional(),
|
||||
/**
|
||||
* Field to sort by
|
||||
*/
|
||||
sort_field: FindPromptsSortField.optional(),
|
||||
/**
|
||||
* Sort order
|
||||
*/
|
||||
sort_order: SortOrder.optional(),
|
||||
/**
|
||||
* Page number
|
||||
*/
|
||||
page: z.coerce.number().int().min(1).optional().default(1),
|
||||
/**
|
||||
* Prompts per page
|
||||
*/
|
||||
per_page: z.coerce.number().int().min(0).optional().default(20),
|
||||
});
|
||||
export type FindPromptsRequestQueryInput = z.input<typeof FindPromptsRequestQuery>;
|
||||
|
||||
export type FindPromptsResponse = z.infer<typeof FindPromptsResponse>;
|
||||
export const FindPromptsResponse = z.object({
|
||||
page: z.number().int(),
|
||||
perPage: z.number().int(),
|
||||
total: z.number().int(),
|
||||
data: z.array(PromptResponse),
|
||||
});
|
|
@ -0,0 +1,108 @@
|
|||
openapi: 3.0.0
|
||||
info:
|
||||
title: Find Prompts API endpoint
|
||||
version: '2023-10-31'
|
||||
paths:
|
||||
/api/elastic_assistant/prompts/_find:
|
||||
get:
|
||||
operationId: FindPrompts
|
||||
x-codegen-enabled: true
|
||||
description: Finds prompts that match the given query.
|
||||
summary: Finds prompts that match the given query.
|
||||
tags:
|
||||
- Prompts API
|
||||
parameters:
|
||||
- name: 'fields'
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
- name: 'filter'
|
||||
in: query
|
||||
description: Search query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: 'sort_field'
|
||||
in: query
|
||||
description: Field to sort by
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/FindPromptsSortField'
|
||||
- name: 'sort_order'
|
||||
in: query
|
||||
description: Sort order
|
||||
required: false
|
||||
schema:
|
||||
$ref: '#/components/schemas/SortOrder'
|
||||
- name: 'page'
|
||||
in: query
|
||||
description: Page number
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: 'per_page'
|
||||
in: query
|
||||
description: Prompts per page
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 20
|
||||
|
||||
responses:
|
||||
200:
|
||||
description: Successful response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
page:
|
||||
type: integer
|
||||
perPage:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: './bulk_crud_prompts_route.schema.yaml#/components/schemas/PromptResponse'
|
||||
required:
|
||||
- page
|
||||
- perPage
|
||||
- total
|
||||
- data
|
||||
400:
|
||||
description: Generic Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
statusCode:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
FindPromptsSortField:
|
||||
type: string
|
||||
enum:
|
||||
- 'created_at'
|
||||
- 'is_default'
|
||||
- 'name'
|
||||
- 'updated_at'
|
||||
|
||||
SortOrder:
|
||||
type: string
|
||||
enum:
|
||||
- 'asc'
|
||||
- 'desc'
|
|
@ -21,3 +21,9 @@ export {
|
|||
} from './impl/data_anonymization/helpers';
|
||||
|
||||
export { transformRawData } from './impl/data_anonymization/transform_raw_data';
|
||||
export {
|
||||
replaceAnonymizedValuesWithOriginalValues,
|
||||
replaceOriginalValuesWithUuidValues,
|
||||
} from './impl/data_anonymization/helpers';
|
||||
|
||||
export * from './constants';
|
||||
|
|
|
@ -16,5 +16,8 @@
|
|||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/zod-helpers",
|
||||
"@kbn/securitysolution-io-ts-utils",
|
||||
"@kbn/core",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import {
|
||||
DeleteConversationParams,
|
||||
GetConversationByIdParams,
|
||||
deleteConversation,
|
||||
getConversationById,
|
||||
} from './conversations';
|
||||
import { HttpSetupMock } from '@kbn/core-http-browser-mocks';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
|
||||
let http: HttpSetupMock = coreMock.createSetup().http;
|
||||
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
|
||||
describe('conversations api', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
http = coreMock.createSetup().http;
|
||||
});
|
||||
|
||||
it('should call api to delete conversation', async () => {
|
||||
await act(async () => {
|
||||
const deleteProps = { http, toasts, id: 'test' } as unknown as DeleteConversationParams;
|
||||
|
||||
const { waitForNextUpdate } = renderHook(() => deleteConversation(deleteProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(deleteProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/api/elastic_assistant/current_user/conversations/test',
|
||||
{
|
||||
method: 'DELETE',
|
||||
signal: undefined,
|
||||
version: '2023-10-31',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast when delete api throws error', async () => {
|
||||
http.fetch.mockRejectedValue(new Error('this is an error'));
|
||||
const deleteProps = { http, toasts, id: 'test' } as unknown as DeleteConversationParams;
|
||||
|
||||
await expect(deleteConversation(deleteProps)).rejects.toThrowError('this is an error');
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call api to get conversation', async () => {
|
||||
await act(async () => {
|
||||
const getProps = { http, toasts, id: 'test' } as unknown as GetConversationByIdParams;
|
||||
const { waitForNextUpdate } = renderHook(() => getConversationById(getProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(getProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/api/elastic_assistant/current_user/conversations/test',
|
||||
{
|
||||
method: 'GET',
|
||||
signal: undefined,
|
||||
version: '2023-10-31',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display error toast when get api throws error', async () => {
|
||||
http.fetch.mockRejectedValue(new Error('this is an error'));
|
||||
const getProps = { http, toasts, id: 'test' } as unknown as GetConversationByIdParams;
|
||||
|
||||
await expect(getConversationById(getProps)).rejects.toThrowError('this is an error');
|
||||
expect(toasts.addError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* 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, IToasts } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
ApiConfig,
|
||||
Replacement,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { Conversation, Message } from '../../../assistant_context/types';
|
||||
|
||||
export interface GetConversationByIdParams {
|
||||
http: HttpSetup;
|
||||
id: string;
|
||||
toasts?: IToasts;
|
||||
signal?: AbortSignal | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* API call for getting conversation by id.
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {HttpSetup} options.http - HttpSetup
|
||||
* @param {string} options.id - Conversation id.
|
||||
* @param {IToasts} [options.toasts] - IToasts
|
||||
* @param {AbortSignal} [options.signal] - AbortSignal
|
||||
*
|
||||
* @returns {Promise<Conversation>}
|
||||
*/
|
||||
export const getConversationById = async ({
|
||||
http,
|
||||
id,
|
||||
signal,
|
||||
toasts,
|
||||
}: GetConversationByIdParams): Promise<Conversation | undefined> => {
|
||||
try {
|
||||
const response = await http.fetch(`${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/${id}`, {
|
||||
method: 'GET',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
signal,
|
||||
});
|
||||
|
||||
return response as Conversation;
|
||||
} catch (error) {
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate('xpack.elasticAssistant.conversations.getConversationError', {
|
||||
defaultMessage: 'Error fetching conversation by id {id}',
|
||||
values: { id },
|
||||
}),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PostConversationParams {
|
||||
http: HttpSetup;
|
||||
conversation: Conversation;
|
||||
toasts?: IToasts;
|
||||
signal?: AbortSignal | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* API call for setting up the Conversation.
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {HttpSetup} options.http - HttpSetup
|
||||
* @param {Conversation} [options.conversation] - Conversation to be added
|
||||
* @param {AbortSignal} [options.signal] - AbortSignal
|
||||
* @param {IToasts} [options.toasts] - IToasts
|
||||
*
|
||||
* @returns {Promise<PostConversationResponse>}
|
||||
*/
|
||||
export const createConversation = async ({
|
||||
http,
|
||||
conversation,
|
||||
signal,
|
||||
toasts,
|
||||
}: PostConversationParams): Promise<Conversation> => {
|
||||
try {
|
||||
const response = await http.post(ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL, {
|
||||
body: JSON.stringify(conversation),
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
signal,
|
||||
});
|
||||
|
||||
return response as Conversation;
|
||||
} catch (error) {
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate('xpack.elasticAssistant.conversations.createConversationError', {
|
||||
defaultMessage: 'Error creating conversation with title {title}',
|
||||
values: { title: conversation.title },
|
||||
}),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface DeleteConversationParams {
|
||||
http: HttpSetup;
|
||||
id: string;
|
||||
toasts?: IToasts;
|
||||
signal?: AbortSignal | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* API call for deleting the Conversation. Provide a id to delete that specific resource.
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {HttpSetup} options.http - HttpSetup
|
||||
* @param {string} [options.title] - Conversation title to be deleted
|
||||
* @param {AbortSignal} [options.signal] - AbortSignal
|
||||
* @param {IToasts} [options.toasts] - IToasts
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export const deleteConversation = async ({
|
||||
http,
|
||||
id,
|
||||
signal,
|
||||
toasts,
|
||||
}: DeleteConversationParams): Promise<boolean> => {
|
||||
try {
|
||||
const response = await http.fetch(`${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/${id}`, {
|
||||
method: 'DELETE',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
signal,
|
||||
});
|
||||
|
||||
return response as boolean;
|
||||
} catch (error) {
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate('xpack.elasticAssistant.conversations.deleteConversationError', {
|
||||
defaultMessage: 'Error deleting conversation by id {id}',
|
||||
values: { id },
|
||||
}),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export interface PutConversationMessageParams {
|
||||
http: HttpSetup;
|
||||
toasts?: IToasts;
|
||||
conversationId: string;
|
||||
title?: string;
|
||||
messages?: Message[];
|
||||
apiConfig?: ApiConfig;
|
||||
replacements?: Replacement[];
|
||||
excludeFromLastConversationStorage?: boolean;
|
||||
signal?: AbortSignal | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* API call for updating conversation.
|
||||
*
|
||||
* @param {PutConversationMessageParams} options - The options object.
|
||||
* @param {HttpSetup} options.http - HttpSetup
|
||||
* @param {string} [options.title] - Conversation title
|
||||
* @param {boolean} [options.excludeFromLastConversationStorage] - Conversation excludeFromLastConversationStorage
|
||||
* @param {ApiConfig} [options.apiConfig] - Conversation apiConfig
|
||||
* @param {Message[]} [options.messages] - Conversation messages
|
||||
* @param {IToasts} [options.toasts] - IToasts
|
||||
* @param {AbortSignal} [options.signal] - AbortSignal
|
||||
*
|
||||
* @returns {Promise<Conversation>}
|
||||
*/
|
||||
export const updateConversation = async ({
|
||||
http,
|
||||
toasts,
|
||||
title,
|
||||
conversationId,
|
||||
messages,
|
||||
apiConfig,
|
||||
replacements,
|
||||
excludeFromLastConversationStorage,
|
||||
signal,
|
||||
}: PutConversationMessageParams): Promise<Conversation> => {
|
||||
try {
|
||||
const response = await http.fetch(
|
||||
`${ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL}/${conversationId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
id: conversationId,
|
||||
title,
|
||||
messages,
|
||||
replacements,
|
||||
apiConfig,
|
||||
excludeFromLastConversationStorage,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
||||
return response as Conversation;
|
||||
} catch (error) {
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate('xpack.elasticAssistant.conversations.updateConversationError', {
|
||||
defaultMessage: 'Error updating conversation by id {conversationId}',
|
||||
values: { conversationId },
|
||||
}),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
|
@ -5,9 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
/** Validates the URL path of a POST request to the `/knowledge_base/{resource}` endpoint */
|
||||
export const PostKnowledgeBasePathParams = t.type({
|
||||
resource: t.union([t.string, t.undefined]),
|
||||
});
|
||||
export * from './conversations';
|
||||
export * from './use_bulk_actions_conversations';
|
||||
export * from './use_fetch_current_user_conversations';
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 { bulkChangeConversations } from './use_bulk_actions_conversations';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
|
||||
const conversation1 = {
|
||||
id: 'conversation1',
|
||||
title: 'Conversation 1',
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'default',
|
||||
messages: [
|
||||
{
|
||||
id: 'message1',
|
||||
role: 'user' as const,
|
||||
content: 'Hello',
|
||||
timestamp: '2024-02-14T19:58:30.299Z',
|
||||
},
|
||||
{
|
||||
id: 'message2',
|
||||
role: 'user' as const,
|
||||
content: 'How are you?',
|
||||
timestamp: '2024-02-14T19:58:30.299Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
const conversation2 = {
|
||||
...conversation1,
|
||||
id: 'conversation2',
|
||||
title: 'Conversation 2',
|
||||
};
|
||||
const toasts = {
|
||||
addError: jest.fn(),
|
||||
};
|
||||
describe('bulkChangeConversations', () => {
|
||||
let httpMock: ReturnType<typeof httpServiceMock.createSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock = httpServiceMock.createSetupContract();
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should send a POST request with the correct parameters and receive a successful response', async () => {
|
||||
const conversationsActions = {
|
||||
create: {},
|
||||
update: {},
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
body: JSON.stringify({
|
||||
update: [],
|
||||
create: [],
|
||||
delete: { ids: [] },
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform the conversations dictionary to an array of conversations to create', async () => {
|
||||
const conversationsActions = {
|
||||
create: {
|
||||
conversation1,
|
||||
conversation2,
|
||||
},
|
||||
update: {},
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
body: JSON.stringify({
|
||||
update: [],
|
||||
create: [conversation1, conversation2],
|
||||
delete: { ids: [] },
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform the conversations dictionary to an array of conversations to update', async () => {
|
||||
const conversationsActions = {
|
||||
update: {
|
||||
conversation1,
|
||||
conversation2,
|
||||
},
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions);
|
||||
|
||||
expect(httpMock.fetch).toHaveBeenCalledWith(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
body: JSON.stringify({
|
||||
update: [conversation1, conversation2],
|
||||
delete: { ids: [] },
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error with the correct message when receiving an unsuccessful response', async () => {
|
||||
httpMock.fetch.mockResolvedValue({
|
||||
success: false,
|
||||
attributes: {
|
||||
errors: [
|
||||
{
|
||||
statusCode: 400,
|
||||
message: 'Error updating conversations',
|
||||
conversations: [{ id: conversation1.id, name: conversation1.title }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const conversationsActions = {
|
||||
create: {},
|
||||
update: {},
|
||||
delete: { ids: [] },
|
||||
};
|
||||
await bulkChangeConversations(httpMock, conversationsActions, toasts as unknown as IToasts);
|
||||
expect(toasts.addError.mock.calls[0][0]).toEqual(
|
||||
new Error('Error message: Error updating conversations for conversation Conversation 1')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle cases where result.attributes.errors is undefined', async () => {
|
||||
httpMock.fetch.mockResolvedValue({
|
||||
success: false,
|
||||
attributes: {},
|
||||
});
|
||||
const conversationsActions = {
|
||||
create: {},
|
||||
update: {},
|
||||
delete: { ids: [] },
|
||||
};
|
||||
|
||||
await bulkChangeConversations(httpMock, conversationsActions, toasts as unknown as IToasts);
|
||||
expect(toasts.addError.mock.calls[0][0]).toEqual(new Error(''));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HttpSetup, IToasts } from '@kbn/core/public';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
ApiConfig,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { Conversation, Message } from '../../../assistant_context/types';
|
||||
|
||||
export interface BulkActionSummary {
|
||||
failed: number;
|
||||
skipped: number;
|
||||
succeeded: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface BulkActionResult {
|
||||
updated: Conversation[];
|
||||
created: Conversation[];
|
||||
deleted: Conversation[];
|
||||
skipped: Conversation[];
|
||||
}
|
||||
|
||||
export interface BulkActionAggregatedError {
|
||||
message: string;
|
||||
status_code: number;
|
||||
err_code?: string;
|
||||
conversations: Array<{ id: string; name?: string }>;
|
||||
}
|
||||
|
||||
export interface BulkActionAttributes {
|
||||
summary: BulkActionSummary;
|
||||
results: BulkActionResult;
|
||||
errors?: BulkActionAggregatedError[];
|
||||
}
|
||||
|
||||
export interface BulkActionResponse {
|
||||
success?: boolean;
|
||||
conversations_count?: number;
|
||||
message?: string;
|
||||
statusCode?: number;
|
||||
attributes: BulkActionAttributes;
|
||||
}
|
||||
|
||||
export interface ConversationUpdateParams {
|
||||
id?: string;
|
||||
title?: string;
|
||||
messages?: Message[];
|
||||
apiConfig?: ApiConfig;
|
||||
}
|
||||
|
||||
export interface ConversationsBulkActions {
|
||||
update?: Record<string, ConversationUpdateParams>;
|
||||
create?: Record<string, Conversation>;
|
||||
delete?: {
|
||||
ids: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const transformCreateActions = (
|
||||
createActions: Record<string, Conversation>,
|
||||
conversationIdsToDelete?: string[]
|
||||
) =>
|
||||
Object.keys(createActions).reduce((conversationsToCreate: Conversation[], conversationId) => {
|
||||
if (createActions && !conversationIdsToDelete?.includes(conversationId)) {
|
||||
conversationsToCreate.push(createActions[conversationId]);
|
||||
}
|
||||
return conversationsToCreate;
|
||||
}, []);
|
||||
|
||||
const transformUpdateActions = (
|
||||
updateActions: Record<string, ConversationUpdateParams>,
|
||||
conversationIdsToDelete?: string[]
|
||||
) =>
|
||||
Object.keys(updateActions).reduce(
|
||||
(conversationsToUpdate: ConversationUpdateParams[], conversationId) => {
|
||||
if (updateActions && !conversationIdsToDelete?.includes(conversationId)) {
|
||||
conversationsToUpdate.push({
|
||||
id: conversationId,
|
||||
...updateActions[conversationId],
|
||||
});
|
||||
}
|
||||
return conversationsToUpdate;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
export const bulkChangeConversations = async (
|
||||
http: HttpSetup,
|
||||
conversationsActions: ConversationsBulkActions,
|
||||
toasts?: IToasts
|
||||
) => {
|
||||
// transform conversations disctionary to array of Conversations to create
|
||||
// filter marked as deleted
|
||||
const conversationsToCreate = conversationsActions.create
|
||||
? transformCreateActions(conversationsActions.create, conversationsActions.delete?.ids)
|
||||
: undefined;
|
||||
|
||||
// transform conversations disctionary to array of Conversations to update
|
||||
// filter marked as deleted
|
||||
const conversationsToUpdate = conversationsActions.update
|
||||
? transformUpdateActions(conversationsActions.update, conversationsActions.delete?.ids)
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
const result = await http.fetch<BulkActionResponse>(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_BULK_ACTION,
|
||||
{
|
||||
method: 'POST',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
body: JSON.stringify({
|
||||
update: conversationsToUpdate,
|
||||
create: conversationsToCreate,
|
||||
delete: conversationsActions.delete,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const serverError = result.attributes.errors
|
||||
?.map(
|
||||
(e) =>
|
||||
`${e.status_code ? `Error code: ${e.status_code}. ` : ''}Error message: ${
|
||||
e.message
|
||||
} for conversation ${e.conversations.map((c) => c.name).join(',')}`
|
||||
)
|
||||
.join(',\n');
|
||||
throw new Error(serverError);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
toasts?.addError(error.body && error.body.message ? new Error(error.body.message) : error, {
|
||||
title: i18n.translate('xpack.elasticAssistant.conversations.bulkActionsConversationsError', {
|
||||
defaultMessage: 'Error updating conversations {error}',
|
||||
values: { error },
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
UseFetchCurrentUserConversationsParams,
|
||||
useFetchCurrentUserConversations,
|
||||
} from './use_fetch_current_user_conversations';
|
||||
|
||||
const statusResponse = { assistantModelEvaluation: true, assistantStreamingEnabled: false };
|
||||
|
||||
const http = {
|
||||
fetch: jest.fn().mockResolvedValue(statusResponse),
|
||||
};
|
||||
const onFetch = jest.fn();
|
||||
|
||||
const defaultProps = { http, onFetch } as unknown as UseFetchCurrentUserConversationsParams;
|
||||
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient();
|
||||
// eslint-disable-next-line react/display-name
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useFetchCurrentUserConversations', () => {
|
||||
it(`should make http request to fetch conversations`, async () => {
|
||||
renderHook(() => useFetchCurrentUserConversations(defaultProps), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook(() =>
|
||||
useFetchCurrentUserConversations(defaultProps)
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(defaultProps.http.fetch).toHaveBeenCalledWith(
|
||||
'/api/elastic_assistant/current_user/conversations/_find',
|
||||
{
|
||||
method: 'GET',
|
||||
query: {
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
},
|
||||
version: '2023-10-31',
|
||||
signal: undefined,
|
||||
}
|
||||
);
|
||||
|
||||
expect(onFetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { HttpSetup } from '@kbn/core/public';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
} from '@kbn/elastic-assistant-common';
|
||||
import { Conversation } from '../../../assistant_context/types';
|
||||
|
||||
export interface FetchConversationsResponse {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
data: Conversation[];
|
||||
}
|
||||
|
||||
export interface UseFetchCurrentUserConversationsParams {
|
||||
http: HttpSetup;
|
||||
onFetch: (result: FetchConversationsResponse) => Record<string, Conversation>;
|
||||
signal?: AbortSignal | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* API call for fetching assistant conversations for the current user
|
||||
*
|
||||
* @param {Object} options - The options object.
|
||||
* @param {HttpSetup} options.http - HttpSetup
|
||||
* @param {Function} [options.onFetch] - transformation function for conversations fetch result
|
||||
* @param {AbortSignal} [options.signal] - AbortSignal
|
||||
*
|
||||
* @returns {useQuery} hook for getting the status of the conversations
|
||||
*/
|
||||
export const useFetchCurrentUserConversations = ({
|
||||
http,
|
||||
onFetch,
|
||||
signal,
|
||||
}: UseFetchCurrentUserConversationsParams) => {
|
||||
const query = {
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
};
|
||||
|
||||
const cachingKeys = [
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
|
||||
query.page,
|
||||
query.perPage,
|
||||
ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
];
|
||||
|
||||
return useQuery([cachingKeys, query], async () => {
|
||||
const res = await http.fetch<FetchConversationsResponse>(
|
||||
ELASTIC_AI_ASSISTANT_CONVERSATIONS_URL_FIND,
|
||||
{
|
||||
method: 'GET',
|
||||
version: ELASTIC_AI_ASSISTANT_API_CURRENT_VERSION,
|
||||
query,
|
||||
signal,
|
||||
}
|
||||
);
|
||||
return onFetch(res);
|
||||
});
|
||||
};
|
|
@ -14,9 +14,9 @@ import {
|
|||
FetchConnectorExecuteAction,
|
||||
getKnowledgeBaseStatus,
|
||||
postKnowledgeBase,
|
||||
} from './api';
|
||||
import type { Conversation, Message } from '../assistant_context/types';
|
||||
import { API_ERROR } from './translations';
|
||||
} from '.';
|
||||
import type { Conversation } from '../../assistant_context/types';
|
||||
import { API_ERROR } from '../translations';
|
||||
|
||||
jest.mock('@kbn/core-http-browser');
|
||||
|
||||
|
@ -26,21 +26,20 @@ const mockHttp = {
|
|||
|
||||
const apiConfig: Conversation['apiConfig'] = {
|
||||
connectorId: 'foo',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
model: 'gpt-4',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
};
|
||||
|
||||
const messages: Message[] = [
|
||||
{ content: 'This is a test', role: 'user', timestamp: new Date().toLocaleString() },
|
||||
];
|
||||
const fetchConnectorArgs: FetchConnectorExecuteAction = {
|
||||
isEnabledRAGAlerts: false,
|
||||
apiConfig,
|
||||
isEnabledKnowledgeBase: true,
|
||||
assistantStreamingEnabled: true,
|
||||
http: mockHttp,
|
||||
messages,
|
||||
onNewReplacements: jest.fn(),
|
||||
message: 'This is a test',
|
||||
conversationId: 'test',
|
||||
replacements: [],
|
||||
};
|
||||
describe('API tests', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -54,10 +53,11 @@ describe('API tests', () => {
|
|||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"},"isEnabledKnowledgeBase":true,"isEnabledRAGAlerts":false}',
|
||||
body: '{"model":"gpt-4","message":"This is a test","subAction":"invokeAI","conversationId":"test","replacements":[],"isEnabledKnowledgeBase":true,"isEnabledRAGAlerts":false,"llmType":"openai"}',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -73,11 +73,12 @@ describe('API tests', () => {
|
|||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeStream"},"isEnabledKnowledgeBase":false,"isEnabledRAGAlerts":false}',
|
||||
body: '{"model":"gpt-4","message":"This is a test","subAction":"invokeStream","conversationId":"test","replacements":[],"isEnabledKnowledgeBase":false,"isEnabledRAGAlerts":false,"llmType":"openai"}',
|
||||
method: 'POST',
|
||||
asResponse: true,
|
||||
rawResponse: true,
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -89,7 +90,7 @@ describe('API tests', () => {
|
|||
alertsIndexPattern: '.alerts-security.alerts-default',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { auuid: 'real.hostname' },
|
||||
replacements: [{ uuid: 'auuid', value: 'real.hostname' }],
|
||||
size: 30,
|
||||
};
|
||||
|
||||
|
@ -98,12 +99,13 @@ describe('API tests', () => {
|
|||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"},"isEnabledKnowledgeBase":true,"isEnabledRAGAlerts":true,"alertsIndexPattern":".alerts-security.alerts-default","allow":["a","b","c"],"allowReplacement":["b","c"],"replacements":{"auuid":"real.hostname"},"size":30}',
|
||||
body: '{"model":"gpt-4","message":"This is a test","subAction":"invokeAI","conversationId":"test","replacements":[{"uuid":"auuid","value":"real.hostname"}],"isEnabledKnowledgeBase":true,"isEnabledRAGAlerts":true,"llmType":"openai","alertsIndexPattern":".alerts-security.alerts-default","allow":["a","b","c"],"allowReplacement":["b","c"],"size":30}',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -120,12 +122,13 @@ describe('API tests', () => {
|
|||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"},"isEnabledKnowledgeBase":false,"isEnabledRAGAlerts":false}',
|
||||
body: '{"model":"gpt-4","message":"This is a test","subAction":"invokeAI","conversationId":"test","replacements":[],"isEnabledKnowledgeBase":false,"isEnabledRAGAlerts":false,"llmType":"openai"}',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -142,12 +145,13 @@ describe('API tests', () => {
|
|||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/internal/elastic_assistant/actions/connector/foo/_execute',
|
||||
{
|
||||
body: '{"params":{"subActionParams":{"model":"gpt-4","messages":[{"role":"user","content":"This is a test"}],"n":1,"stop":null,"temperature":0.2},"subAction":"invokeAI"},"isEnabledKnowledgeBase":false,"isEnabledRAGAlerts":true}',
|
||||
body: '{"model":"gpt-4","message":"This is a test","subAction":"invokeAI","conversationId":"test","replacements":[],"isEnabledKnowledgeBase":false,"isEnabledRAGAlerts":true,"llmType":"openai"}',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -224,23 +228,6 @@ describe('API tests', () => {
|
|||
expect(result).toEqual({ response: API_ERROR, isStream: false, isError: true });
|
||||
});
|
||||
|
||||
it('returns the value of the action_input property when isEnabledKnowledgeBase is true, and `content` has properly prefixed and suffixed JSON with the action_input property', async () => {
|
||||
const response = '```json\n{"action_input": "value from action_input"}\n```';
|
||||
|
||||
(mockHttp.fetch as jest.Mock).mockResolvedValue({
|
||||
status: 'ok',
|
||||
data: response,
|
||||
});
|
||||
|
||||
const result = await fetchConnectorExecuteAction(fetchConnectorArgs);
|
||||
|
||||
expect(result).toEqual({
|
||||
response: 'value from action_input',
|
||||
isStream: false,
|
||||
isError: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the original content when isEnabledKnowledgeBase is true, and `content` has properly formatted JSON WITHOUT the action_input property', async () => {
|
||||
const response = '```json\n{"some_key": "some value"}\n```';
|
||||
|
||||
|
@ -281,6 +268,7 @@ describe('API tests', () => {
|
|||
{
|
||||
method: 'GET',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -305,6 +293,7 @@ describe('API tests', () => {
|
|||
{
|
||||
method: 'POST',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -327,6 +316,7 @@ describe('API tests', () => {
|
|||
{
|
||||
method: 'DELETE',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
|
@ -5,30 +5,25 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
|
||||
|
||||
import { HttpSetup, IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { Conversation, Message } from '../assistant_context/types';
|
||||
import { API_ERROR } from './translations';
|
||||
import { MODEL_GPT_3_5_TURBO } from '../connectorland/models/model_selector/model_selector';
|
||||
import {
|
||||
getFormattedMessageContent,
|
||||
getOptionalRequestParams,
|
||||
hasParsableResponse,
|
||||
} from './helpers';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import { ApiConfig, Replacement } from '@kbn/elastic-assistant-common';
|
||||
import { API_ERROR } from '../translations';
|
||||
import { getOptionalRequestParams, llmTypeDictionary } from '../helpers';
|
||||
export * from './conversations';
|
||||
|
||||
export interface FetchConnectorExecuteAction {
|
||||
conversationId: string;
|
||||
isEnabledRAGAlerts: boolean;
|
||||
alertsIndexPattern?: string;
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
isEnabledKnowledgeBase: boolean;
|
||||
assistantStreamingEnabled: boolean;
|
||||
apiConfig: Conversation['apiConfig'];
|
||||
apiConfig: ApiConfig;
|
||||
http: HttpSetup;
|
||||
messages: Message[];
|
||||
onNewReplacements: (newReplacements: Record<string, string>) => void;
|
||||
replacements?: Record<string, string>;
|
||||
message?: string;
|
||||
replacements: Replacement[];
|
||||
signal?: AbortSignal | undefined;
|
||||
size?: number;
|
||||
}
|
||||
|
@ -44,6 +39,7 @@ export interface FetchConnectorExecuteResponse {
|
|||
}
|
||||
|
||||
export const fetchConnectorExecuteAction = async ({
|
||||
conversationId,
|
||||
isEnabledRAGAlerts,
|
||||
alertsIndexPattern,
|
||||
allow,
|
||||
|
@ -51,32 +47,13 @@ export const fetchConnectorExecuteAction = async ({
|
|||
isEnabledKnowledgeBase,
|
||||
assistantStreamingEnabled,
|
||||
http,
|
||||
messages,
|
||||
onNewReplacements,
|
||||
message,
|
||||
replacements,
|
||||
apiConfig,
|
||||
signal,
|
||||
size,
|
||||
}: FetchConnectorExecuteAction): Promise<FetchConnectorExecuteResponse> => {
|
||||
const outboundMessages = messages.map((msg) => ({
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
const body =
|
||||
apiConfig?.provider === OpenAiProviderType.OpenAi
|
||||
? {
|
||||
model: apiConfig.model ?? MODEL_GPT_3_5_TURBO,
|
||||
messages: outboundMessages,
|
||||
n: 1,
|
||||
stop: null,
|
||||
temperature: 0.2,
|
||||
}
|
||||
: {
|
||||
// Azure OpenAI and Bedrock invokeAI both expect this body format
|
||||
messages: outboundMessages,
|
||||
};
|
||||
|
||||
const llmType = llmTypeDictionary[apiConfig.connectorTypeTitle];
|
||||
// TODO: Remove in part 3 of streaming work for security solution
|
||||
// tracked here: https://github.com/elastic/security-team/issues/7363
|
||||
// In part 3 I will make enhancements to langchain to introduce streaming
|
||||
|
@ -87,29 +64,21 @@ export const fetchConnectorExecuteAction = async ({
|
|||
alertsIndexPattern,
|
||||
allow,
|
||||
allowReplacement,
|
||||
replacements,
|
||||
size,
|
||||
});
|
||||
|
||||
const requestBody = isStream
|
||||
? {
|
||||
params: {
|
||||
subActionParams: body,
|
||||
subAction: 'invokeStream',
|
||||
},
|
||||
isEnabledKnowledgeBase,
|
||||
isEnabledRAGAlerts,
|
||||
...optionalRequestParams,
|
||||
}
|
||||
: {
|
||||
params: {
|
||||
subActionParams: body,
|
||||
subAction: 'invokeAI',
|
||||
},
|
||||
isEnabledKnowledgeBase,
|
||||
isEnabledRAGAlerts,
|
||||
...optionalRequestParams,
|
||||
};
|
||||
const requestBody = {
|
||||
// only used for openai, azure and bedrock ignore field
|
||||
model: apiConfig?.model,
|
||||
message,
|
||||
subAction: isStream ? 'invokeStream' : 'invokeAI',
|
||||
conversationId,
|
||||
replacements,
|
||||
isEnabledKnowledgeBase,
|
||||
isEnabledRAGAlerts,
|
||||
llmType,
|
||||
...optionalRequestParams,
|
||||
};
|
||||
|
||||
try {
|
||||
if (isStream) {
|
||||
|
@ -121,6 +90,7 @@ export const fetchConnectorExecuteAction = async ({
|
|||
signal,
|
||||
asResponse: isStream,
|
||||
rawResponse: isStream,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -147,7 +117,7 @@ export const fetchConnectorExecuteAction = async ({
|
|||
connector_id: string;
|
||||
status: string;
|
||||
data: string;
|
||||
replacements?: Record<string, string>;
|
||||
replacements?: Replacement[];
|
||||
service_message?: string;
|
||||
trace_data?: {
|
||||
transaction_id: string;
|
||||
|
@ -158,6 +128,7 @@ export const fetchConnectorExecuteAction = async ({
|
|||
body: JSON.stringify(requestBody),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal,
|
||||
version: '1',
|
||||
});
|
||||
|
||||
if (response.status !== 'ok' || !response.data) {
|
||||
|
@ -184,15 +155,8 @@ export const fetchConnectorExecuteAction = async ({
|
|||
}
|
||||
: undefined;
|
||||
|
||||
onNewReplacements(response.replacements ?? {});
|
||||
|
||||
return {
|
||||
response: hasParsableResponse({
|
||||
isEnabledRAGAlerts,
|
||||
isEnabledKnowledgeBase,
|
||||
})
|
||||
? getFormattedMessageContent(response.data)
|
||||
: response.data,
|
||||
response: response.data,
|
||||
isError: false,
|
||||
isStream: false,
|
||||
traceData,
|
||||
|
@ -251,6 +215,7 @@ export const getKnowledgeBaseStatus = async ({
|
|||
const response = await http.fetch(path, {
|
||||
method: 'GET',
|
||||
signal,
|
||||
version: '1',
|
||||
});
|
||||
|
||||
return response as GetKnowledgeBaseStatusResponse;
|
||||
|
@ -289,6 +254,7 @@ export const postKnowledgeBase = async ({
|
|||
const response = await http.fetch(path, {
|
||||
method: 'POST',
|
||||
signal,
|
||||
version: '1',
|
||||
});
|
||||
|
||||
return response as PostKnowledgeBaseResponse;
|
||||
|
@ -327,6 +293,7 @@ export const deleteKnowledgeBase = async ({
|
|||
const response = await http.fetch(path, {
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
version: '1',
|
||||
});
|
||||
|
||||
return response as DeleteKnowledgeBaseResponse;
|
|
@ -6,13 +6,21 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act, fireEvent, render } from '@testing-library/react';
|
||||
import { AssistantHeader } from '.';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { alertConvo, emptyWelcomeConvo } from '../../mock/conversation';
|
||||
import { alertConvo, emptyWelcomeConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { useLoadConnectors } from '../../connectorland/use_load_connectors';
|
||||
import { mockConnectors } from '../../mock/connectors';
|
||||
|
||||
const onConversationSelected = jest.fn();
|
||||
const setCurrentConversation = jest.fn();
|
||||
const mockConversations = {
|
||||
[alertConvo.title]: alertConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
};
|
||||
const testProps = {
|
||||
currentConversation: emptyWelcomeConvo,
|
||||
currentConversation: welcomeConvo,
|
||||
title: 'Test Title',
|
||||
docLinks: {
|
||||
ELASTIC_WEBSITE_URL: 'https://www.elastic.co/',
|
||||
|
@ -20,15 +28,45 @@ const testProps = {
|
|||
},
|
||||
isDisabled: false,
|
||||
isSettingsModalVisible: false,
|
||||
onConversationSelected: jest.fn(),
|
||||
onConversationSelected,
|
||||
onToggleShowAnonymizedValues: jest.fn(),
|
||||
selectedConversationId: emptyWelcomeConvo.id,
|
||||
setIsSettingsModalVisible: jest.fn(),
|
||||
setSelectedConversationId: jest.fn(),
|
||||
setCurrentConversation,
|
||||
onConversationDeleted: jest.fn(),
|
||||
showAnonymizedValues: false,
|
||||
conversations: mockConversations,
|
||||
refetchConversationsState: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../../connectorland/use_load_connectors', () => ({
|
||||
useLoadConnectors: jest.fn(() => {
|
||||
return {
|
||||
data: [],
|
||||
error: null,
|
||||
isSuccess: true,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
(useLoadConnectors as jest.Mock).mockReturnValue({
|
||||
data: mockConnectors,
|
||||
error: null,
|
||||
isSuccess: true,
|
||||
});
|
||||
const mockSetApiConfig = alertConvo;
|
||||
jest.mock('../use_conversation', () => ({
|
||||
useConversation: jest.fn(() => {
|
||||
return {
|
||||
setApiConfig: jest.fn().mockReturnValue(mockSetApiConfig),
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('AssistantHeader', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('showAnonymizedValues is not checked when currentConversation.replacements is null', () => {
|
||||
const { getByText, getByTestId } = render(<AssistantHeader {...testProps} />, {
|
||||
wrapper: TestProviders,
|
||||
|
@ -41,7 +79,7 @@ describe('AssistantHeader', () => {
|
|||
const { getByText, getByTestId } = render(
|
||||
<AssistantHeader
|
||||
{...testProps}
|
||||
currentConversation={{ ...emptyWelcomeConvo, replacements: {} }}
|
||||
currentConversation={{ ...emptyWelcomeConvo, replacements: [] }}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
|
@ -53,11 +91,7 @@ describe('AssistantHeader', () => {
|
|||
|
||||
it('showAnonymizedValues is not checked when currentConversation.replacements has values and showAnonymizedValues is false', () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantHeader
|
||||
{...testProps}
|
||||
currentConversation={alertConvo}
|
||||
selectedConversationId={alertConvo.id}
|
||||
/>,
|
||||
<AssistantHeader {...testProps} currentConversation={alertConvo} />,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
|
@ -67,16 +101,28 @@ describe('AssistantHeader', () => {
|
|||
|
||||
it('showAnonymizedValues is checked when currentConversation.replacements has values and showAnonymizedValues is true', () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantHeader
|
||||
{...testProps}
|
||||
currentConversation={alertConvo}
|
||||
selectedConversationId={alertConvo.id}
|
||||
showAnonymizedValues
|
||||
/>,
|
||||
<AssistantHeader {...testProps} currentConversation={alertConvo} showAnonymizedValues />,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
expect(getByTestId('showAnonymizedValues')).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
it('Conversation is updated when connector change occurs', async () => {
|
||||
const { getByTestId } = render(<AssistantHeader {...testProps} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
fireEvent.click(getByTestId('connectorSelectorPlaceholderButton'));
|
||||
fireEvent.click(getByTestId('connector-selector'));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(getByTestId('connectorId'));
|
||||
});
|
||||
expect(setCurrentConversation).toHaveBeenCalledWith(alertConvo);
|
||||
expect(onConversationSelected).toHaveBeenCalledWith({
|
||||
cId: alertConvo.id,
|
||||
cTitle: alertConvo.title,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -17,7 +17,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantTitle } from '../assistant_title';
|
||||
import { ConversationSelector } from '../conversations/conversation_selector';
|
||||
|
@ -26,19 +26,20 @@ import * as i18n from '../translations';
|
|||
|
||||
interface OwnProps {
|
||||
currentConversation: Conversation;
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
defaultConnector?: AIConnector;
|
||||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
isDisabled: boolean;
|
||||
isSettingsModalVisible: boolean;
|
||||
onConversationSelected: (cId: string) => void;
|
||||
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
|
||||
onConversationDeleted: (conversationId: string) => void;
|
||||
onToggleShowAnonymizedValues: (e: EuiSwitchEvent) => void;
|
||||
selectedConversationId: string;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
setCurrentConversation: React.Dispatch<React.SetStateAction<Conversation>>;
|
||||
shouldDisableKeyboardShortcut?: () => boolean;
|
||||
showAnonymizedValues: boolean;
|
||||
title: string | JSX.Element;
|
||||
conversations: Record<string, Conversation>;
|
||||
refetchConversationsState: () => Promise<void>;
|
||||
}
|
||||
|
||||
type Props = OwnProps;
|
||||
|
@ -49,19 +50,20 @@ type Props = OwnProps;
|
|||
*/
|
||||
export const AssistantHeader: React.FC<Props> = ({
|
||||
currentConversation,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
defaultConnector,
|
||||
docLinks,
|
||||
isDisabled,
|
||||
isSettingsModalVisible,
|
||||
onConversationSelected,
|
||||
onConversationDeleted,
|
||||
onToggleShowAnonymizedValues,
|
||||
selectedConversationId,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedConversationId,
|
||||
shouldDisableKeyboardShortcut,
|
||||
showAnonymizedValues,
|
||||
title,
|
||||
setCurrentConversation,
|
||||
conversations,
|
||||
refetchConversationsState,
|
||||
}) => {
|
||||
const showAnonymizedValuesChecked = useMemo(
|
||||
() =>
|
||||
|
@ -70,6 +72,16 @@ export const AssistantHeader: React.FC<Props> = ({
|
|||
showAnonymizedValues,
|
||||
[currentConversation.replacements, showAnonymizedValues]
|
||||
);
|
||||
const onConversationChange = useCallback(
|
||||
(updatedConversation) => {
|
||||
setCurrentConversation(updatedConversation);
|
||||
onConversationSelected({
|
||||
cId: updatedConversation.id,
|
||||
cTitle: updatedConversation.title,
|
||||
});
|
||||
},
|
||||
[onConversationSelected, setCurrentConversation]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
|
@ -84,6 +96,7 @@ export const AssistantHeader: React.FC<Props> = ({
|
|||
isDisabled={isDisabled}
|
||||
docLinks={docLinks}
|
||||
selectedConversation={currentConversation}
|
||||
onChange={onConversationChange}
|
||||
title={title}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -95,12 +108,13 @@ export const AssistantHeader: React.FC<Props> = ({
|
|||
`}
|
||||
>
|
||||
<ConversationSelector
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
selectedConversationId={selectedConversationId}
|
||||
defaultConnector={defaultConnector}
|
||||
selectedConversationTitle={currentConversation.title}
|
||||
onConversationSelected={onConversationSelected}
|
||||
shouldDisableKeyboardShortcut={shouldDisableKeyboardShortcut}
|
||||
isDisabled={isDisabled}
|
||||
conversations={conversations}
|
||||
onConversationDeleted={onConversationDeleted}
|
||||
/>
|
||||
|
||||
<>
|
||||
|
@ -125,13 +139,14 @@ export const AssistantHeader: React.FC<Props> = ({
|
|||
|
||||
<EuiFlexItem grow={false}>
|
||||
<AssistantSettingsButton
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
defaultConnector={defaultConnector}
|
||||
isDisabled={isDisabled}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
selectedConversation={currentConversation}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
onConversationSelected={onConversationSelected}
|
||||
conversations={conversations}
|
||||
refetchConversationsState={refetchConversationsState}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -29,11 +29,12 @@ const StyledEuiModal = styled(EuiModal)`
|
|||
*/
|
||||
export const AssistantOverlay = React.memo(() => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [conversationId, setConversationId] = useState<string | undefined>(
|
||||
const [conversationTitle, setConversationTitle] = useState<string | undefined>(
|
||||
WELCOME_CONVERSATION_TITLE
|
||||
);
|
||||
const [promptContextId, setPromptContextId] = useState<string | undefined>();
|
||||
const { assistantTelemetry, setShowAssistantOverlay, getConversationId } = useAssistantContext();
|
||||
const { assistantTelemetry, setShowAssistantOverlay, getLastConversationTitle } =
|
||||
useAssistantContext();
|
||||
|
||||
// Bind `showAssistantOverlay` in SecurityAssistantContext to this modal instance
|
||||
const showOverlay = useCallback(
|
||||
|
@ -41,20 +42,20 @@ export const AssistantOverlay = React.memo(() => {
|
|||
({
|
||||
showOverlay: so,
|
||||
promptContextId: pid,
|
||||
conversationId: cid,
|
||||
conversationTitle: cTitle,
|
||||
}: ShowAssistantOverlayProps) => {
|
||||
const newConversationId = getConversationId(cid);
|
||||
const newConversationTitle = getLastConversationTitle(cTitle);
|
||||
if (so)
|
||||
assistantTelemetry?.reportAssistantInvoked({
|
||||
conversationId: newConversationId,
|
||||
conversationId: newConversationTitle,
|
||||
invokedBy: 'click',
|
||||
});
|
||||
|
||||
setIsModalVisible(so);
|
||||
setPromptContextId(pid);
|
||||
setConversationId(newConversationId);
|
||||
setConversationTitle(newConversationTitle);
|
||||
},
|
||||
[assistantTelemetry, getConversationId]
|
||||
[assistantTelemetry, getLastConversationTitle]
|
||||
);
|
||||
useEffect(() => {
|
||||
setShowAssistantOverlay(showOverlay);
|
||||
|
@ -64,15 +65,15 @@ export const AssistantOverlay = React.memo(() => {
|
|||
const handleShortcutPress = useCallback(() => {
|
||||
// Try to restore the last conversation on shortcut pressed
|
||||
if (!isModalVisible) {
|
||||
setConversationId(getConversationId());
|
||||
setConversationTitle(getLastConversationTitle());
|
||||
assistantTelemetry?.reportAssistantInvoked({
|
||||
invokedBy: 'shortcut',
|
||||
conversationId: getConversationId(),
|
||||
conversationId: getLastConversationTitle(),
|
||||
});
|
||||
}
|
||||
|
||||
setIsModalVisible(!isModalVisible);
|
||||
}, [assistantTelemetry, isModalVisible, getConversationId]);
|
||||
}, [isModalVisible, getLastConversationTitle, assistantTelemetry]);
|
||||
|
||||
// Register keyboard listener to show the modal when cmd + ; is pressed
|
||||
const onKeyDown = useCallback(
|
||||
|
@ -90,8 +91,8 @@ export const AssistantOverlay = React.memo(() => {
|
|||
const cleanupAndCloseModal = useCallback(() => {
|
||||
setIsModalVisible(false);
|
||||
setPromptContextId(undefined);
|
||||
setConversationId(conversationId);
|
||||
}, [conversationId]);
|
||||
setConversationTitle(conversationTitle);
|
||||
}, [conversationTitle]);
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
cleanupAndCloseModal();
|
||||
|
@ -101,7 +102,7 @@ export const AssistantOverlay = React.memo(() => {
|
|||
<>
|
||||
{isModalVisible && (
|
||||
<StyledEuiModal onClose={handleCloseModal} data-test-subj="ai-assistant-modal">
|
||||
<Assistant conversationId={conversationId} promptContextId={promptContextId} />
|
||||
<Assistant conversationTitle={conversationTitle} promptContextId={promptContextId} />
|
||||
</StyledEuiModal>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -14,6 +14,7 @@ const testProps = {
|
|||
title: 'Test Title',
|
||||
docLinks: { ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', DOC_LINK_VERSION: '7.15' },
|
||||
selectedConversation: undefined,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
describe('AssistantTitle', () => {
|
||||
|
|
|
@ -32,7 +32,8 @@ export const AssistantTitle: React.FC<{
|
|||
title: string | JSX.Element;
|
||||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
selectedConversation: Conversation | undefined;
|
||||
}> = ({ isDisabled = false, title, docLinks, selectedConversation }) => {
|
||||
onChange: (updatedConversation: Conversation) => void;
|
||||
}> = ({ isDisabled = false, title, docLinks, selectedConversation, onChange }) => {
|
||||
const selectedConnectorId = selectedConversation?.apiConfig?.connectorId;
|
||||
|
||||
const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks;
|
||||
|
@ -112,6 +113,7 @@ export const AssistantTitle: React.FC<{
|
|||
isDisabled={isDisabled || selectedConversation === undefined}
|
||||
selectedConnectorId={selectedConnectorId}
|
||||
selectedConversation={selectedConversation}
|
||||
onConnectorSelected={onChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { useSendMessages } from '../use_send_messages';
|
||||
import { useSendMessage } from '../use_send_message';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { emptyWelcomeConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { defaultSystemPrompt, mockSystemPrompt } from '../../mock/system_prompt';
|
||||
|
@ -14,24 +14,26 @@ import { useChatSend, UseChatSendProps } from './use_chat_send';
|
|||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { useAssistantContext } from '../../..';
|
||||
|
||||
jest.mock('../use_send_messages');
|
||||
jest.mock('../use_send_message');
|
||||
jest.mock('../use_conversation');
|
||||
jest.mock('../../..');
|
||||
|
||||
const setEditingSystemPromptId = jest.fn();
|
||||
const setPromptTextPreview = jest.fn();
|
||||
const setSelectedPromptContexts = jest.fn();
|
||||
const setUserPrompt = jest.fn();
|
||||
const sendMessages = jest.fn();
|
||||
const appendMessage = jest.fn();
|
||||
const sendMessage = jest.fn();
|
||||
const removeLastMessage = jest.fn();
|
||||
const appendReplacements = jest.fn();
|
||||
const clearConversation = jest.fn();
|
||||
const refresh = jest.fn();
|
||||
const setCurrentConversation = jest.fn();
|
||||
|
||||
export const testProps: UseChatSendProps = {
|
||||
selectedPromptContexts: {},
|
||||
allSystemPrompts: [defaultSystemPrompt, mockSystemPrompt],
|
||||
currentConversation: emptyWelcomeConvo,
|
||||
currentConversation: { ...emptyWelcomeConvo, id: 'an-id' },
|
||||
http: {
|
||||
basePath: {
|
||||
basePath: '/mfg',
|
||||
|
@ -45,23 +47,30 @@ export const testProps: UseChatSendProps = {
|
|||
setPromptTextPreview,
|
||||
setSelectedPromptContexts,
|
||||
setUserPrompt,
|
||||
refresh,
|
||||
setCurrentConversation,
|
||||
};
|
||||
const robotMessage = { response: 'Response message from the robot', isError: false };
|
||||
const reportAssistantMessageSent = jest.fn();
|
||||
describe('use chat send', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useSendMessages as jest.Mock).mockReturnValue({
|
||||
(useSendMessage as jest.Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
sendMessages: sendMessages.mockReturnValue(robotMessage),
|
||||
sendMessage: sendMessage.mockReturnValue(robotMessage),
|
||||
});
|
||||
(useConversation as jest.Mock).mockReturnValue({
|
||||
appendMessage,
|
||||
appendReplacements,
|
||||
removeLastMessage,
|
||||
clearConversation,
|
||||
});
|
||||
(useAssistantContext as jest.Mock).mockReturnValue({
|
||||
assistantTelemetry: {
|
||||
reportAssistantMessageSent,
|
||||
},
|
||||
knowledgeBase: { isEnabledKnowledgeBase: false, isEnabledRAGAlerts: false },
|
||||
});
|
||||
});
|
||||
it('handleOnChatCleared clears the conversation', () => {
|
||||
it('handleOnChatCleared clears the conversation', async () => {
|
||||
const { result } = renderHook(() => useChatSend(testProps), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
@ -70,7 +79,10 @@ describe('use chat send', () => {
|
|||
expect(setPromptTextPreview).toHaveBeenCalledWith('');
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
expect(setSelectedPromptContexts).toHaveBeenCalledWith({});
|
||||
expect(clearConversation).toHaveBeenCalledWith(testProps.currentConversation.id);
|
||||
await waitFor(() => {
|
||||
expect(clearConversation).toHaveBeenCalledWith(testProps.currentConversation.id);
|
||||
expect(refresh).toHaveBeenCalled();
|
||||
});
|
||||
expect(setEditingSystemPromptId).toHaveBeenCalledWith(defaultSystemPrompt.id);
|
||||
});
|
||||
it('handlePromptChange updates prompt successfully', () => {
|
||||
|
@ -90,21 +102,18 @@ describe('use chat send', () => {
|
|||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessages).toHaveBeenCalled();
|
||||
const appendMessageSend = appendMessage.mock.calls[0][0];
|
||||
const appendMessageResponse = appendMessage.mock.calls[1][0];
|
||||
expect(appendMessageSend.message.content).toEqual(
|
||||
expect(sendMessage).toHaveBeenCalled();
|
||||
const appendMessageSend = sendMessage.mock.calls[0][0].message;
|
||||
expect(appendMessageSend).toEqual(
|
||||
`You are a helpful, expert assistant who answers questions about Elastic Security. Do not answer questions unrelated to Elastic Security.\nIf you answer a question related to KQL or EQL, it should be immediately usable within an Elastic Security timeline; please always format the output correctly with back ticks. Any answer provided for Query DSL should also be usable in a security timeline. This means you should only ever include the "filter" portion of the query.\nUse the following context to answer questions:\n\n\n\n${promptText}`
|
||||
);
|
||||
expect(appendMessageSend.message.role).toEqual('user');
|
||||
expect(appendMessageResponse.message.content).toEqual(robotMessage.response);
|
||||
expect(appendMessageResponse.message.role).toEqual('assistant');
|
||||
});
|
||||
});
|
||||
it('handleButtonSendMessage sends message with only provided prompt text and context already exists in convo history', async () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(
|
||||
() => useChatSend({ ...testProps, currentConversation: welcomeConvo }),
|
||||
() =>
|
||||
useChatSend({ ...testProps, currentConversation: { ...welcomeConvo, id: 'welcome-id' } }),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
|
@ -114,24 +123,50 @@ describe('use chat send', () => {
|
|||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessages).toHaveBeenCalled();
|
||||
expect(appendMessage.mock.calls[0][0].message.content).toEqual(`\n\n${promptText}`);
|
||||
expect(sendMessage).toHaveBeenCalled();
|
||||
const messages = setCurrentConversation.mock.calls[0][0].messages;
|
||||
expect(messages[messages.length - 1].content).toEqual(`\n\n${promptText}`);
|
||||
});
|
||||
});
|
||||
it('handleRegenerateResponse removes the last message of the conversation, resends the convo to GenAI, and appends the message received', async () => {
|
||||
const { result } = renderHook(
|
||||
() => useChatSend({ ...testProps, currentConversation: welcomeConvo }),
|
||||
() =>
|
||||
useChatSend({ ...testProps, currentConversation: { ...welcomeConvo, id: 'welcome-id' } }),
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
result.current.handleRegenerateResponse();
|
||||
expect(removeLastMessage).toHaveBeenCalledWith('Welcome');
|
||||
expect(removeLastMessage).toHaveBeenCalledWith('welcome-id');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendMessages).toHaveBeenCalled();
|
||||
expect(appendMessage.mock.calls[0][0].message.content).toEqual(robotMessage.response);
|
||||
expect(sendMessage).toHaveBeenCalled();
|
||||
const messages = setCurrentConversation.mock.calls[1][0].messages;
|
||||
expect(messages[messages.length - 1].content).toEqual(robotMessage.response);
|
||||
});
|
||||
});
|
||||
it('sends telemetry events for both user and assistant', async () => {
|
||||
const promptText = 'prompt text';
|
||||
const { result } = renderHook(() => useChatSend(testProps), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
result.current.handleButtonSendMessage(promptText);
|
||||
expect(setUserPrompt).toHaveBeenCalledWith('');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(1, {
|
||||
conversationId: testProps.currentConversation.title,
|
||||
role: 'user',
|
||||
isEnabledKnowledgeBase: false,
|
||||
isEnabledRAGAlerts: false,
|
||||
});
|
||||
expect(reportAssistantMessageSent).toHaveBeenNthCalledWith(2, {
|
||||
conversationId: testProps.currentConversation.title,
|
||||
role: 'assistant',
|
||||
isEnabledKnowledgeBase: false,
|
||||
isEnabledRAGAlerts: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,11 +7,12 @@
|
|||
|
||||
import React, { useCallback } from 'react';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SelectedPromptContext } from '../prompt_context/types';
|
||||
import { useSendMessages } from '../use_send_messages';
|
||||
import { useSendMessage } from '../use_send_message';
|
||||
import { useConversation } from '../use_conversation';
|
||||
import { getCombinedMessage } from '../prompt/helpers';
|
||||
import { Conversation, Message, Prompt } from '../../..';
|
||||
import { Conversation, Message, Prompt, useAssistantContext } from '../../..';
|
||||
import { getMessageFromRawResponse } from '../helpers';
|
||||
import { getDefaultSystemPrompt } from '../use_conversation/helpers';
|
||||
|
||||
|
@ -27,6 +28,8 @@ export interface UseChatSendProps {
|
|||
React.SetStateAction<Record<string, SelectedPromptContext>>
|
||||
>;
|
||||
setUserPrompt: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
refresh: () => Promise<Conversation | undefined>;
|
||||
setCurrentConversation: React.Dispatch<React.SetStateAction<Conversation>>;
|
||||
}
|
||||
|
||||
export interface UseChatSend {
|
||||
|
@ -53,10 +56,17 @@ export const useChatSend = ({
|
|||
setPromptTextPreview,
|
||||
setSelectedPromptContexts,
|
||||
setUserPrompt,
|
||||
refresh,
|
||||
setCurrentConversation,
|
||||
}: UseChatSendProps): UseChatSend => {
|
||||
const { isLoading, sendMessages } = useSendMessages();
|
||||
const { appendMessage, appendReplacements, clearConversation, removeLastMessage } =
|
||||
useConversation();
|
||||
const {
|
||||
assistantTelemetry,
|
||||
knowledgeBase: { isEnabledKnowledgeBase, isEnabledRAGAlerts },
|
||||
toasts,
|
||||
} = useAssistantContext();
|
||||
|
||||
const { isLoading, sendMessage } = useSendMessage();
|
||||
const { clearConversation, removeLastMessage } = useConversation();
|
||||
|
||||
const handlePromptChange = (prompt: string) => {
|
||||
setPromptTextPreview(prompt);
|
||||
|
@ -66,88 +76,121 @@ export const useChatSend = ({
|
|||
// Handles sending latest user prompt to API
|
||||
const handleSendMessage = useCallback(
|
||||
async (promptText: string) => {
|
||||
const onNewReplacements = (newReplacements: Record<string, string>) =>
|
||||
appendReplacements({
|
||||
conversationId: currentConversation.id,
|
||||
replacements: newReplacements,
|
||||
});
|
||||
|
||||
if (!currentConversation.apiConfig) {
|
||||
toasts?.addError(
|
||||
new Error('The conversation needs a connector configured in order to send a message.'),
|
||||
{
|
||||
title: i18n.translate('xpack.elasticAssistant.knowledgeBase.setupError', {
|
||||
defaultMessage: 'Error setting up Knowledge Base',
|
||||
}),
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
const systemPrompt = allSystemPrompts.find((prompt) => prompt.id === editingSystemPromptId);
|
||||
|
||||
const message = await getCombinedMessage({
|
||||
const userMessage = getCombinedMessage({
|
||||
isNewChat: currentConversation.messages.length === 0,
|
||||
currentReplacements: currentConversation.replacements,
|
||||
onNewReplacements,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt: systemPrompt,
|
||||
});
|
||||
|
||||
const updatedMessages = appendMessage({
|
||||
conversationId: currentConversation.id,
|
||||
message,
|
||||
const replacements = userMessage.replacements ?? currentConversation.replacements;
|
||||
const updatedMessages = [...currentConversation.messages, userMessage].map((m) => ({
|
||||
...m,
|
||||
content: m.content ?? '',
|
||||
}));
|
||||
setCurrentConversation({
|
||||
...currentConversation,
|
||||
replacements,
|
||||
messages: updatedMessages,
|
||||
});
|
||||
|
||||
// Reset prompt context selection and preview before sending:
|
||||
setSelectedPromptContexts({});
|
||||
setPromptTextPreview('');
|
||||
|
||||
const rawResponse = await sendMessages({
|
||||
const rawResponse = await sendMessage({
|
||||
apiConfig: currentConversation.apiConfig,
|
||||
http,
|
||||
messages: updatedMessages,
|
||||
onNewReplacements,
|
||||
replacements: currentConversation.replacements ?? {},
|
||||
message: userMessage.content ?? '',
|
||||
conversationId: currentConversation.id,
|
||||
replacements,
|
||||
});
|
||||
|
||||
assistantTelemetry?.reportAssistantMessageSent({
|
||||
conversationId: currentConversation.title,
|
||||
role: userMessage.role,
|
||||
isEnabledKnowledgeBase,
|
||||
isEnabledRAGAlerts,
|
||||
});
|
||||
|
||||
const responseMessage: Message = getMessageFromRawResponse(rawResponse);
|
||||
appendMessage({ conversationId: currentConversation.id, message: responseMessage });
|
||||
|
||||
setCurrentConversation({
|
||||
...currentConversation,
|
||||
replacements,
|
||||
messages: [...updatedMessages, responseMessage],
|
||||
});
|
||||
assistantTelemetry?.reportAssistantMessageSent({
|
||||
conversationId: currentConversation.title,
|
||||
role: responseMessage.role,
|
||||
isEnabledKnowledgeBase,
|
||||
isEnabledRAGAlerts,
|
||||
});
|
||||
},
|
||||
[
|
||||
allSystemPrompts,
|
||||
appendMessage,
|
||||
appendReplacements,
|
||||
currentConversation.apiConfig,
|
||||
currentConversation.id,
|
||||
currentConversation.messages.length,
|
||||
currentConversation.replacements,
|
||||
assistantTelemetry,
|
||||
currentConversation,
|
||||
editingSystemPromptId,
|
||||
http,
|
||||
isEnabledKnowledgeBase,
|
||||
isEnabledRAGAlerts,
|
||||
selectedPromptContexts,
|
||||
sendMessages,
|
||||
sendMessage,
|
||||
setCurrentConversation,
|
||||
setPromptTextPreview,
|
||||
setSelectedPromptContexts,
|
||||
toasts,
|
||||
]
|
||||
);
|
||||
|
||||
const handleRegenerateResponse = useCallback(async () => {
|
||||
const onNewReplacements = (newReplacements: Record<string, string>) =>
|
||||
appendReplacements({
|
||||
conversationId: currentConversation.id,
|
||||
replacements: newReplacements,
|
||||
});
|
||||
if (!currentConversation.apiConfig) {
|
||||
toasts?.addError(
|
||||
new Error('The conversation needs a connector configured in order to send a message.'),
|
||||
{
|
||||
title: i18n.translate('xpack.elasticAssistant.knowledgeBase.setupError', {
|
||||
defaultMessage: 'Error setting up Knowledge Base',
|
||||
}),
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
// remove last message from the local state immediately
|
||||
setCurrentConversation({
|
||||
...currentConversation,
|
||||
messages: currentConversation.messages.slice(0, -1),
|
||||
});
|
||||
const updatedMessages = (await removeLastMessage(currentConversation.id)) ?? [];
|
||||
|
||||
const updatedMessages = removeLastMessage(currentConversation.id);
|
||||
|
||||
const rawResponse = await sendMessages({
|
||||
const rawResponse = await sendMessage({
|
||||
apiConfig: currentConversation.apiConfig,
|
||||
http,
|
||||
messages: updatedMessages,
|
||||
onNewReplacements,
|
||||
replacements: currentConversation.replacements ?? {},
|
||||
// do not send any new messages, the previous conversation is already stored
|
||||
conversationId: currentConversation.id,
|
||||
replacements: [],
|
||||
});
|
||||
|
||||
const responseMessage: Message = getMessageFromRawResponse(rawResponse);
|
||||
appendMessage({ conversationId: currentConversation.id, message: responseMessage });
|
||||
}, [
|
||||
appendMessage,
|
||||
appendReplacements,
|
||||
currentConversation.apiConfig,
|
||||
currentConversation.id,
|
||||
currentConversation.replacements,
|
||||
http,
|
||||
removeLastMessage,
|
||||
sendMessages,
|
||||
]);
|
||||
setCurrentConversation({
|
||||
...currentConversation,
|
||||
messages: [...updatedMessages, responseMessage],
|
||||
});
|
||||
}, [currentConversation, http, removeLastMessage, sendMessage, setCurrentConversation, toasts]);
|
||||
|
||||
const handleButtonSendMessage = useCallback(
|
||||
(message: string) => {
|
||||
|
@ -157,7 +200,7 @@ export const useChatSend = ({
|
|||
[handleSendMessage, setUserPrompt]
|
||||
);
|
||||
|
||||
const handleOnChatCleared = useCallback(() => {
|
||||
const handleOnChatCleared = useCallback(async () => {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: currentConversation,
|
||||
|
@ -166,12 +209,15 @@ export const useChatSend = ({
|
|||
setPromptTextPreview('');
|
||||
setUserPrompt('');
|
||||
setSelectedPromptContexts({});
|
||||
clearConversation(currentConversation.id);
|
||||
await clearConversation(currentConversation.id);
|
||||
await refresh();
|
||||
|
||||
setEditingSystemPromptId(defaultSystemPromptId);
|
||||
}, [
|
||||
allSystemPrompts,
|
||||
clearConversation,
|
||||
currentConversation,
|
||||
refresh,
|
||||
setEditingSystemPromptId,
|
||||
setPromptTextPreview,
|
||||
setSelectedPromptContexts,
|
||||
|
|
|
@ -25,17 +25,31 @@ const mockConversation = {
|
|||
setConversation,
|
||||
};
|
||||
|
||||
const mockConversations = {
|
||||
[alertConvo.title]: alertConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
};
|
||||
|
||||
const mockConversationsWithCustom = {
|
||||
[alertConvo.title]: alertConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
[customConvo.title]: customConvo,
|
||||
};
|
||||
|
||||
jest.mock('../../use_conversation', () => ({
|
||||
useConversation: () => mockConversation,
|
||||
}));
|
||||
|
||||
const onConversationSelected = jest.fn();
|
||||
const onConversationDeleted = jest.fn();
|
||||
const defaultProps = {
|
||||
isDisabled: false,
|
||||
onConversationSelected,
|
||||
selectedConversationId: 'Welcome',
|
||||
selectedConversationTitle: 'Welcome',
|
||||
defaultConnectorId: '123',
|
||||
defaultProvider: OpenAiProviderType.OpenAi,
|
||||
conversations: mockConversations,
|
||||
onConversationDeleted,
|
||||
};
|
||||
describe('Conversation selector', () => {
|
||||
beforeAll(() => {
|
||||
|
@ -46,47 +60,29 @@ describe('Conversation selector', () => {
|
|||
});
|
||||
it('renders with correct selected conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<TestProviders>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
expect(getByTestId('conversation-selector')).toBeInTheDocument();
|
||||
expect(getByTestId('comboBoxSearchInput')).toHaveValue(welcomeConvo.id);
|
||||
expect(getByTestId('comboBoxSearchInput')).toHaveValue(welcomeConvo.title);
|
||||
});
|
||||
it('On change, selects new item', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<TestProviders>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
fireEvent.click(getByTestId(`convo-option-${alertConvo.id}`));
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(alertConvo.id);
|
||||
fireEvent.click(getByTestId(`convo-option-${alertConvo.title}`));
|
||||
expect(onConversationSelected).toHaveBeenCalledWith({
|
||||
cId: '',
|
||||
cTitle: alertConvo.title,
|
||||
});
|
||||
});
|
||||
it('On clear input, clears selected options', () => {
|
||||
const { getByPlaceholderText, queryByPlaceholderText, getByTestId, queryByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<TestProviders>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -99,15 +95,8 @@ describe('Conversation selector', () => {
|
|||
|
||||
it('We can add a custom option', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
<TestProviders>
|
||||
<ConversationSelector {...defaultProps} conversations={mockConversationsWithCustom} />
|
||||
</TestProviders>
|
||||
);
|
||||
const customOption = 'Custom option';
|
||||
|
@ -117,102 +106,75 @@ describe('Conversation selector', () => {
|
|||
code: 'Enter',
|
||||
charCode: 13,
|
||||
});
|
||||
expect(setConversation).toHaveBeenCalledWith({
|
||||
conversation: {
|
||||
id: customOption,
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
defaultSystemPromptId: undefined,
|
||||
provider: 'OpenAI',
|
||||
},
|
||||
},
|
||||
expect(onConversationSelected).toHaveBeenCalledWith({
|
||||
cId: '',
|
||||
cTitle: customOption,
|
||||
});
|
||||
});
|
||||
|
||||
it('Only custom options can be deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
<TestProviders>
|
||||
<ConversationSelector
|
||||
{...{ ...defaultProps, conversations: mockConversationsWithCustom }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
expect(
|
||||
within(getByTestId(`convo-option-${customConvo.id}`)).getByTestId('delete-option')
|
||||
within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(getByTestId(`convo-option-${alertConvo.id}`)).queryByTestId('delete-option')
|
||||
within(getByTestId(`convo-option-${alertConvo.title}`)).queryByTestId('delete-option')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Custom options can be deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
<TestProviders>
|
||||
<ConversationSelector
|
||||
{...{ ...defaultProps, conversations: mockConversationsWithCustom }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
fireEvent.click(
|
||||
within(getByTestId(`convo-option-${customConvo.id}`)).getByTestId('delete-option')
|
||||
within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option')
|
||||
);
|
||||
jest.runAllTimers();
|
||||
expect(onConversationSelected).not.toHaveBeenCalled();
|
||||
|
||||
expect(deleteConversation).toHaveBeenCalledWith(customConvo.id);
|
||||
expect(onConversationDeleted).toHaveBeenCalledWith(customConvo.title);
|
||||
});
|
||||
|
||||
it('Previous conversation is set to active when selected conversation is deleted', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} selectedConversationId={customConvo.id} />
|
||||
<TestProviders>
|
||||
<ConversationSelector
|
||||
{...{ ...defaultProps, conversations: mockConversationsWithCustom }}
|
||||
selectedConversationTitle={customConvo.title}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
fireEvent.click(getByTestId('comboBoxSearchInput'));
|
||||
fireEvent.click(
|
||||
within(getByTestId(`convo-option-${customConvo.id}`)).getByTestId('delete-option')
|
||||
within(getByTestId(`convo-option-${customConvo.title}`)).getByTestId('delete-option')
|
||||
);
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(welcomeConvo.id);
|
||||
expect(onConversationSelected).toHaveBeenCalledWith({
|
||||
cId: '',
|
||||
cTitle: welcomeConvo.title,
|
||||
});
|
||||
});
|
||||
|
||||
it('Left arrow selects first conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
<TestProviders>
|
||||
<ConversationSelector
|
||||
{...{ ...defaultProps, conversations: mockConversationsWithCustom }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -222,21 +184,16 @@ describe('Conversation selector', () => {
|
|||
code: 'ArrowLeft',
|
||||
charCode: 27,
|
||||
});
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(alertConvo.id);
|
||||
expect(onConversationSelected).toHaveBeenCalledWith({
|
||||
cId: '',
|
||||
cTitle: alertConvo.title,
|
||||
});
|
||||
});
|
||||
|
||||
it('Right arrow selects last conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
<TestProviders>
|
||||
<ConversationSelector {...defaultProps} conversations={mockConversationsWithCustom} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -246,21 +203,16 @@ describe('Conversation selector', () => {
|
|||
code: 'ArrowRight',
|
||||
charCode: 26,
|
||||
});
|
||||
expect(onConversationSelected).toHaveBeenCalledWith(customConvo.id);
|
||||
expect(onConversationSelected).toHaveBeenCalledWith({
|
||||
cId: '',
|
||||
cTitle: customConvo.title,
|
||||
});
|
||||
});
|
||||
|
||||
it('Right arrow does nothing when ctrlKey is false', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
<TestProviders>
|
||||
<ConversationSelector {...defaultProps} conversations={mockConversationsWithCustom} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
@ -275,14 +227,13 @@ describe('Conversation selector', () => {
|
|||
|
||||
it('Right arrow does nothing when conversation lenth is 1', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<ConversationSelector {...defaultProps} />
|
||||
<TestProviders>
|
||||
<ConversationSelector
|
||||
{...defaultProps}
|
||||
conversations={{
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
}}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import useEvent from 'react-use/lib/useEvent';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { AIConnector } from '../../../connectorland/connector_selector';
|
||||
import { Conversation } from '../../../..';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
import * as i18n from './translations';
|
||||
|
@ -30,58 +30,68 @@ import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/sy
|
|||
const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
|
||||
|
||||
interface Props {
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
selectedConversationId: string | undefined;
|
||||
onConversationSelected: (conversationId: string) => void;
|
||||
defaultConnector?: AIConnector;
|
||||
selectedConversationTitle: string | undefined;
|
||||
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
|
||||
onConversationDeleted: (conversationId: string) => void;
|
||||
shouldDisableKeyboardShortcut?: () => boolean;
|
||||
isDisabled?: boolean;
|
||||
conversations: Record<string, Conversation>;
|
||||
}
|
||||
|
||||
const getPreviousConversationId = (conversationIds: string[], selectedConversationId: string) => {
|
||||
return conversationIds.indexOf(selectedConversationId) === 0
|
||||
? conversationIds[conversationIds.length - 1]
|
||||
: conversationIds[conversationIds.indexOf(selectedConversationId) - 1];
|
||||
const getPreviousConversationTitle = (
|
||||
conversationTitles: string[],
|
||||
selectedConversationTitle: string
|
||||
) => {
|
||||
return conversationTitles.indexOf(selectedConversationTitle) === 0
|
||||
? conversationTitles[conversationTitles.length - 1]
|
||||
: conversationTitles[conversationTitles.indexOf(selectedConversationTitle) - 1];
|
||||
};
|
||||
|
||||
const getNextConversationId = (conversationIds: string[], selectedConversationId: string) => {
|
||||
return conversationIds.indexOf(selectedConversationId) + 1 >= conversationIds.length
|
||||
? conversationIds[0]
|
||||
: conversationIds[conversationIds.indexOf(selectedConversationId) + 1];
|
||||
const getNextConversationTitle = (
|
||||
conversationTitles: string[],
|
||||
selectedConversationTitle: string
|
||||
) => {
|
||||
return conversationTitles.indexOf(selectedConversationTitle) + 1 >= conversationTitles.length
|
||||
? conversationTitles[0]
|
||||
: conversationTitles[conversationTitles.indexOf(selectedConversationTitle) + 1];
|
||||
};
|
||||
|
||||
const getConvoId = (cId: string, cTitle: string): string => (cId === cTitle ? '' : cId);
|
||||
|
||||
export type ConversationSelectorOption = EuiComboBoxOptionOption<{
|
||||
isDefault: boolean;
|
||||
}>;
|
||||
|
||||
export const ConversationSelector: React.FC<Props> = React.memo(
|
||||
({
|
||||
selectedConversationId = DEFAULT_CONVERSATION_TITLE,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
selectedConversationTitle = DEFAULT_CONVERSATION_TITLE,
|
||||
defaultConnector,
|
||||
onConversationSelected,
|
||||
onConversationDeleted,
|
||||
shouldDisableKeyboardShortcut = () => false,
|
||||
isDisabled = false,
|
||||
conversations,
|
||||
}) => {
|
||||
const { allSystemPrompts, conversations } = useAssistantContext();
|
||||
const { allSystemPrompts } = useAssistantContext();
|
||||
|
||||
const { deleteConversation, setConversation } = useConversation();
|
||||
|
||||
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
|
||||
const { createConversation } = useConversation();
|
||||
const conversationTitles = useMemo(() => Object.keys(conversations), [conversations]);
|
||||
const conversationOptions = useMemo<ConversationSelectorOption[]>(() => {
|
||||
return Object.values(conversations).map((conversation) => ({
|
||||
value: { isDefault: conversation.isDefault ?? false },
|
||||
label: conversation.id,
|
||||
id: conversation.id !== '' ? conversation.id : conversation.title,
|
||||
label: conversation.title,
|
||||
}));
|
||||
}, [conversations]);
|
||||
|
||||
const [selectedOptions, setSelectedOptions] = useState<ConversationSelectorOption[]>(() => {
|
||||
return conversationOptions.filter((c) => c.label === selectedConversationId) ?? [];
|
||||
return conversationOptions.filter((c) => c.label === selectedConversationTitle) ?? [];
|
||||
});
|
||||
|
||||
// Callback for when user types to create a new system prompt
|
||||
const onCreateOption = useCallback(
|
||||
(searchValue, flattenedOptions = []) => {
|
||||
async (searchValue, flattenedOptions = []) => {
|
||||
if (!searchValue || !searchValue.trim().toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
@ -96,66 +106,96 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
option.label.trim().toLowerCase() === normalizedSearchValue
|
||||
) !== -1;
|
||||
|
||||
let createdConversation;
|
||||
if (!optionExists) {
|
||||
const newConversation: Conversation = {
|
||||
id: searchValue,
|
||||
id: '',
|
||||
title: searchValue,
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: defaultConnectorId,
|
||||
provider: defaultProvider,
|
||||
defaultSystemPromptId: defaultSystemPrompt?.id,
|
||||
},
|
||||
replacements: [],
|
||||
...(defaultConnector
|
||||
? {
|
||||
apiConfig: {
|
||||
connectorId: defaultConnector.id,
|
||||
connectorTypeTitle: defaultConnector.connectorTypeTitle,
|
||||
provider: defaultConnector.apiProvider,
|
||||
defaultSystemPromptId: defaultSystemPrompt?.id,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
setConversation({ conversation: newConversation });
|
||||
createdConversation = await createConversation(newConversation);
|
||||
}
|
||||
onConversationSelected(searchValue);
|
||||
|
||||
onConversationSelected(
|
||||
createdConversation
|
||||
? { cId: '', cTitle: createdConversation.title }
|
||||
: { cId: '', cTitle: DEFAULT_CONVERSATION_TITLE }
|
||||
);
|
||||
},
|
||||
[
|
||||
allSystemPrompts,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
setConversation,
|
||||
onConversationSelected,
|
||||
]
|
||||
[allSystemPrompts, onConversationSelected, defaultConnector, createConversation]
|
||||
);
|
||||
|
||||
// Callback for when user deletes a conversation
|
||||
const onDelete = useCallback(
|
||||
(cId: string) => {
|
||||
if (selectedConversationId === cId) {
|
||||
onConversationSelected(getPreviousConversationId(conversationIds, cId));
|
||||
(deletedTitle: string) => {
|
||||
onConversationDeleted(deletedTitle);
|
||||
if (selectedConversationTitle === deletedTitle) {
|
||||
const prevConversationTitle = getPreviousConversationTitle(
|
||||
conversationTitles,
|
||||
selectedConversationTitle
|
||||
);
|
||||
|
||||
onConversationSelected({
|
||||
cId: getConvoId(conversations[prevConversationTitle].id, prevConversationTitle),
|
||||
cTitle: prevConversationTitle,
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
deleteConversation(cId);
|
||||
}, 0);
|
||||
},
|
||||
[conversationIds, deleteConversation, selectedConversationId, onConversationSelected]
|
||||
[
|
||||
selectedConversationTitle,
|
||||
onConversationDeleted,
|
||||
onConversationSelected,
|
||||
conversationTitles,
|
||||
conversations,
|
||||
]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(newOptions: ConversationSelectorOption[]) => {
|
||||
if (newOptions.length === 0) {
|
||||
async (newOptions: ConversationSelectorOption[]) => {
|
||||
if (newOptions.length === 0 || !newOptions?.[0].id) {
|
||||
setSelectedOptions([]);
|
||||
} else if (conversationOptions.findIndex((o) => o.label === newOptions?.[0].label) !== -1) {
|
||||
onConversationSelected(newOptions?.[0].label);
|
||||
} else if (conversationOptions.findIndex((o) => o.id === newOptions?.[0].id) !== -1) {
|
||||
const { id, label } = newOptions?.[0];
|
||||
|
||||
await onConversationSelected({ cId: getConvoId(id, label), cTitle: label });
|
||||
}
|
||||
},
|
||||
[conversationOptions, onConversationSelected]
|
||||
);
|
||||
|
||||
const onLeftArrowClick = useCallback(() => {
|
||||
const prevId = getPreviousConversationId(conversationIds, selectedConversationId);
|
||||
onConversationSelected(prevId);
|
||||
}, [conversationIds, selectedConversationId, onConversationSelected]);
|
||||
const prevTitle = getPreviousConversationTitle(conversationTitles, selectedConversationTitle);
|
||||
|
||||
onConversationSelected({
|
||||
cId: getConvoId(conversations[prevTitle].id, prevTitle),
|
||||
cTitle: prevTitle,
|
||||
});
|
||||
}, [conversationTitles, selectedConversationTitle, onConversationSelected, conversations]);
|
||||
const onRightArrowClick = useCallback(() => {
|
||||
const nextId = getNextConversationId(conversationIds, selectedConversationId);
|
||||
onConversationSelected(nextId);
|
||||
}, [conversationIds, selectedConversationId, onConversationSelected]);
|
||||
const nextTitle = getNextConversationTitle(conversationTitles, selectedConversationTitle);
|
||||
|
||||
onConversationSelected({
|
||||
cId: getConvoId(conversations[nextTitle].id, nextTitle),
|
||||
cTitle: nextTitle,
|
||||
});
|
||||
}, [conversationTitles, selectedConversationTitle, onConversationSelected, conversations]);
|
||||
|
||||
// Register keyboard listener for quick conversation switching
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (isDisabled || conversationIds.length <= 1) {
|
||||
if (isDisabled || conversationTitles.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -177,7 +217,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
}
|
||||
},
|
||||
[
|
||||
conversationIds.length,
|
||||
conversationTitles.length,
|
||||
isDisabled,
|
||||
onLeftArrowClick,
|
||||
onRightArrowClick,
|
||||
|
@ -187,8 +227,8 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
useEvent('keydown', onKeyDown);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOptions(conversationOptions.filter((c) => c.label === selectedConversationId));
|
||||
}, [conversationOptions, selectedConversationId]);
|
||||
setSelectedOptions(conversationOptions.filter((c) => c.label === selectedConversationTitle));
|
||||
}, [conversationOptions, selectedConversationTitle]);
|
||||
|
||||
const renderOption: (
|
||||
option: ConversationSelectorOption,
|
||||
|
@ -196,6 +236,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
OPTION_CONTENT_CLASSNAME: string
|
||||
) => React.ReactNode = (option, searchValue, contentClassName) => {
|
||||
const { label, value } = option;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
|
@ -230,7 +271,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
color="danger"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDelete(label);
|
||||
onDelete(label ?? '');
|
||||
}}
|
||||
data-test-subj="delete-option"
|
||||
css={css`
|
||||
|
@ -264,7 +305,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
options={conversationOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onChange}
|
||||
onCreateOption={onCreateOption}
|
||||
onCreateOption={onCreateOption as unknown as () => void}
|
||||
renderOption={renderOption}
|
||||
compressed={true}
|
||||
isDisabled={isDisabled}
|
||||
|
@ -274,7 +315,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
iconType="arrowLeft"
|
||||
aria-label={i18n.PREVIOUS_CONVERSATION_TITLE}
|
||||
onClick={onLeftArrowClick}
|
||||
disabled={isDisabled || conversationIds.length <= 1}
|
||||
disabled={isDisabled || conversationTitles.length <= 1}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
|
@ -284,7 +325,7 @@ export const ConversationSelector: React.FC<Props> = React.memo(
|
|||
iconType="arrowRight"
|
||||
aria-label={i18n.NEXT_CONVERSATION_TITLE}
|
||||
onClick={onRightArrowClick}
|
||||
disabled={isDisabled || conversationIds.length <= 1}
|
||||
disabled={isDisabled || conversationTitles.length <= 1}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
|
|
|
@ -13,13 +13,13 @@ import { alertConvo, customConvo, welcomeConvo } from '../../../mock/conversatio
|
|||
const onConversationSelectionChange = jest.fn();
|
||||
const onConversationDeleted = jest.fn();
|
||||
const mockConversations = {
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
[alertConvo.title]: alertConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
[customConvo.title]: customConvo,
|
||||
};
|
||||
const testProps = {
|
||||
conversations: mockConversations,
|
||||
selectedConversationId: welcomeConvo.id,
|
||||
selectedConversationTitle: welcomeConvo.title,
|
||||
onConversationDeleted,
|
||||
onConversationSelectionChange,
|
||||
};
|
||||
|
@ -30,9 +30,9 @@ describe('ConversationSelectorSettings', () => {
|
|||
});
|
||||
it('Selects an existing conversation', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
expect(getByTestId('comboBoxSearchInput')).toHaveValue(welcomeConvo.id);
|
||||
expect(getByTestId('comboBoxSearchInput')).toHaveValue(welcomeConvo.title);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
fireEvent.click(getByTestId(alertConvo.id));
|
||||
fireEvent.click(getByTestId(alertConvo.title));
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith(alertConvo);
|
||||
});
|
||||
it('Only custom option can be deleted', () => {
|
||||
|
@ -40,11 +40,11 @@ describe('ConversationSelectorSettings', () => {
|
|||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
// there is only one delete conversation because there is only one custom convo
|
||||
fireEvent.click(getByTestId('delete-conversation'));
|
||||
expect(onConversationDeleted).toHaveBeenCalledWith(customConvo.id);
|
||||
expect(onConversationDeleted).toHaveBeenCalledWith(customConvo.title);
|
||||
});
|
||||
it('Selects existing conversation from the search input', () => {
|
||||
const { getByTestId } = render(<ConversationSelectorSettings {...testProps} />);
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: alertConvo.id } });
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), { target: { value: alertConvo.title } });
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
code: 'Enter',
|
||||
|
|
|
@ -19,27 +19,34 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
import { css } from '@emotion/react';
|
||||
|
||||
import { Conversation } from '../../../..';
|
||||
import { UseAssistantContext } from '../../../assistant_context';
|
||||
import * as i18n from './translations';
|
||||
import * as i18n from '../conversation_selector/translations';
|
||||
import { SystemPromptSelectorOption } from '../../prompt_editor/system_prompt/system_prompt_modal/system_prompt_selector/system_prompt_selector';
|
||||
|
||||
interface Props {
|
||||
conversations: UseAssistantContext['conversations'];
|
||||
onConversationDeleted: (conversationId: string) => void;
|
||||
conversations: Record<string, Conversation>;
|
||||
onConversationDeleted: (conversationTitle: string) => void;
|
||||
onConversationSelectionChange: (conversation?: Conversation | string) => void;
|
||||
selectedConversationId?: string;
|
||||
selectedConversationTitle: string;
|
||||
shouldDisableKeyboardShortcut?: () => boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const getPreviousConversationId = (conversationIds: string[], selectedConversationId = '') => {
|
||||
return conversationIds.indexOf(selectedConversationId) === 0
|
||||
? conversationIds[conversationIds.length - 1]
|
||||
: conversationIds[conversationIds.indexOf(selectedConversationId) - 1];
|
||||
const getPreviousConversationTitle = (
|
||||
conversationTitles: string[],
|
||||
selectedConversationTitle: string
|
||||
) => {
|
||||
return conversationTitles.indexOf(selectedConversationTitle) === 0
|
||||
? conversationTitles[conversationTitles.length - 1]
|
||||
: conversationTitles[conversationTitles.indexOf(selectedConversationTitle) - 1];
|
||||
};
|
||||
|
||||
const getNextConversationId = (conversationIds: string[], selectedConversationId = '') => {
|
||||
return conversationIds.indexOf(selectedConversationId) + 1 >= conversationIds.length
|
||||
? conversationIds[0]
|
||||
: conversationIds[conversationIds.indexOf(selectedConversationId) + 1];
|
||||
const getNextConversationTitle = (
|
||||
conversationTitles: string[],
|
||||
selectedConversationTitle: string
|
||||
) => {
|
||||
return conversationTitles.indexOf(selectedConversationTitle) + 1 >= conversationTitles.length
|
||||
? conversationTitles[0]
|
||||
: conversationTitles[conversationTitles.indexOf(selectedConversationTitle) + 1];
|
||||
};
|
||||
|
||||
export type ConversationSelectorSettingsOption = EuiComboBoxOptionOption<{
|
||||
|
@ -57,25 +64,28 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
conversations,
|
||||
onConversationDeleted,
|
||||
onConversationSelectionChange,
|
||||
selectedConversationId,
|
||||
selectedConversationTitle,
|
||||
isDisabled,
|
||||
shouldDisableKeyboardShortcut = () => false,
|
||||
}) => {
|
||||
const conversationIds = useMemo(() => Object.keys(conversations), [conversations]);
|
||||
const conversationTitles = useMemo(() => Object.keys(conversations), [conversations]);
|
||||
|
||||
const [conversationOptions, setConversationOptions] = useState<
|
||||
ConversationSelectorSettingsOption[]
|
||||
>(() => {
|
||||
return Object.values(conversations).map((conversation) => ({
|
||||
value: { isDefault: conversation.isDefault ?? false },
|
||||
label: conversation.id,
|
||||
'data-test-subj': conversation.id,
|
||||
label: conversation.title,
|
||||
id: conversation.id,
|
||||
'data-test-subj': conversation.title,
|
||||
}));
|
||||
});
|
||||
|
||||
const selectedOptions = useMemo<ConversationSelectorSettingsOption[]>(() => {
|
||||
return selectedConversationId
|
||||
? conversationOptions.filter((c) => c.label === selectedConversationId) ?? []
|
||||
return selectedConversationTitle
|
||||
? conversationOptions.filter((c) => c.label === selectedConversationTitle) ?? []
|
||||
: [];
|
||||
}, [conversationOptions, selectedConversationId]);
|
||||
}, [conversationOptions, selectedConversationTitle]);
|
||||
|
||||
const handleSelectionChange = useCallback(
|
||||
(conversationSelectorSettingsOption: ConversationSelectorSettingsOption[]) => {
|
||||
|
@ -83,8 +93,10 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
conversationSelectorSettingsOption.length === 0
|
||||
? undefined
|
||||
: Object.values(conversations).find(
|
||||
(conversation) => conversation.id === conversationSelectorSettingsOption[0]?.label
|
||||
(conversation) =>
|
||||
conversation.title === conversationSelectorSettingsOption[0]?.label
|
||||
) ?? conversationSelectorSettingsOption[0]?.label;
|
||||
|
||||
onConversationSelectionChange(newConversation);
|
||||
},
|
||||
[onConversationSelectionChange, conversations]
|
||||
|
@ -107,6 +119,7 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
const newOption = {
|
||||
value: searchValue,
|
||||
label: searchValue,
|
||||
id: '',
|
||||
};
|
||||
|
||||
if (!optionExists) {
|
||||
|
@ -142,21 +155,21 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
);
|
||||
|
||||
const onLeftArrowClick = useCallback(() => {
|
||||
const prevId = getPreviousConversationId(conversationIds, selectedConversationId);
|
||||
const previousOption = conversationOptions.filter((c) => c.label === prevId);
|
||||
const prevTitle = getPreviousConversationTitle(conversationTitles, selectedConversationTitle);
|
||||
const previousOption = conversationOptions.filter((c) => c.label === prevTitle);
|
||||
handleSelectionChange(previousOption);
|
||||
}, [conversationIds, conversationOptions, handleSelectionChange, selectedConversationId]);
|
||||
}, [conversationTitles, selectedConversationTitle, conversationOptions, handleSelectionChange]);
|
||||
const onRightArrowClick = useCallback(() => {
|
||||
const nextId = getNextConversationId(conversationIds, selectedConversationId);
|
||||
const nextOption = conversationOptions.filter((c) => c.label === nextId);
|
||||
const nextTitle = getNextConversationTitle(conversationTitles, selectedConversationTitle);
|
||||
const nextOption = conversationOptions.filter((c) => c.label === nextTitle);
|
||||
handleSelectionChange(nextOption);
|
||||
}, [conversationIds, conversationOptions, handleSelectionChange, selectedConversationId]);
|
||||
}, [conversationTitles, selectedConversationTitle, conversationOptions, handleSelectionChange]);
|
||||
|
||||
const renderOption: (
|
||||
option: ConversationSelectorSettingsOption,
|
||||
searchValue: string,
|
||||
OPTION_CONTENT_CLASSNAME: string
|
||||
) => React.ReactNode = (option, searchValue, contentClassName) => {
|
||||
) => React.ReactNode = (option, searchValue) => {
|
||||
const { label, value } = option;
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
|
@ -217,6 +230,7 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
`}
|
||||
>
|
||||
<EuiComboBox
|
||||
data-test-subj="conversation-selector"
|
||||
aria-label={i18n.CONVERSATION_SELECTOR_ARIA_LABEL}
|
||||
customOptionText={`${i18n.CONVERSATION_SELECTOR_CUSTOM_OPTION_TEXT} {searchValue}`}
|
||||
placeholder={i18n.CONVERSATION_SELECTOR_PLACE_HOLDER}
|
||||
|
@ -227,13 +241,14 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
onCreateOption={onCreateOption}
|
||||
renderOption={renderOption}
|
||||
compressed={true}
|
||||
isDisabled={isDisabled}
|
||||
prepend={
|
||||
<EuiButtonIcon
|
||||
iconType="arrowLeft"
|
||||
data-test-subj="arrowLeft"
|
||||
aria-label={i18n.PREVIOUS_CONVERSATION_TITLE}
|
||||
onClick={onLeftArrowClick}
|
||||
disabled={conversationIds.length <= 1}
|
||||
disabled={isDisabled || conversationTitles.length <= 1}
|
||||
/>
|
||||
}
|
||||
append={
|
||||
|
@ -242,7 +257,7 @@ export const ConversationSelectorSettings: React.FC<Props> = React.memo(
|
|||
data-test-subj="arrowRight"
|
||||
aria-label={i18n.NEXT_CONVERSATION_TITLE}
|
||||
onClick={onRightArrowClick}
|
||||
disabled={conversationIds.length <= 1}
|
||||
disabled={isDisabled || conversationTitles.length <= 1}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SELECTED_CONVERSATION_LABEL = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.defaultConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Conversations',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONVERSATION_SELECTOR_ARIA_LABEL = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Conversation selector',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONVERSATION_SELECTOR_PLACE_HOLDER = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.placeholderTitle',
|
||||
{
|
||||
defaultMessage: 'Select or type to create new...',
|
||||
}
|
||||
);
|
||||
|
||||
export const CONVERSATION_SELECTOR_CUSTOM_OPTION_TEXT = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.CustomOptionTextTitle',
|
||||
{
|
||||
defaultMessage: 'Create new conversation:',
|
||||
}
|
||||
);
|
||||
|
||||
export const PREVIOUS_CONVERSATION_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.previousConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Previous conversation',
|
||||
}
|
||||
);
|
||||
|
||||
export const NEXT_CONVERSATION_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.nextConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Next conversation',
|
||||
}
|
||||
);
|
||||
|
||||
export const DELETE_CONVERSATION = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.conversationSelectorSettings.deleteConversationTitle',
|
||||
{
|
||||
defaultMessage: 'Delete conversation',
|
||||
}
|
||||
);
|
|
@ -15,15 +15,14 @@ import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/c
|
|||
import { mockConnectors } from '../../../mock/connectors';
|
||||
|
||||
const mockConvos = {
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[alertConvo.id]: alertConvo,
|
||||
[customConvo.id]: customConvo,
|
||||
[welcomeConvo.title]: { ...welcomeConvo, id: '1234' },
|
||||
[alertConvo.title]: { ...alertConvo, id: '12345' },
|
||||
[customConvo.title]: { ...customConvo, id: '123' },
|
||||
};
|
||||
const onSelectedConversationChange = jest.fn();
|
||||
|
||||
const setUpdatedConversationSettings = jest.fn().mockImplementation((fn) => {
|
||||
return fn(mockConvos);
|
||||
});
|
||||
const setConversationSettings = jest.fn();
|
||||
const setConversationsSettingsBulkActions = jest.fn();
|
||||
|
||||
const testProps = {
|
||||
allSystemPrompts: mockSystemPrompts,
|
||||
|
@ -32,8 +31,10 @@ const testProps = {
|
|||
defaultProvider: OpenAiProviderType.OpenAi,
|
||||
http: { basePath: { get: jest.fn() } },
|
||||
onSelectedConversationChange,
|
||||
selectedConversation: welcomeConvo,
|
||||
setUpdatedConversationSettings,
|
||||
selectedConversation: mockConvos[welcomeConvo.title],
|
||||
setConversationSettings,
|
||||
conversationsSettingsBulkActions: {},
|
||||
setConversationsSettingsBulkActions,
|
||||
} as unknown as ConversationSettingsProps;
|
||||
|
||||
jest.mock('../../../connectorland/use_load_connectors', () => ({
|
||||
|
@ -44,7 +45,7 @@ jest.mock('../../../connectorland/use_load_connectors', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const mockConvo = alertConvo;
|
||||
const mockConvo = mockConvos[alertConvo.title];
|
||||
jest.mock('../conversation_selector_settings', () => ({
|
||||
// @ts-ignore
|
||||
ConversationSelectorSettings: ({ onConversationDeleted, onConversationSelectionChange }) => (
|
||||
|
@ -59,6 +60,16 @@ jest.mock('../conversation_selector_settings', () => ({
|
|||
data-test-subj="change-convo"
|
||||
onClick={() => onConversationSelectionChange(mockConvo)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-new-convo"
|
||||
onClick={() => onConversationSelectionChange({ ...mockConvo, id: '' })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="bad-id-convo"
|
||||
onClick={() => onConversationSelectionChange({ ...mockConvo, id: 'not-the-right-id' })}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-test-subj="change-convo-custom"
|
||||
|
@ -115,10 +126,10 @@ describe('ConversationSettings', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-sp'));
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith({
|
||||
...mockConvos,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
[welcomeConvo.title]: {
|
||||
...mockConvos[welcomeConvo.title],
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
defaultSystemPromptId: 'mock-superhero-system-prompt-1',
|
||||
|
@ -127,6 +138,29 @@ describe('ConversationSettings', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('Selecting a system prompt updates the defaultSystemPromptId for the selected conversation when the id does not match the title', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings
|
||||
{...testProps}
|
||||
selectedConversation={{ ...mockConvo, id: 'not-the-right-id' }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-sp'));
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith({
|
||||
...mockConvos,
|
||||
[mockConvo.title]: {
|
||||
...mockConvo,
|
||||
id: 'not-the-right-id',
|
||||
apiConfig: {
|
||||
...mockConvo.apiConfig,
|
||||
defaultSystemPromptId: 'mock-superhero-system-prompt-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Selecting an existing conversation updates the selected convo and does not update convo settings', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
|
@ -135,8 +169,24 @@ describe('ConversationSettings', () => {
|
|||
);
|
||||
fireEvent.click(getByTestId('change-convo'));
|
||||
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith(mockConvos);
|
||||
expect(onSelectedConversationChange).toHaveBeenCalledWith(alertConvo);
|
||||
expect(setConversationSettings).toHaveBeenCalled();
|
||||
expect(onSelectedConversationChange).toHaveBeenCalledWith(mockConvo);
|
||||
});
|
||||
|
||||
it('Selecting a new conversation updates the selected convo and does not update convo settings', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-new-convo'));
|
||||
|
||||
expect(onSelectedConversationChange).toHaveBeenCalledWith({ ...mockConvo, id: '' });
|
||||
expect(setConversationsSettingsBulkActions).toHaveBeenCalledWith({
|
||||
create: {
|
||||
[mockConvo.title]: { ...mockConvo, id: '' },
|
||||
},
|
||||
});
|
||||
});
|
||||
it('Selecting an existing conversation updates the selected convo and is added to the convo settings', () => {
|
||||
const { getByTestId } = render(
|
||||
|
@ -146,17 +196,15 @@ describe('ConversationSettings', () => {
|
|||
);
|
||||
fireEvent.click(getByTestId('change-convo-custom'));
|
||||
const newConvo = {
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
provider: 'OpenAI',
|
||||
},
|
||||
id: 'Cool new conversation',
|
||||
category: 'assistant',
|
||||
id: '',
|
||||
title: 'Cool new conversation',
|
||||
messages: [],
|
||||
replacements: [],
|
||||
};
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith({
|
||||
...mockConvos,
|
||||
[newConvo.id]: newConvo,
|
||||
[newConvo.title]: newConvo,
|
||||
});
|
||||
expect(onSelectedConversationChange).toHaveBeenCalledWith(newConvo);
|
||||
});
|
||||
|
@ -167,8 +215,8 @@ describe('ConversationSettings', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('delete-convo'));
|
||||
const { [customConvo.id]: _, ...rest } = mockConvos;
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith(rest);
|
||||
const { [customConvo.title]: _, ...rest } = mockConvos;
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith(rest);
|
||||
});
|
||||
it('Selecting a new connector updates the conversation', () => {
|
||||
const { getByTestId } = render(
|
||||
|
@ -177,12 +225,28 @@ describe('ConversationSettings', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-connector'));
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith({
|
||||
...mockConvos,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
[welcomeConvo.title]: {
|
||||
...mockConvos[welcomeConvo.title],
|
||||
apiConfig: {
|
||||
connectorId: mockConnector.id,
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
model: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(setConversationsSettingsBulkActions).toHaveBeenLastCalledWith({
|
||||
update: {
|
||||
[welcomeConvo.title]: {
|
||||
...mockConvos[welcomeConvo.title],
|
||||
apiConfig: {
|
||||
connectorId: mockConnector.id,
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
model: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -194,15 +258,84 @@ describe('ConversationSettings', () => {
|
|||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-model'));
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith({
|
||||
...mockConvos,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
[welcomeConvo.title]: {
|
||||
...mockConvos[welcomeConvo.title],
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
model: 'MODEL_GPT_4',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(setConversationsSettingsBulkActions).toHaveBeenLastCalledWith({
|
||||
update: {
|
||||
[welcomeConvo.title]: {
|
||||
...mockConvos[welcomeConvo.title],
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
model: 'MODEL_GPT_4',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it.skip('Selecting a new connector model updates the conversation when the id does not match the title', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} selectedConversation={{ ...mockConvo, id: '' }} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-model'));
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith({
|
||||
...mockConvos,
|
||||
'not-the-right-id': {
|
||||
...mockConvo,
|
||||
id: '',
|
||||
apiConfig: {
|
||||
...mockConvo.apiConfig,
|
||||
model: 'MODEL_GPT_4',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
it('Selecting a connector with a new id updates the conversation settings', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings {...testProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('bad-id-convo'));
|
||||
expect(setConversationSettings).toHaveBeenCalled();
|
||||
expect(onSelectedConversationChange).toHaveBeenCalledWith({
|
||||
...mockConvo,
|
||||
id: 'not-the-right-id',
|
||||
});
|
||||
expect(setConversationsSettingsBulkActions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Selecting a new connector updates the conversation when the id does not match the title', () => {
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
<ConversationSettings
|
||||
{...testProps}
|
||||
selectedConversation={{ ...mockConvo, id: 'not-the-right-id' }}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
fireEvent.click(getByTestId('change-connector'));
|
||||
expect(setConversationSettings).toHaveBeenLastCalledWith({
|
||||
...mockConvos,
|
||||
[mockConvo.title]: {
|
||||
...mockConvo,
|
||||
id: 'not-the-right-id',
|
||||
apiConfig: {
|
||||
connectorId: mockConnector.id,
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
model: undefined,
|
||||
provider: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,29 +12,32 @@ import { HttpSetup } from '@kbn/core-http-browser';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/public/common';
|
||||
import { noop } from 'lodash/fp';
|
||||
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { Conversation, Prompt } from '../../../..';
|
||||
import * as i18n from './translations';
|
||||
import * as i18nModel from '../../../connectorland/models/model_selector/translations';
|
||||
|
||||
import { ConnectorSelector } from '../../../connectorland/connector_selector';
|
||||
import { AIConnector, ConnectorSelector } from '../../../connectorland/connector_selector';
|
||||
import { SelectSystemPrompt } from '../../prompt_editor/system_prompt/select_system_prompt';
|
||||
import { ModelSelector } from '../../../connectorland/models/model_selector/model_selector';
|
||||
import { UseAssistantContext } from '../../../assistant_context';
|
||||
import { ConversationSelectorSettings } from '../conversation_selector_settings';
|
||||
import { getDefaultSystemPrompt } from '../../use_conversation/helpers';
|
||||
import { useLoadConnectors } from '../../../connectorland/use_load_connectors';
|
||||
import { getGenAiConfig } from '../../../connectorland/helpers';
|
||||
import { ConversationsBulkActions } from '../../api';
|
||||
|
||||
export interface ConversationSettingsProps {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
allSystemPrompts: Prompt[];
|
||||
conversationSettings: UseAssistantContext['conversations'];
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
conversationSettings: Record<string, Conversation>;
|
||||
conversationsSettingsBulkActions: ConversationsBulkActions;
|
||||
defaultConnector?: AIConnector;
|
||||
http: HttpSetup;
|
||||
onSelectedConversationChange: (conversation?: Conversation) => void;
|
||||
selectedConversation: Conversation | undefined;
|
||||
setUpdatedConversationSettings: React.Dispatch<
|
||||
React.SetStateAction<UseAssistantContext['conversations']>
|
||||
selectedConversation?: Conversation;
|
||||
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setConversationsSettingsBulkActions: React.Dispatch<
|
||||
React.SetStateAction<ConversationsBulkActions>
|
||||
>;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
@ -44,15 +47,17 @@ export interface ConversationSettingsProps {
|
|||
*/
|
||||
export const ConversationSettings: React.FC<ConversationSettingsProps> = React.memo(
|
||||
({
|
||||
actionTypeRegistry,
|
||||
allSystemPrompts,
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
defaultConnector,
|
||||
selectedConversation,
|
||||
onSelectedConversationChange,
|
||||
conversationSettings,
|
||||
http,
|
||||
setUpdatedConversationSettings,
|
||||
isDisabled = false,
|
||||
setConversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
setConversationsSettingsBulkActions,
|
||||
}) => {
|
||||
const defaultSystemPrompt = useMemo(() => {
|
||||
return getDefaultSystemPrompt({ allSystemPrompts, conversation: undefined });
|
||||
|
@ -62,30 +67,54 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
return getDefaultSystemPrompt({ allSystemPrompts, conversation: selectedConversation });
|
||||
}, [allSystemPrompts, selectedConversation]);
|
||||
|
||||
const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({ http });
|
||||
const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({
|
||||
actionTypeRegistry,
|
||||
http,
|
||||
});
|
||||
|
||||
// Conversation callbacks
|
||||
// When top level conversation selection changes
|
||||
const onConversationSelectionChange = useCallback(
|
||||
(c?: Conversation | string) => {
|
||||
const isNew = typeof c === 'string';
|
||||
|
||||
const newSelectedConversation: Conversation | undefined = isNew
|
||||
? {
|
||||
id: c ?? '',
|
||||
id: '',
|
||||
title: c ?? '',
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: {
|
||||
connectorId: defaultConnectorId,
|
||||
provider: defaultProvider,
|
||||
defaultSystemPromptId: defaultSystemPrompt?.id,
|
||||
},
|
||||
replacements: [],
|
||||
...(defaultConnector
|
||||
? {
|
||||
apiConfig: {
|
||||
connectorId: defaultConnector.id,
|
||||
connectorTypeTitle: defaultConnector.connectorTypeTitle,
|
||||
provider: defaultConnector.apiProvider,
|
||||
defaultSystemPromptId: defaultSystemPrompt?.id,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: c;
|
||||
|
||||
if (newSelectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => {
|
||||
if (newSelectedConversation && (isNew || newSelectedConversation.id === '')) {
|
||||
setConversationSettings({
|
||||
...conversationSettings,
|
||||
[isNew ? c : newSelectedConversation.title]: newSelectedConversation,
|
||||
});
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
create: {
|
||||
...(conversationsSettingsBulkActions.create ?? {}),
|
||||
[newSelectedConversation.title]: newSelectedConversation,
|
||||
},
|
||||
});
|
||||
} else if (newSelectedConversation != null) {
|
||||
setConversationSettings((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[newSelectedConversation.id]: newSelectedConversation,
|
||||
[newSelectedConversation.title]: newSelectedConversation,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -93,102 +122,227 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
onSelectedConversationChange(newSelectedConversation);
|
||||
},
|
||||
[
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
defaultConnector,
|
||||
defaultSystemPrompt?.id,
|
||||
onSelectedConversationChange,
|
||||
setUpdatedConversationSettings,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
]
|
||||
);
|
||||
|
||||
const onConversationDeleted = useCallback(
|
||||
(conversationId: string) => {
|
||||
setUpdatedConversationSettings((prev) => {
|
||||
const { [conversationId]: prevConversation, ...updatedConversations } = prev;
|
||||
if (prevConversation != null) {
|
||||
return updatedConversations;
|
||||
}
|
||||
return prev;
|
||||
(conversationTitle: string) => {
|
||||
const conversationId = conversationSettings[conversationTitle].id;
|
||||
const updatedConversationSettings = { ...conversationSettings };
|
||||
delete updatedConversationSettings[conversationTitle];
|
||||
setConversationSettings(updatedConversationSettings);
|
||||
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
delete: {
|
||||
ids: [...(conversationsSettingsBulkActions.delete?.ids ?? []), conversationId],
|
||||
},
|
||||
});
|
||||
},
|
||||
[setUpdatedConversationSettings]
|
||||
[
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
]
|
||||
);
|
||||
|
||||
const handleOnSystemPromptSelectionChange = useCallback(
|
||||
(systemPromptId?: string | undefined) => {
|
||||
if (selectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => ({
|
||||
...prev,
|
||||
[selectedConversation.id]: {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
defaultSystemPromptId: systemPromptId,
|
||||
},
|
||||
if (selectedConversation != null && selectedConversation.apiConfig) {
|
||||
const updatedConversation = {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
defaultSystemPromptId: systemPromptId,
|
||||
},
|
||||
}));
|
||||
};
|
||||
setConversationSettings({
|
||||
...conversationSettings,
|
||||
[updatedConversation.title]: updatedConversation,
|
||||
});
|
||||
if (selectedConversation.id !== '') {
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
update: {
|
||||
...(conversationsSettingsBulkActions.update ?? {}),
|
||||
[updatedConversation.title]: {
|
||||
...updatedConversation,
|
||||
...(conversationsSettingsBulkActions.update
|
||||
? conversationsSettingsBulkActions.update[updatedConversation.title] ?? {}
|
||||
: {}),
|
||||
apiConfig: {
|
||||
...updatedConversation.apiConfig,
|
||||
...((conversationsSettingsBulkActions.update
|
||||
? conversationsSettingsBulkActions.update[updatedConversation.title] ?? {}
|
||||
: {}
|
||||
).apiConfig ?? {}),
|
||||
defaultSystemPromptId: systemPromptId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
create: {
|
||||
...(conversationsSettingsBulkActions.create ?? {}),
|
||||
[updatedConversation.title]: updatedConversation,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedConversation, setUpdatedConversationSettings]
|
||||
[
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
selectedConversation,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
]
|
||||
);
|
||||
|
||||
const selectedConnector = useMemo(() => {
|
||||
const selectedConnectorId = selectedConversation?.apiConfig.connectorId;
|
||||
const selectedConnectorId = selectedConversation?.apiConfig?.connectorId;
|
||||
if (areConnectorsFetched) {
|
||||
return connectors?.find((c) => c.id === selectedConnectorId);
|
||||
}
|
||||
return undefined;
|
||||
}, [areConnectorsFetched, connectors, selectedConversation?.apiConfig.connectorId]);
|
||||
}, [areConnectorsFetched, connectors, selectedConversation?.apiConfig?.connectorId]);
|
||||
|
||||
const selectedProvider = useMemo(
|
||||
() => selectedConversation?.apiConfig.provider,
|
||||
[selectedConversation?.apiConfig.provider]
|
||||
() => selectedConversation?.apiConfig?.provider,
|
||||
[selectedConversation?.apiConfig?.provider]
|
||||
);
|
||||
|
||||
const handleOnConnectorSelectionChange = useCallback(
|
||||
(connector) => {
|
||||
if (selectedConversation != null) {
|
||||
const config = getGenAiConfig(connector);
|
||||
|
||||
setUpdatedConversationSettings((prev) => ({
|
||||
...prev,
|
||||
[selectedConversation.id]: {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
connectorId: connector?.id,
|
||||
provider: config?.apiProvider,
|
||||
model: config?.defaultModel,
|
||||
},
|
||||
const updatedConversation = {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
connectorId: connector.id,
|
||||
connectorTypeTitle: connector.connectorTypeTitle,
|
||||
provider: config?.apiProvider,
|
||||
model: config?.defaultModel,
|
||||
},
|
||||
}));
|
||||
};
|
||||
setConversationSettings({
|
||||
...conversationSettings,
|
||||
[selectedConversation.title]: updatedConversation,
|
||||
});
|
||||
if (selectedConversation.id !== '') {
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
update: {
|
||||
...(conversationsSettingsBulkActions.update ?? {}),
|
||||
[updatedConversation.title]: {
|
||||
...updatedConversation,
|
||||
...(conversationsSettingsBulkActions.update
|
||||
? conversationsSettingsBulkActions.update[updatedConversation.title] ?? {}
|
||||
: {}),
|
||||
apiConfig: {
|
||||
...updatedConversation.apiConfig,
|
||||
...((conversationsSettingsBulkActions.update
|
||||
? conversationsSettingsBulkActions.update[updatedConversation.title] ?? {}
|
||||
: {}
|
||||
).apiConfig ?? {}),
|
||||
connectorId: connector?.id,
|
||||
connectorTypeTitle: connector?.connectorTypeTitle,
|
||||
provider: config?.apiProvider,
|
||||
model: config?.defaultModel,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
create: {
|
||||
...(conversationsSettingsBulkActions.create ?? {}),
|
||||
[updatedConversation.title]: updatedConversation,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedConversation, setUpdatedConversationSettings]
|
||||
[
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
selectedConversation,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
]
|
||||
);
|
||||
|
||||
const selectedModel = useMemo(() => {
|
||||
const connectorModel = getGenAiConfig(selectedConnector)?.defaultModel;
|
||||
// Prefer conversation configuration over connector default
|
||||
return selectedConversation?.apiConfig.model ?? connectorModel;
|
||||
}, [selectedConnector, selectedConversation?.apiConfig.model]);
|
||||
return selectedConversation?.apiConfig?.model ?? connectorModel;
|
||||
}, [selectedConnector, selectedConversation?.apiConfig?.model]);
|
||||
|
||||
const handleOnModelSelectionChange = useCallback(
|
||||
(model?: string) => {
|
||||
if (selectedConversation != null) {
|
||||
setUpdatedConversationSettings((prev) => ({
|
||||
...prev,
|
||||
[selectedConversation.id]: {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
model,
|
||||
},
|
||||
if (selectedConversation != null && selectedConversation.apiConfig) {
|
||||
const updatedConversation = {
|
||||
...selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
model,
|
||||
},
|
||||
}));
|
||||
};
|
||||
setConversationSettings({
|
||||
...conversationSettings,
|
||||
[updatedConversation.title]: updatedConversation,
|
||||
});
|
||||
if (selectedConversation.id !== '') {
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
update: {
|
||||
...(conversationsSettingsBulkActions.update ?? {}),
|
||||
[updatedConversation.title]: {
|
||||
...updatedConversation,
|
||||
...(conversationsSettingsBulkActions.update
|
||||
? conversationsSettingsBulkActions.update[updatedConversation.title] ?? {}
|
||||
: {}),
|
||||
apiConfig: {
|
||||
...updatedConversation.apiConfig,
|
||||
...((conversationsSettingsBulkActions.update
|
||||
? conversationsSettingsBulkActions.update[updatedConversation.title] ?? {}
|
||||
: {}
|
||||
).apiConfig ?? {}),
|
||||
model,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setConversationsSettingsBulkActions({
|
||||
...conversationsSettingsBulkActions,
|
||||
create: {
|
||||
...(conversationsSettingsBulkActions.create ?? {}),
|
||||
[updatedConversation.title]: updatedConversation,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedConversation, setUpdatedConversationSettings]
|
||||
[
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
selectedConversation,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -201,7 +355,7 @@ export const ConversationSettings: React.FC<ConversationSettingsProps> = React.m
|
|||
<EuiHorizontalRule margin={'s'} />
|
||||
|
||||
<ConversationSelectorSettings
|
||||
selectedConversationId={selectedConversation?.id}
|
||||
selectedConversationTitle={selectedConversation?.title ?? ''}
|
||||
conversations={conversationSettings}
|
||||
onConversationDeleted={onConversationDeleted}
|
||||
onConversationSelectionChange={onConversationSelectionChange}
|
||||
|
|
|
@ -8,22 +8,24 @@
|
|||
import {
|
||||
getBlockBotConversation,
|
||||
getDefaultConnector,
|
||||
getFormattedMessageContent,
|
||||
getOptionalRequestParams,
|
||||
hasParsableResponse,
|
||||
mergeBaseWithPersistedConversations,
|
||||
} from './helpers';
|
||||
import { enterpriseMessaging } from './use_conversation/sample_conversations';
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { AIConnector } from '../connectorland/connector_selector';
|
||||
|
||||
describe('getBlockBotConversation', () => {
|
||||
describe('helpers', () => {
|
||||
describe('isAssistantEnabled = false', () => {
|
||||
const isAssistantEnabled = false;
|
||||
it('When no conversation history, return only enterprise messaging', () => {
|
||||
const conversation = {
|
||||
id: 'conversation_id',
|
||||
category: 'assistant',
|
||||
theme: {},
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
title: 'conversation_id',
|
||||
};
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages).toEqual(enterpriseMessaging);
|
||||
|
@ -33,7 +35,6 @@ describe('getBlockBotConversation', () => {
|
|||
it('When conversation history and the last message is not enterprise messaging, appends enterprise messaging to conversation', () => {
|
||||
const conversation = {
|
||||
id: 'conversation_id',
|
||||
theme: {},
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
|
@ -45,7 +46,10 @@ describe('getBlockBotConversation', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
title: 'conversation_id',
|
||||
};
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(2);
|
||||
|
@ -54,9 +58,11 @@ describe('getBlockBotConversation', () => {
|
|||
it('returns the conversation without changes when the last message is enterprise messaging', () => {
|
||||
const conversation = {
|
||||
id: 'conversation_id',
|
||||
theme: {},
|
||||
title: 'conversation_id',
|
||||
messages: enterpriseMessaging,
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
};
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(1);
|
||||
|
@ -66,7 +72,8 @@ describe('getBlockBotConversation', () => {
|
|||
it('returns the conversation with new enterprise message when conversation has enterprise messaging, but not as the last message', () => {
|
||||
const conversation = {
|
||||
id: 'conversation_id',
|
||||
theme: {},
|
||||
title: 'conversation_id',
|
||||
category: 'assistant',
|
||||
messages: [
|
||||
...enterpriseMessaging,
|
||||
{
|
||||
|
@ -79,7 +86,8 @@ describe('getBlockBotConversation', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
};
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(3);
|
||||
|
@ -91,9 +99,11 @@ describe('getBlockBotConversation', () => {
|
|||
it('when no conversation history, returns the welcome conversation', () => {
|
||||
const conversation = {
|
||||
id: 'conversation_id',
|
||||
theme: {},
|
||||
title: 'conversation_id',
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
};
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(3);
|
||||
|
@ -101,7 +111,8 @@ describe('getBlockBotConversation', () => {
|
|||
it('returns a conversation history with the welcome conversation appended', () => {
|
||||
const conversation = {
|
||||
id: 'conversation_id',
|
||||
theme: {},
|
||||
title: 'conversation_id',
|
||||
category: 'assistant',
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
|
@ -113,7 +124,8 @@ describe('getBlockBotConversation', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
};
|
||||
const result = getBlockBotConversation(conversation, isAssistantEnabled);
|
||||
expect(result.messages.length).toEqual(4);
|
||||
|
@ -129,17 +141,17 @@ describe('getBlockBotConversation', () => {
|
|||
});
|
||||
|
||||
it('should return undefined if connectors array is empty', () => {
|
||||
const connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> =
|
||||
[];
|
||||
const connectors: AIConnector[] = [];
|
||||
const result = getDefaultConnector(connectors);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the connector id if there is only one connector', () => {
|
||||
const connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> = [
|
||||
const connectors: AIConnector[] = [
|
||||
{
|
||||
actionTypeId: '.gen-ai',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 0,
|
||||
|
@ -160,9 +172,10 @@ describe('getBlockBotConversation', () => {
|
|||
});
|
||||
|
||||
it('should return undefined if there are multiple connectors', () => {
|
||||
const connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> = [
|
||||
const connectors: AIConnector[] = [
|
||||
{
|
||||
actionTypeId: '.gen-ai',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 0,
|
||||
|
@ -178,6 +191,7 @@ describe('getBlockBotConversation', () => {
|
|||
},
|
||||
{
|
||||
actionTypeId: '.gen-ai',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
isPreconfigured: false,
|
||||
isDeprecated: false,
|
||||
referencedByCount: 0,
|
||||
|
@ -197,43 +211,6 @@ describe('getBlockBotConversation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getFormattedMessageContent', () => {
|
||||
it('returns the value of the action_input property when `content` has properly prefixed and suffixed JSON with the action_input property', () => {
|
||||
const content = '```json\n{"action_input": "value from action_input"}\n```';
|
||||
|
||||
expect(getFormattedMessageContent(content)).toBe('value from action_input');
|
||||
});
|
||||
|
||||
it('returns the original content when `content` has properly formatted JSON WITHOUT the action_input property', () => {
|
||||
const content = '```json\n{"some_key": "some value"}\n```';
|
||||
expect(getFormattedMessageContent(content)).toBe(content);
|
||||
});
|
||||
|
||||
it('returns the original content when `content` has improperly formatted JSON', () => {
|
||||
const content = '```json\n{"action_input": "value from action_input",}\n```'; // <-- the trailing comma makes it invalid
|
||||
|
||||
expect(getFormattedMessageContent(content)).toBe(content);
|
||||
});
|
||||
|
||||
it('returns the original content when `content` is missing the prefix', () => {
|
||||
const content = '{"action_input": "value from action_input"}\n```'; // <-- missing prefix
|
||||
|
||||
expect(getFormattedMessageContent(content)).toBe(content);
|
||||
});
|
||||
|
||||
it('returns the original content when `content` is missing the suffix', () => {
|
||||
const content = '```json\n{"action_input": "value from action_input"}'; // <-- missing suffix
|
||||
|
||||
expect(getFormattedMessageContent(content)).toBe(content);
|
||||
});
|
||||
|
||||
it('returns the original content when `content` does NOT contain a JSON string', () => {
|
||||
const content = 'plain text content';
|
||||
|
||||
expect(getFormattedMessageContent(content)).toBe(content);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptionalRequestParams', () => {
|
||||
it('should return an empty object when alerts is false', () => {
|
||||
const params = {
|
||||
|
@ -241,7 +218,6 @@ describe('getBlockBotConversation', () => {
|
|||
alertsIndexPattern: 'indexPattern',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 'value' },
|
||||
size: 10,
|
||||
};
|
||||
|
||||
|
@ -256,7 +232,6 @@ describe('getBlockBotConversation', () => {
|
|||
alertsIndexPattern: 'indexPattern',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 'value' },
|
||||
size: 10,
|
||||
};
|
||||
|
||||
|
@ -266,7 +241,6 @@ describe('getBlockBotConversation', () => {
|
|||
alertsIndexPattern: 'indexPattern',
|
||||
allow: ['a', 'b', 'c'],
|
||||
allowReplacement: ['b', 'c'],
|
||||
replacements: { key: 'value' },
|
||||
size: 10,
|
||||
});
|
||||
});
|
||||
|
@ -285,41 +259,161 @@ describe('getBlockBotConversation', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('hasParsableResponse', () => {
|
||||
it('returns true when just isEnabledKnowledgeBase is true', () => {
|
||||
const result = hasParsableResponse({
|
||||
isEnabledRAGAlerts: false,
|
||||
isEnabledKnowledgeBase: true,
|
||||
});
|
||||
describe('mergeBaseWithPersistedConversations', () => {
|
||||
const messages = [
|
||||
{ content: 'Message 1', role: 'user' as const, timestamp: '2024-02-14T22:29:43.862Z' },
|
||||
{ content: 'Message 2', role: 'user' as const, timestamp: '2024-02-14T22:29:43.862Z' },
|
||||
];
|
||||
const defaultProps = {
|
||||
messages,
|
||||
category: 'assistant',
|
||||
theme: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
};
|
||||
const baseConversations = {
|
||||
conversation1: {
|
||||
...defaultProps,
|
||||
title: 'Conversation 1',
|
||||
id: 'conversation_1',
|
||||
},
|
||||
conversation2: {
|
||||
...defaultProps,
|
||||
title: 'Conversation 2',
|
||||
id: 'conversation_2',
|
||||
},
|
||||
};
|
||||
const conversationsData = {
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 2,
|
||||
data: Object.values(baseConversations).map((c) => c),
|
||||
};
|
||||
|
||||
expect(result).toBe(true);
|
||||
it('should merge base conversations with user conversations when both are non-empty', () => {
|
||||
const moreData = {
|
||||
...conversationsData,
|
||||
data: [
|
||||
{
|
||||
...defaultProps,
|
||||
title: 'Conversation 3',
|
||||
id: 'conversation_3',
|
||||
},
|
||||
{
|
||||
...defaultProps,
|
||||
title: 'Conversation 4',
|
||||
id: 'conversation_4',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = mergeBaseWithPersistedConversations(baseConversations, moreData);
|
||||
|
||||
expect(result).toEqual({
|
||||
conversation1: {
|
||||
title: 'Conversation 1',
|
||||
id: 'conversation_1',
|
||||
...defaultProps,
|
||||
},
|
||||
conversation2: {
|
||||
title: 'Conversation 2',
|
||||
id: 'conversation_2',
|
||||
...defaultProps,
|
||||
},
|
||||
'Conversation 3': {
|
||||
title: 'Conversation 3',
|
||||
id: 'conversation_3',
|
||||
...defaultProps,
|
||||
},
|
||||
'Conversation 4': {
|
||||
title: 'Conversation 4',
|
||||
id: 'conversation_4',
|
||||
...defaultProps,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns true when just isEnabledRAGAlerts is true', () => {
|
||||
const result = hasParsableResponse({
|
||||
isEnabledRAGAlerts: true,
|
||||
isEnabledKnowledgeBase: false,
|
||||
it('should return base conversations when user conversations are empty', () => {
|
||||
const result = mergeBaseWithPersistedConversations(baseConversations, {
|
||||
...conversationsData,
|
||||
total: 0,
|
||||
data: [],
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(result).toEqual(baseConversations);
|
||||
});
|
||||
|
||||
it('returns true when both isEnabledKnowledgeBase and isEnabledRAGAlerts are true', () => {
|
||||
const result = hasParsableResponse({
|
||||
isEnabledRAGAlerts: true,
|
||||
isEnabledKnowledgeBase: true,
|
||||
});
|
||||
it('should return user conversations when base conversations are empty', () => {
|
||||
const result = mergeBaseWithPersistedConversations({}, conversationsData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(result).toEqual({
|
||||
'Conversation 1': {
|
||||
...defaultProps,
|
||||
title: 'Conversation 1',
|
||||
id: 'conversation_1',
|
||||
},
|
||||
'Conversation 2': {
|
||||
...defaultProps,
|
||||
title: 'Conversation 2',
|
||||
id: 'conversation_2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false when both isEnabledKnowledgeBase and isEnabledRAGAlerts are false', () => {
|
||||
const result = hasParsableResponse({
|
||||
isEnabledRAGAlerts: false,
|
||||
isEnabledKnowledgeBase: false,
|
||||
});
|
||||
it('should handle and merge conversations with duplicate titles', () => {
|
||||
const result = mergeBaseWithPersistedConversations(
|
||||
{
|
||||
'Conversation 1': {
|
||||
title: 'Conversation 1',
|
||||
id: 'conversation1',
|
||||
...defaultProps,
|
||||
},
|
||||
},
|
||||
{
|
||||
page: 1,
|
||||
perPage: 10,
|
||||
total: 1,
|
||||
data: [
|
||||
{
|
||||
title: 'Conversation 1',
|
||||
id: 'conversation1',
|
||||
...defaultProps,
|
||||
messages: [
|
||||
{
|
||||
content: 'Message 3',
|
||||
role: 'user' as const,
|
||||
timestamp: '2024-02-14T22:29:43.862Z',
|
||||
},
|
||||
{
|
||||
content: 'Message 4',
|
||||
role: 'user' as const,
|
||||
timestamp: '2024-02-14T22:29:43.862Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(result).toEqual({
|
||||
'Conversation 1': {
|
||||
title: 'Conversation 1',
|
||||
id: 'conversation1',
|
||||
...defaultProps,
|
||||
messages: [
|
||||
{
|
||||
content: 'Message 3',
|
||||
role: 'user',
|
||||
timestamp: '2024-02-14T22:29:43.862Z',
|
||||
},
|
||||
{
|
||||
content: 'Message 4',
|
||||
role: 'user',
|
||||
timestamp: '2024-02-14T22:29:43.862Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,8 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { FetchConnectorExecuteResponse } from './api';
|
||||
import { merge } from 'lodash/fp';
|
||||
import { AIConnector } from '../connectorland/connector_selector';
|
||||
import { FetchConnectorExecuteResponse, FetchConversationsResponse } from './api';
|
||||
import { Conversation } from '../..';
|
||||
import type { Message } from '../assistant_context/types';
|
||||
import { enterpriseMessaging, WELCOME_CONVERSATION } from './use_conversation/sample_conversations';
|
||||
|
@ -34,6 +35,20 @@ export const getMessageFromRawResponse = (rawResponse: FetchConnectorExecuteResp
|
|||
}
|
||||
};
|
||||
|
||||
export const mergeBaseWithPersistedConversations = (
|
||||
baseConversations: Record<string, Conversation>,
|
||||
conversationsData: FetchConversationsResponse
|
||||
): Record<string, Conversation> => {
|
||||
const userConversations = (conversationsData?.data ?? []).reduce<Record<string, Conversation>>(
|
||||
(transformed, conversation) => {
|
||||
transformed[conversation.title] = conversation;
|
||||
return transformed;
|
||||
},
|
||||
{}
|
||||
);
|
||||
return merge(baseConversations, userConversations);
|
||||
};
|
||||
|
||||
export const getBlockBotConversation = (
|
||||
conversation: Conversation,
|
||||
isAssistantEnabled: boolean
|
||||
|
@ -63,36 +78,13 @@ export const getBlockBotConversation = (
|
|||
* @param connectors
|
||||
*/
|
||||
export const getDefaultConnector = (
|
||||
connectors: Array<ActionConnector<Record<string, unknown>, Record<string, unknown>>> | undefined
|
||||
): ActionConnector<Record<string, unknown>, Record<string, unknown>> | undefined =>
|
||||
connectors?.length === 1 ? connectors[0] : undefined;
|
||||
|
||||
/**
|
||||
* When `content` is a JSON string, prefixed with "```json\n"
|
||||
* and suffixed with "\n```", this function will attempt to parse it and return
|
||||
* the `action_input` property if it exists.
|
||||
*/
|
||||
export const getFormattedMessageContent = (content: string): string => {
|
||||
const formattedContentMatch = content.match(/```json\n([\s\S]+)\n```/);
|
||||
|
||||
if (formattedContentMatch) {
|
||||
try {
|
||||
const parsedContent = JSON.parse(formattedContentMatch[1]);
|
||||
|
||||
return parsedContent.action_input ?? content;
|
||||
} catch {
|
||||
// we don't want to throw an error here, so we'll fall back to the original content
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
connectors: AIConnector[] | undefined
|
||||
): AIConnector | undefined => (connectors?.length === 1 ? connectors[0] : undefined);
|
||||
|
||||
interface OptionalRequestParams {
|
||||
alertsIndexPattern?: string;
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
replacements?: Record<string, string>;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
|
@ -101,20 +93,17 @@ export const getOptionalRequestParams = ({
|
|||
alertsIndexPattern,
|
||||
allow,
|
||||
allowReplacement,
|
||||
replacements,
|
||||
size,
|
||||
}: {
|
||||
isEnabledRAGAlerts: boolean;
|
||||
alertsIndexPattern?: string;
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
replacements?: Record<string, string>;
|
||||
size?: number;
|
||||
}): OptionalRequestParams => {
|
||||
const optionalAlertsIndexPattern = alertsIndexPattern ? { alertsIndexPattern } : undefined;
|
||||
const optionalAllow = allow ? { allow } : undefined;
|
||||
const optionalAllowReplacement = allowReplacement ? { allowReplacement } : undefined;
|
||||
const optionalReplacements = replacements ? { replacements } : undefined;
|
||||
const optionalSize = size ? { size } : undefined;
|
||||
|
||||
// the settings toggle must be enabled:
|
||||
|
@ -126,15 +115,12 @@ export const getOptionalRequestParams = ({
|
|||
...optionalAlertsIndexPattern,
|
||||
...optionalAllow,
|
||||
...optionalAllowReplacement,
|
||||
...optionalReplacements,
|
||||
...optionalSize,
|
||||
};
|
||||
};
|
||||
|
||||
export const hasParsableResponse = ({
|
||||
isEnabledRAGAlerts,
|
||||
isEnabledKnowledgeBase,
|
||||
}: {
|
||||
isEnabledRAGAlerts: boolean;
|
||||
isEnabledKnowledgeBase: boolean;
|
||||
}): boolean => isEnabledKnowledgeBase || isEnabledRAGAlerts;
|
||||
export const llmTypeDictionary: Record<string, string> = {
|
||||
'Amazon Bedrock': 'bedrock',
|
||||
'Azure OpenAI': 'openai',
|
||||
OpenAI: 'openai',
|
||||
};
|
||||
|
|
|
@ -7,11 +7,9 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { Assistant } from '.';
|
||||
import { Conversation } from '../assistant_context/types';
|
||||
import type { IHttpFetchError } from '@kbn/core/public';
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
import { useLoadConnectors } from '../connectorland/use_load_connectors';
|
||||
import { useConnectorSetup } from '../connectorland/connector_setup';
|
||||
|
@ -23,6 +21,11 @@ import { useLocalStorage } from 'react-use';
|
|||
import { PromptEditor } from './prompt_editor';
|
||||
import { QuickPrompts } from './quick_prompts/quick_prompts';
|
||||
import { mockAssistantAvailability, TestProviders } from '../mock/test_providers/test_providers';
|
||||
import { useFetchCurrentUserConversations } from './api';
|
||||
import { Conversation } from '../assistant_context/types';
|
||||
import * as all from './chat_send/use_chat_send';
|
||||
import { useConversation } from './use_conversation';
|
||||
import { AIConnector } from '../connectorland/connector_selector';
|
||||
|
||||
jest.mock('../connectorland/use_load_connectors');
|
||||
jest.mock('../connectorland/connector_setup');
|
||||
|
@ -30,31 +33,45 @@ jest.mock('react-use');
|
|||
|
||||
jest.mock('./prompt_editor', () => ({ PromptEditor: jest.fn() }));
|
||||
jest.mock('./quick_prompts/quick_prompts', () => ({ QuickPrompts: jest.fn() }));
|
||||
jest.mock('./api/conversations/use_fetch_current_user_conversations');
|
||||
|
||||
const MOCK_CONVERSATION_TITLE = 'electric sheep';
|
||||
|
||||
const getInitialConversations = (): Record<string, Conversation> => ({
|
||||
[WELCOME_CONVERSATION_TITLE]: {
|
||||
id: WELCOME_CONVERSATION_TITLE,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
},
|
||||
[MOCK_CONVERSATION_TITLE]: {
|
||||
id: MOCK_CONVERSATION_TITLE,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
},
|
||||
});
|
||||
jest.mock('./use_conversation');
|
||||
|
||||
const renderAssistant = (extraProps = {}, providerProps = {}) =>
|
||||
render(
|
||||
<TestProviders getInitialConversations={getInitialConversations} {...providerProps}>
|
||||
<TestProviders>
|
||||
<Assistant {...extraProps} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
const mockData = {
|
||||
Welcome: {
|
||||
id: 'Welcome Id',
|
||||
title: 'Welcome',
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
},
|
||||
'electric sheep': {
|
||||
id: 'electric sheep id',
|
||||
category: 'assistant',
|
||||
title: 'electric sheep',
|
||||
messages: [],
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
},
|
||||
};
|
||||
const mockDeleteConvo = jest.fn();
|
||||
const mockUseConversation = {
|
||||
getConversation: jest.fn(),
|
||||
getDefaultConversation: jest.fn().mockReturnValue(mockData.Welcome),
|
||||
deleteConversation: mockDeleteConvo,
|
||||
};
|
||||
|
||||
describe('Assistant', () => {
|
||||
beforeAll(() => {
|
||||
(useConversation as jest.Mock).mockReturnValue(mockUseConversation);
|
||||
jest.mocked(useConnectorSetup).mockReturnValue({
|
||||
comments: [],
|
||||
prompt: <></>,
|
||||
|
@ -62,6 +79,32 @@ describe('Assistant', () => {
|
|||
|
||||
jest.mocked(PromptEditor).mockReturnValue(null);
|
||||
jest.mocked(QuickPrompts).mockReturnValue(null);
|
||||
const connectors: unknown[] = [
|
||||
{
|
||||
id: 'hi',
|
||||
name: 'OpenAI connector',
|
||||
actionTypeId: '.gen-ai',
|
||||
},
|
||||
];
|
||||
jest.mocked(useLoadConnectors).mockReturnValue({
|
||||
isSuccess: true,
|
||||
data: connectors,
|
||||
} as unknown as UseQueryResult<AIConnector[], IHttpFetchError>);
|
||||
|
||||
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
|
||||
data: mockData,
|
||||
isLoading: false,
|
||||
refetch: jest.fn().mockResolvedValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
...mockData,
|
||||
Welcome: {
|
||||
...mockData.Welcome,
|
||||
apiConfig: { newProp: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as unknown as UseQueryResult<Record<string, Conversation>, unknown>);
|
||||
});
|
||||
|
||||
let persistToLocalStorage: jest.Mock;
|
||||
|
@ -77,15 +120,85 @@ describe('Assistant', () => {
|
|||
>);
|
||||
});
|
||||
|
||||
describe('persistent storage', () => {
|
||||
it('should refetchConversationsState after settings save button click', async () => {
|
||||
const chatSendSpy = jest.spyOn(all, 'useChatSend');
|
||||
const setConversationTitle = jest.fn();
|
||||
|
||||
renderAssistant({ setConversationTitle });
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings'));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('save-button'));
|
||||
});
|
||||
|
||||
expect(chatSendSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
currentConversation: {
|
||||
apiConfig: { newProp: true },
|
||||
category: 'assistant',
|
||||
id: 'Welcome Id',
|
||||
messages: [],
|
||||
title: 'Welcome',
|
||||
replacements: [],
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should refetchConversationsState after settings save button click, but do not update convos when refetch returns bad results', async () => {
|
||||
const { Welcome, ...rest } = mockData;
|
||||
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
|
||||
data: mockData,
|
||||
isLoading: false,
|
||||
refetch: jest.fn().mockResolvedValue({
|
||||
isLoading: false,
|
||||
data: rest,
|
||||
}),
|
||||
} as unknown as UseQueryResult<Record<string, Conversation>, unknown>);
|
||||
const chatSendSpy = jest.spyOn(all, 'useChatSend');
|
||||
const setConversationTitle = jest.fn();
|
||||
|
||||
renderAssistant({ setConversationTitle });
|
||||
|
||||
fireEvent.click(screen.getByTestId('settings'));
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('save-button'));
|
||||
});
|
||||
|
||||
expect(chatSendSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
currentConversation: {
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
id: 'Welcome Id',
|
||||
messages: [],
|
||||
title: 'Welcome',
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete conversation when delete button is clicked', async () => {
|
||||
renderAssistant();
|
||||
await act(async () => {
|
||||
fireEvent.click(
|
||||
within(screen.getByTestId('conversation-selector')).getByTestId(
|
||||
'comboBoxToggleListButton'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const deleteButton = screen.getAllByTestId('delete-option')[0];
|
||||
await act(async () => {
|
||||
fireEvent.click(deleteButton);
|
||||
});
|
||||
expect(mockDeleteConvo).toHaveBeenCalledWith('Welcome Id');
|
||||
});
|
||||
});
|
||||
describe('when selected conversation changes and some connectors are loaded', () => {
|
||||
it('should persist the conversation id to local storage', async () => {
|
||||
const connectors: unknown[] = [{}];
|
||||
|
||||
jest.mocked(useLoadConnectors).mockReturnValue({
|
||||
isSuccess: true,
|
||||
data: connectors,
|
||||
} as unknown as UseQueryResult<ActionConnector[], IHttpFetchError>);
|
||||
|
||||
it('should persist the conversation title to local storage', async () => {
|
||||
renderAssistant();
|
||||
|
||||
expect(persistToLocalStorage).toHaveBeenCalled();
|
||||
|
@ -103,31 +216,19 @@ describe('Assistant', () => {
|
|||
});
|
||||
|
||||
it('should not persist the conversation id to local storage when excludeFromLastConversationStorage flag is indicated', async () => {
|
||||
const connectors: unknown[] = [{}];
|
||||
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
|
||||
data: {
|
||||
...mockData,
|
||||
'electric sheep': {
|
||||
...mockData['electric sheep'],
|
||||
excludeFromLastConversationStorage: true,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
refetch: jest.fn(),
|
||||
} as unknown as UseQueryResult<Record<string, Conversation>, unknown>);
|
||||
|
||||
jest.mocked(useLoadConnectors).mockReturnValue({
|
||||
isSuccess: true,
|
||||
data: connectors,
|
||||
} as unknown as UseQueryResult<ActionConnector[], IHttpFetchError>);
|
||||
|
||||
const { getByLabelText } = renderAssistant(
|
||||
{},
|
||||
{
|
||||
getInitialConversations: () => ({
|
||||
[WELCOME_CONVERSATION_TITLE]: {
|
||||
id: WELCOME_CONVERSATION_TITLE,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
},
|
||||
[MOCK_CONVERSATION_TITLE]: {
|
||||
id: MOCK_CONVERSATION_TITLE,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
excludeFromLastConversationStorage: true,
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
const { getByLabelText } = renderAssistant();
|
||||
|
||||
expect(persistToLocalStorage).toHaveBeenCalled();
|
||||
|
||||
|
@ -142,33 +243,81 @@ describe('Assistant', () => {
|
|||
});
|
||||
expect(persistToLocalStorage).toHaveBeenLastCalledWith(WELCOME_CONVERSATION_TITLE);
|
||||
});
|
||||
it('should call the setConversationId callback if it is defined and the conversation id changes', async () => {
|
||||
const connectors: unknown[] = [{}];
|
||||
const setConversationId = jest.fn();
|
||||
jest.mocked(useLoadConnectors).mockReturnValue({
|
||||
isSuccess: true,
|
||||
data: connectors,
|
||||
} as unknown as UseQueryResult<ActionConnector[], IHttpFetchError>);
|
||||
it('should call the setConversationTitle callback if it is defined and the conversation id changes', async () => {
|
||||
const setConversationTitle = jest.fn();
|
||||
|
||||
renderAssistant({ setConversationId });
|
||||
renderAssistant({ setConversationTitle });
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByLabelText('Previous conversation'));
|
||||
});
|
||||
|
||||
expect(setConversationId).toHaveBeenLastCalledWith('electric sheep');
|
||||
expect(setConversationTitle).toHaveBeenLastCalledWith('electric sheep');
|
||||
});
|
||||
it('should fetch current conversation when id has value', async () => {
|
||||
const chatSendSpy = jest.spyOn(all, 'useChatSend');
|
||||
(useConversation as jest.Mock).mockReturnValue({
|
||||
...mockUseConversation,
|
||||
getConversation: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ ...mockData['electric sheep'], title: 'updated title' }),
|
||||
});
|
||||
renderAssistant();
|
||||
|
||||
const previousConversationButton = screen.getByLabelText('Previous conversation');
|
||||
await act(async () => {
|
||||
fireEvent.click(previousConversationButton);
|
||||
});
|
||||
|
||||
expect(chatSendSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
currentConversation: {
|
||||
...mockData['electric sheep'],
|
||||
title: 'updated title',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(persistToLocalStorage).toHaveBeenLastCalledWith('updated title');
|
||||
});
|
||||
it('should refetch all conversations when id is empty', async () => {
|
||||
const chatSendSpy = jest.spyOn(all, 'useChatSend');
|
||||
jest.mocked(useFetchCurrentUserConversations).mockReturnValue({
|
||||
data: {
|
||||
...mockData,
|
||||
'electric sheep': { ...mockData['electric sheep'], id: '' },
|
||||
},
|
||||
isLoading: false,
|
||||
refetch: jest.fn().mockResolvedValue({
|
||||
isLoading: false,
|
||||
data: {
|
||||
...mockData,
|
||||
'electric sheep': {
|
||||
...mockData['electric sheep'],
|
||||
apiConfig: { newProp: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as unknown as UseQueryResult<Record<string, Conversation>, unknown>);
|
||||
renderAssistant();
|
||||
|
||||
const previousConversationButton = screen.getByLabelText('Previous conversation');
|
||||
await act(async () => {
|
||||
fireEvent.click(previousConversationButton);
|
||||
});
|
||||
expect(chatSendSpy).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
currentConversation: {
|
||||
...mockData['electric sheep'],
|
||||
apiConfig: { newProp: true },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no connectors are loaded', () => {
|
||||
it('should set welcome conversation id in local storage', async () => {
|
||||
const emptyConnectors: unknown[] = [];
|
||||
|
||||
jest.mocked(useLoadConnectors).mockReturnValue({
|
||||
isSuccess: true,
|
||||
data: emptyConnectors,
|
||||
} as unknown as UseQueryResult<ActionConnector[], IHttpFetchError>);
|
||||
|
||||
renderAssistant();
|
||||
|
||||
expect(persistToLocalStorage).toHaveBeenCalled();
|
||||
|
|
|
@ -29,14 +29,16 @@ import {
|
|||
import { createPortal } from 'react-dom';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { ActionConnectorProps } from '@kbn/triggers-actions-ui-plugin/public/types';
|
||||
import { useChatSend } from './chat_send/use_chat_send';
|
||||
import { ChatSend } from './chat_send';
|
||||
import { BlockBotCallToAction } from './block_bot/cta';
|
||||
import { AssistantHeader } from './assistant_header';
|
||||
import { WELCOME_CONVERSATION_TITLE } from './use_conversation/translations';
|
||||
import { getDefaultConnector, getBlockBotConversation } from './helpers';
|
||||
import {
|
||||
getDefaultConnector,
|
||||
getBlockBotConversation,
|
||||
mergeBaseWithPersistedConversations,
|
||||
} from './helpers';
|
||||
|
||||
import { useAssistantContext } from '../assistant_context';
|
||||
import { ContextPills } from './context_pills';
|
||||
|
@ -49,14 +51,20 @@ import { QuickPrompts } from './quick_prompts/quick_prompts';
|
|||
import { useLoadConnectors } from '../connectorland/use_load_connectors';
|
||||
import { useConnectorSetup } from '../connectorland/connector_setup';
|
||||
import { ConnectorMissingCallout } from '../connectorland/connector_missing_callout';
|
||||
import {
|
||||
FetchConversationsResponse,
|
||||
useFetchCurrentUserConversations,
|
||||
} from './api/conversations/use_fetch_current_user_conversations';
|
||||
import { Conversation } from '../assistant_context/types';
|
||||
import { clearPresentationData } from '../connectorland/connector_setup/helpers';
|
||||
|
||||
export interface Props {
|
||||
conversationId?: string;
|
||||
conversationTitle?: string;
|
||||
embeddedLayout?: boolean;
|
||||
promptContextId?: string;
|
||||
shouldRefocusPrompt?: boolean;
|
||||
showTitle?: boolean;
|
||||
setConversationId?: Dispatch<SetStateAction<string>>;
|
||||
setConversationTitle?: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,76 +72,126 @@ export interface Props {
|
|||
* quick prompts for common actions, settings, and prompt context providers.
|
||||
*/
|
||||
const AssistantComponent: React.FC<Props> = ({
|
||||
conversationId,
|
||||
conversationTitle,
|
||||
embeddedLayout = false,
|
||||
promptContextId = '',
|
||||
shouldRefocusPrompt = false,
|
||||
showTitle = true,
|
||||
setConversationId,
|
||||
setConversationTitle,
|
||||
}) => {
|
||||
const {
|
||||
actionTypeRegistry,
|
||||
assistantTelemetry,
|
||||
augmentMessageCodeBlocks,
|
||||
assistantAvailability: { isAssistantEnabled },
|
||||
conversations,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
promptContexts,
|
||||
setLastConversationId,
|
||||
getConversationId,
|
||||
setLastConversationTitle,
|
||||
getLastConversationTitle,
|
||||
title,
|
||||
allSystemPrompts,
|
||||
baseConversations,
|
||||
} = useAssistantContext();
|
||||
|
||||
const { getDefaultConversation, getConversation, deleteConversation } = useConversation();
|
||||
|
||||
const [selectedPromptContexts, setSelectedPromptContexts] = useState<
|
||||
Record<string, SelectedPromptContext>
|
||||
>({});
|
||||
const [conversations, setConversations] = useState<Record<string, Conversation>>({});
|
||||
const selectedPromptContextsCount = useMemo(
|
||||
() => Object.keys(selectedPromptContexts).length,
|
||||
[selectedPromptContexts]
|
||||
);
|
||||
|
||||
const { amendMessage, createConversation } = useConversation();
|
||||
const onFetchedConversations = useCallback(
|
||||
(conversationsData: FetchConversationsResponse): Record<string, Conversation> =>
|
||||
mergeBaseWithPersistedConversations(baseConversations, conversationsData),
|
||||
[baseConversations]
|
||||
);
|
||||
const {
|
||||
data: conversationsData,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
} = useFetchCurrentUserConversations({ http, onFetch: onFetchedConversations });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isError) {
|
||||
setConversations(conversationsData ?? {});
|
||||
}
|
||||
}, [conversationsData, isError, isLoading]);
|
||||
|
||||
const refetchResults = useCallback(async () => {
|
||||
const updatedConv = await refetch();
|
||||
if (!updatedConv.isLoading) {
|
||||
setConversations(updatedConv.data ?? {});
|
||||
return updatedConv.data;
|
||||
}
|
||||
}, [refetch]);
|
||||
|
||||
// Connector details
|
||||
const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({ http });
|
||||
const defaultConnectorId = useMemo(() => getDefaultConnector(connectors)?.id, [connectors]);
|
||||
const defaultProvider = useMemo(
|
||||
() =>
|
||||
(
|
||||
getDefaultConnector(connectors) as ActionConnectorProps<
|
||||
{ apiProvider: OpenAiProviderType },
|
||||
unknown
|
||||
>
|
||||
)?.config?.apiProvider,
|
||||
[connectors]
|
||||
);
|
||||
const { data: connectors, isSuccess: areConnectorsFetched } = useLoadConnectors({
|
||||
actionTypeRegistry,
|
||||
http,
|
||||
});
|
||||
const defaultConnector = useMemo(() => getDefaultConnector(connectors), [connectors]);
|
||||
|
||||
const [selectedConversationId, setSelectedConversationId] = useState<string>(
|
||||
isAssistantEnabled ? getConversationId(conversationId) : WELCOME_CONVERSATION_TITLE
|
||||
const [selectedConversationTitle, setSelectedConversationTitle] = useState<string>(
|
||||
isAssistantEnabled ? getLastConversationTitle(conversationTitle) : WELCOME_CONVERSATION_TITLE
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (setConversationId) {
|
||||
setConversationId(selectedConversationId);
|
||||
if (setConversationTitle) {
|
||||
setConversationTitle(selectedConversationTitle);
|
||||
}
|
||||
}, [selectedConversationId, setConversationId]);
|
||||
}, [selectedConversationTitle, setConversationTitle]);
|
||||
|
||||
const currentConversation = useMemo(
|
||||
() =>
|
||||
conversations[selectedConversationId] ??
|
||||
createConversation({ conversationId: selectedConversationId }),
|
||||
[conversations, createConversation, selectedConversationId]
|
||||
const [currentConversation, setCurrentConversation] = useState<Conversation>(
|
||||
getDefaultConversation({ cTitle: selectedConversationTitle })
|
||||
);
|
||||
|
||||
const refetchCurrentConversation = useCallback(
|
||||
async (cId?: string) => {
|
||||
if (cId === '' || !conversations[selectedConversationTitle]) {
|
||||
return;
|
||||
}
|
||||
const updatedConversation = await getConversation(
|
||||
cId ?? conversations[selectedConversationTitle].id
|
||||
);
|
||||
if (updatedConversation) {
|
||||
setCurrentConversation(updatedConversation);
|
||||
}
|
||||
return updatedConversation;
|
||||
},
|
||||
[conversations, getConversation, selectedConversationTitle]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && Object.keys(conversations).length > 0) {
|
||||
const conversation =
|
||||
conversations[selectedConversationTitle ?? getLastConversationTitle(conversationTitle)];
|
||||
if (conversation) {
|
||||
setCurrentConversation(conversation);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
conversationTitle,
|
||||
conversations,
|
||||
getLastConversationTitle,
|
||||
isLoading,
|
||||
selectedConversationTitle,
|
||||
]);
|
||||
|
||||
// Welcome setup state
|
||||
const isWelcomeSetup = useMemo(() => {
|
||||
// if any conversation has a connector id, we're not in welcome set up
|
||||
return Object.keys(conversations).some(
|
||||
(conversation) => conversations[conversation].apiConfig.connectorId != null
|
||||
(conversation) => conversations[conversation].apiConfig?.connectorId != null
|
||||
)
|
||||
? false
|
||||
: (connectors?.length ?? 0) === 0;
|
||||
|
@ -154,20 +212,20 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
// Clear it if there is no connectors
|
||||
useEffect(() => {
|
||||
if (areConnectorsFetched && !connectors?.length) {
|
||||
return setLastConversationId(WELCOME_CONVERSATION_TITLE);
|
||||
return setLastConversationTitle(WELCOME_CONVERSATION_TITLE);
|
||||
}
|
||||
|
||||
if (!currentConversation.excludeFromLastConversationStorage) {
|
||||
setLastConversationId(currentConversation.id);
|
||||
setLastConversationTitle(currentConversation.title);
|
||||
}
|
||||
}, [areConnectorsFetched, connectors?.length, currentConversation, setLastConversationId]);
|
||||
|
||||
const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({
|
||||
conversation: blockBotConversation,
|
||||
});
|
||||
|
||||
const currentTitle: string | JSX.Element =
|
||||
isWelcomeSetup && blockBotConversation.theme?.title ? blockBotConversation.theme?.title : title;
|
||||
}, [
|
||||
areConnectorsFetched,
|
||||
connectors?.length,
|
||||
conversationsData,
|
||||
currentConversation,
|
||||
isLoading,
|
||||
setLastConversationTitle,
|
||||
]);
|
||||
|
||||
const [promptTextPreview, setPromptTextPreview] = useState<string>('');
|
||||
const [autoPopulatedOnce, setAutoPopulatedOnce] = useState<boolean>(false);
|
||||
|
@ -182,9 +240,9 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
useLayoutEffect(() => {
|
||||
// need in order for code block controls to be added to the DOM
|
||||
setTimeout(() => {
|
||||
setMessageCodeBlocks(augmentMessageCodeBlocks(currentConversation));
|
||||
setMessageCodeBlocks(augmentMessageCodeBlocks(currentConversation, showAnonymizedValues));
|
||||
}, 0);
|
||||
}, [augmentMessageCodeBlocks, currentConversation]);
|
||||
}, [augmentMessageCodeBlocks, currentConversation, showAnonymizedValues]);
|
||||
|
||||
const isSendingDisabled = useMemo(() => {
|
||||
return isDisabled || showMissingConnectorCallout;
|
||||
|
@ -235,13 +293,48 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
);
|
||||
|
||||
const handleOnConversationSelected = useCallback(
|
||||
(cId: string) => {
|
||||
setSelectedConversationId(cId);
|
||||
setEditingSystemPromptId(
|
||||
getDefaultSystemPrompt({ allSystemPrompts, conversation: conversations[cId] })?.id
|
||||
);
|
||||
async ({ cId, cTitle }: { cId: string; cTitle: string }) => {
|
||||
if (cId === '') {
|
||||
const updatedConv = await refetchResults();
|
||||
if (updatedConv) {
|
||||
setCurrentConversation(updatedConv[cTitle]);
|
||||
setSelectedConversationTitle(cTitle);
|
||||
setEditingSystemPromptId(
|
||||
getDefaultSystemPrompt({ allSystemPrompts, conversation: updatedConv[cTitle] })?.id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setSelectedConversationTitle(cTitle);
|
||||
const refetchedConversation = await refetchCurrentConversation(cId);
|
||||
setEditingSystemPromptId(
|
||||
getDefaultSystemPrompt({ allSystemPrompts, conversation: refetchedConversation })?.id
|
||||
);
|
||||
}
|
||||
},
|
||||
[allSystemPrompts, conversations]
|
||||
[allSystemPrompts, refetchCurrentConversation, refetchResults]
|
||||
);
|
||||
|
||||
const { comments: connectorComments, prompt: connectorPrompt } = useConnectorSetup({
|
||||
conversation: blockBotConversation,
|
||||
onConversationUpdate: handleOnConversationSelected,
|
||||
onSetupComplete: () => {
|
||||
setConversations({
|
||||
...conversations,
|
||||
[currentConversation.title]: clearPresentationData(currentConversation),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleOnConversationDeleted = useCallback(
|
||||
async (cTitle: string) => {
|
||||
setTimeout(() => {
|
||||
deleteConversation(conversations[cTitle].id);
|
||||
}, 0);
|
||||
const deletedConv = { ...conversations };
|
||||
delete deletedConv[cTitle];
|
||||
setConversations(deletedConv);
|
||||
},
|
||||
[conversations, deleteConversation]
|
||||
);
|
||||
|
||||
const handleOnSystemPromptSelectionChange = useCallback((systemPromptId?: string) => {
|
||||
|
@ -264,8 +357,8 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Adding `conversationId !== selectedConversationId` to prevent auto-run still executing after changing selected conversation
|
||||
if (currentConversation.messages.length || conversationId !== selectedConversationId) {
|
||||
// Adding `conversationTitle !== selectedConversationTitle` to prevent auto-run still executing after changing selected conversation
|
||||
if (currentConversation.messages.length || conversationTitle !== selectedConversationTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -302,8 +395,8 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
currentConversation.messages,
|
||||
promptContexts,
|
||||
promptContextId,
|
||||
conversationId,
|
||||
selectedConversationId,
|
||||
conversationTitle,
|
||||
selectedConversationTitle,
|
||||
selectedPromptContexts,
|
||||
autoPopulatedOnce,
|
||||
defaultAllow,
|
||||
|
@ -356,6 +449,8 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
setEditingSystemPromptId,
|
||||
selectedPromptContexts,
|
||||
setSelectedPromptContexts,
|
||||
setCurrentConversation,
|
||||
refresh: refetchCurrentConversation,
|
||||
});
|
||||
|
||||
const chatbotComments = useMemo(
|
||||
|
@ -365,7 +460,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
comments={getComments({
|
||||
currentConversation,
|
||||
showAnonymizedValues,
|
||||
amendMessage,
|
||||
refetchCurrentConversation,
|
||||
regenerateMessage: handleRegenerateResponse,
|
||||
isFetchingResponse: isLoadingChatSend,
|
||||
})}
|
||||
|
@ -395,7 +490,7 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
</>
|
||||
),
|
||||
[
|
||||
amendMessage,
|
||||
refetchCurrentConversation,
|
||||
currentConversation,
|
||||
editingSystemPromptId,
|
||||
getComments,
|
||||
|
@ -429,13 +524,20 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
const trackPrompt = useCallback(
|
||||
(promptTitle: string) => {
|
||||
assistantTelemetry?.reportAssistantQuickPrompt({
|
||||
conversationId: selectedConversationId,
|
||||
conversationId: currentConversation.title,
|
||||
promptTitle,
|
||||
});
|
||||
},
|
||||
[assistantTelemetry, selectedConversationId]
|
||||
[assistantTelemetry, currentConversation.title]
|
||||
);
|
||||
|
||||
const refetchConversationsState = useCallback(async () => {
|
||||
const refetchedConversations = await refetchResults();
|
||||
if (refetchedConversations && refetchedConversations[currentConversation.title]) {
|
||||
setCurrentConversation(refetchedConversations[currentConversation.title]);
|
||||
}
|
||||
}, [currentConversation.title, refetchResults]);
|
||||
|
||||
return getWrapper(
|
||||
<>
|
||||
<EuiModalHeader
|
||||
|
@ -447,18 +549,19 @@ const AssistantComponent: React.FC<Props> = ({
|
|||
{showTitle && (
|
||||
<AssistantHeader
|
||||
currentConversation={currentConversation}
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
setCurrentConversation={setCurrentConversation}
|
||||
defaultConnector={defaultConnector}
|
||||
docLinks={docLinks}
|
||||
isDisabled={isDisabled}
|
||||
isSettingsModalVisible={isSettingsModalVisible}
|
||||
onConversationSelected={handleOnConversationSelected}
|
||||
onToggleShowAnonymizedValues={onToggleShowAnonymizedValues}
|
||||
selectedConversationId={selectedConversationId}
|
||||
setIsSettingsModalVisible={setIsSettingsModalVisible}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
showAnonymizedValues={showAnonymizedValues}
|
||||
title={currentTitle}
|
||||
title={title}
|
||||
conversations={conversations}
|
||||
onConversationDeleted={handleOnConversationDeleted}
|
||||
refetchConversationsState={refetchConversationsState}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -64,9 +64,8 @@ describe('helpers', () => {
|
|||
describe('getCombinedMessage', () => {
|
||||
it('returns correct content for a new chat with a system prompt', async () => {
|
||||
const message: Message = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
isNewChat: true,
|
||||
onNewReplacements: jest.fn(),
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
|
@ -87,9 +86,8 @@ User prompt text`);
|
|||
|
||||
it('returns correct content for a new chat WITHOUT a system prompt', async () => {
|
||||
const message: Message = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
isNewChat: true,
|
||||
onNewReplacements: jest.fn(),
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
|
@ -109,9 +107,8 @@ User prompt text`);
|
|||
|
||||
it('returns the correct content for an existing chat', async () => {
|
||||
const message: Message = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
isNewChat: false,
|
||||
onNewReplacements: jest.fn(),
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
|
@ -129,9 +126,8 @@ User prompt text`);
|
|||
|
||||
it('returns the expected role', async () => {
|
||||
const message: Message = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
isNewChat: true,
|
||||
onNewReplacements: jest.fn(),
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockSelectedAlertPromptContext.promptContextId]: mockSelectedAlertPromptContext,
|
||||
|
@ -144,9 +140,8 @@ User prompt text`);
|
|||
|
||||
it('returns a valid timestamp', async () => {
|
||||
const message: Message = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
isNewChat: true,
|
||||
onNewReplacements: jest.fn(),
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
|
@ -156,8 +151,6 @@ User prompt text`);
|
|||
});
|
||||
|
||||
describe('when there is data to anonymize', () => {
|
||||
const onNewReplacements = jest.fn();
|
||||
|
||||
const mockPromptContextWithDataToAnonymize: SelectedPromptContext = {
|
||||
allow: ['field1', 'field2'],
|
||||
allowReplacement: ['field1', 'field2'],
|
||||
|
@ -169,11 +162,10 @@ User prompt text`);
|
|||
};
|
||||
|
||||
it('invokes `onNewReplacements` with the expected replacements', async () => {
|
||||
await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
const message = await getCombinedMessage({
|
||||
currentReplacements: [],
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
isNewChat: true,
|
||||
onNewReplacements,
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {
|
||||
[mockPromptContextWithDataToAnonymize.promptContextId]:
|
||||
|
@ -182,22 +174,21 @@ User prompt text`);
|
|||
selectedSystemPrompt: mockSystemPrompt,
|
||||
});
|
||||
|
||||
expect(onNewReplacements).toBeCalledWith({
|
||||
elzoof: 'foozle',
|
||||
oof: 'foo',
|
||||
rab: 'bar',
|
||||
zab: 'baz',
|
||||
});
|
||||
expect(message.replacements).toEqual([
|
||||
{ uuid: 'oof', value: 'foo' },
|
||||
{ uuid: 'rab', value: 'bar' },
|
||||
{ uuid: 'zab', value: 'baz' },
|
||||
{ uuid: 'elzoof', value: 'foozle' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the expected content when `isNewChat` is false', async () => {
|
||||
const isNewChat = false; // <-- not a new chat
|
||||
|
||||
const message: Message = await getCombinedMessage({
|
||||
currentReplacements: {},
|
||||
currentReplacements: [],
|
||||
getAnonymizedValue: mockGetAnonymizedValue,
|
||||
isNewChat,
|
||||
onNewReplacements: jest.fn(),
|
||||
promptText: 'User prompt text',
|
||||
selectedPromptContexts: {},
|
||||
selectedSystemPrompt: mockSystemPrompt,
|
||||
|
|
|
@ -5,13 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transformRawData } from '@kbn/elastic-assistant-common';
|
||||
|
||||
import type { Message } from '../../assistant_context/types';
|
||||
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations';
|
||||
import { Replacement, transformRawData } from '@kbn/elastic-assistant-common';
|
||||
import { getAnonymizedValue as defaultGetAnonymizedValue } from '../get_anonymized_value';
|
||||
import type { Message } from '../../assistant_context/types';
|
||||
import type { SelectedPromptContext } from '../prompt_context/types';
|
||||
import type { Prompt } from '../types';
|
||||
import { SYSTEM_PROMPT_CONTEXT_NON_I18N } from '../../content/prompts/system/translations';
|
||||
|
||||
export const getSystemMessages = ({
|
||||
isNewChat,
|
||||
|
@ -33,16 +32,15 @@ export const getSystemMessages = ({
|
|||
return [message];
|
||||
};
|
||||
|
||||
export async function getCombinedMessage({
|
||||
export function getCombinedMessage({
|
||||
currentReplacements,
|
||||
getAnonymizedValue = defaultGetAnonymizedValue,
|
||||
isNewChat,
|
||||
onNewReplacements,
|
||||
promptText,
|
||||
selectedPromptContexts,
|
||||
selectedSystemPrompt,
|
||||
}: {
|
||||
currentReplacements: Record<string, string> | undefined;
|
||||
currentReplacements: Replacement[] | undefined;
|
||||
getAnonymizedValue?: ({
|
||||
currentReplacements,
|
||||
rawValue,
|
||||
|
@ -51,15 +49,19 @@ export async function getCombinedMessage({
|
|||
rawValue: string;
|
||||
}) => string;
|
||||
isNewChat: boolean;
|
||||
onNewReplacements: (newReplacements: Record<string, string>) => void;
|
||||
promptText: string;
|
||||
selectedPromptContexts: Record<string, SelectedPromptContext>;
|
||||
selectedSystemPrompt: Prompt | undefined;
|
||||
}): Promise<Message> {
|
||||
}): Message {
|
||||
const replacements: Replacement[] = currentReplacements ?? [];
|
||||
const onNewReplacements = (newReplacements: Replacement[]) => {
|
||||
replacements.push(...newReplacements);
|
||||
};
|
||||
|
||||
const promptContextsContent = Object.keys(selectedPromptContexts)
|
||||
.sort()
|
||||
.map((id) => {
|
||||
const promptContext = transformRawData({
|
||||
const promptContextData = transformRawData({
|
||||
allow: selectedPromptContexts[id].allow,
|
||||
allowReplacement: selectedPromptContexts[id].allowReplacement,
|
||||
currentReplacements,
|
||||
|
@ -68,16 +70,15 @@ export async function getCombinedMessage({
|
|||
rawData: selectedPromptContexts[id].rawData,
|
||||
});
|
||||
|
||||
return `${SYSTEM_PROMPT_CONTEXT_NON_I18N(promptContext)}`;
|
||||
return `${SYSTEM_PROMPT_CONTEXT_NON_I18N(promptContextData)}`;
|
||||
});
|
||||
|
||||
return {
|
||||
content: `${
|
||||
isNewChat ? `${selectedSystemPrompt?.content ?? ''}\n\n` : ''
|
||||
}${promptContextsContent}
|
||||
|
||||
${promptText}`,
|
||||
}${promptContextsContent}\n\n${promptText}`,
|
||||
role: 'user', // we are combining the system and user messages into one message
|
||||
timestamp: new Date().toLocaleString(),
|
||||
replacements,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import { WELCOME_CONVERSATION } from '../../use_conversation/sample_conversation
|
|||
const BASE_CONVERSATION: Conversation = {
|
||||
...WELCOME_CONVERSATION,
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: mockSystemPrompt.id,
|
||||
},
|
||||
};
|
||||
|
@ -372,14 +374,19 @@ describe('SystemPrompt', () => {
|
|||
it('should save new prompt correctly when prompt is removed from a conversation and linked to another conversation in a single transaction', async () => {
|
||||
const secondMockConversation: Conversation = {
|
||||
id: 'second',
|
||||
category: 'assistant',
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: undefined,
|
||||
},
|
||||
title: 'second',
|
||||
messages: [],
|
||||
replacements: [],
|
||||
};
|
||||
const localMockConversations: Record<string, Conversation> = {
|
||||
[DEFAULT_CONVERSATION_TITLE]: BASE_CONVERSATION,
|
||||
[secondMockConversation.id]: secondMockConversation,
|
||||
[secondMockConversation.title]: secondMockConversation,
|
||||
};
|
||||
|
||||
const localMockUseAssistantContext = {
|
||||
|
@ -451,9 +458,11 @@ describe('SystemPrompt', () => {
|
|||
defaultSystemPromptId: undefined,
|
||||
}),
|
||||
}),
|
||||
[secondMockConversation.id]: {
|
||||
[secondMockConversation.title]: {
|
||||
...secondMockConversation,
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: mockSystemPrompt.id,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -36,12 +36,12 @@ const SystemPromptComponent: React.FC<Props> = ({
|
|||
if (editingSystemPromptId !== undefined) {
|
||||
return (
|
||||
allSystemPrompts?.find((p) => p.id === editingSystemPromptId) ??
|
||||
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig.defaultSystemPromptId)
|
||||
allSystemPrompts?.find((p) => p.id === conversation?.apiConfig?.defaultSystemPromptId)
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [allSystemPrompts, conversation?.apiConfig.defaultSystemPromptId, editingSystemPromptId]);
|
||||
}, [allSystemPrompts, conversation?.apiConfig?.defaultSystemPromptId, editingSystemPromptId]);
|
||||
|
||||
const [isEditing, setIsEditing] = React.useState<boolean>(false);
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ import { TEST_IDS } from '../../../constants';
|
|||
export interface Props {
|
||||
allSystemPrompts: Prompt[];
|
||||
compressed?: boolean;
|
||||
conversation: Conversation | undefined;
|
||||
conversation?: Conversation;
|
||||
selectedPrompt: Prompt | undefined;
|
||||
clearSelectedSystemPrompt?: () => void;
|
||||
isClearable?: boolean;
|
||||
|
@ -71,9 +71,9 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
// Write the selected system prompt to the conversation config
|
||||
const setSelectedSystemPrompt = useCallback(
|
||||
(prompt: Prompt | undefined) => {
|
||||
if (conversation) {
|
||||
if (conversation && conversation.apiConfig) {
|
||||
setApiConfig({
|
||||
conversationId: conversation.id,
|
||||
conversation,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
defaultSystemPromptId: prompt?.id,
|
||||
|
@ -174,7 +174,7 @@ const SelectSystemPromptComponent: React.FC<Props> = ({
|
|||
onBlur={handleOnBlur}
|
||||
options={[...options, addNewSystemPrompt]}
|
||||
placeholder={i18n.SELECT_A_SYSTEM_PROMPT}
|
||||
valueOfSelected={selectedPrompt?.id ?? allSystemPrompts[0].id}
|
||||
valueOfSelected={selectedPrompt?.id ?? allSystemPrompts[0]?.id}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
|
|
@ -23,16 +23,16 @@ describe('ConversationMultiSelector', () => {
|
|||
});
|
||||
it('Selects an existing quick prompt', () => {
|
||||
const { getByTestId } = render(<ConversationMultiSelector {...testProps} />);
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(welcomeConvo.id);
|
||||
expect(getByTestId('euiComboBoxPill')).toHaveTextContent(welcomeConvo.title);
|
||||
fireEvent.click(getByTestId('comboBoxToggleListButton'));
|
||||
fireEvent.click(getByTestId(`conversationMultiSelectorOption-${alertConvo.id}`));
|
||||
fireEvent.click(getByTestId(`conversationMultiSelectorOption-${alertConvo.title}`));
|
||||
expect(onConversationSelectionChange).toHaveBeenCalledWith([alertConvo, welcomeConvo]);
|
||||
});
|
||||
|
||||
it('Selects existing conversation from the search input', () => {
|
||||
const { getByTestId } = render(<ConversationMultiSelector {...testProps} />);
|
||||
fireEvent.change(getByTestId('comboBoxSearchInput'), {
|
||||
target: { value: alertConvo.id },
|
||||
target: { value: alertConvo.title },
|
||||
});
|
||||
fireEvent.keyDown(getByTestId('comboBoxSearchInput'), {
|
||||
key: 'Enter',
|
||||
|
|
|
@ -33,15 +33,15 @@ export const ConversationMultiSelector: React.FC<Props> = React.memo(
|
|||
const options = useMemo<EuiComboBoxOptionOption[]>(
|
||||
() =>
|
||||
conversations.map((conversation) => ({
|
||||
label: conversation.id,
|
||||
'data-test-subj': TEST_IDS.CONVERSATIONS_MULTISELECTOR_OPTION(conversation.id),
|
||||
label: conversation.title ?? '',
|
||||
'data-test-subj': TEST_IDS.CONVERSATIONS_MULTISELECTOR_OPTION(conversation.title),
|
||||
})),
|
||||
[conversations]
|
||||
);
|
||||
const selectedOptions = useMemo<EuiComboBoxOptionOption[]>(() => {
|
||||
return selectedConversations != null
|
||||
? selectedConversations.map((conversation) => ({
|
||||
label: conversation.id,
|
||||
label: conversation.title,
|
||||
}))
|
||||
: [];
|
||||
}, [selectedConversations]);
|
||||
|
@ -49,7 +49,7 @@ export const ConversationMultiSelector: React.FC<Props> = React.memo(
|
|||
const handleSelectionChange = useCallback(
|
||||
(conversationMultiSelectorOption: EuiComboBoxOptionOption[]) => {
|
||||
const newConversationSelection = conversations.filter((conversation) =>
|
||||
conversationMultiSelectorOption.some((cmso) => conversation.id === cmso.label)
|
||||
conversationMultiSelectorOption.some((cmso) => conversation.title === cmso.label)
|
||||
);
|
||||
onConversationSelectionChange(newConversationSelection);
|
||||
},
|
||||
|
|
|
@ -17,22 +17,24 @@ const onSelectedSystemPromptChange = jest.fn();
|
|||
const setUpdatedSystemPromptSettings = jest.fn().mockImplementation((fn) => {
|
||||
return fn(mockSystemPrompts);
|
||||
});
|
||||
const setUpdatedConversationSettings = jest.fn().mockImplementation((fn) => {
|
||||
const setConversationSettings = jest.fn().mockImplementation((fn) => {
|
||||
return fn({
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
[alertConvo.title]: alertConvo,
|
||||
});
|
||||
});
|
||||
|
||||
const testProps = {
|
||||
conversationSettings: {
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
},
|
||||
onSelectedSystemPromptChange,
|
||||
selectedSystemPrompt: mockSystemPrompts[0],
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedConversationSettings,
|
||||
setConversationSettings,
|
||||
systemPromptSettings: mockSystemPrompts,
|
||||
conversationsSettingsBulkActions: {},
|
||||
setConversationsSettingsBulkActions: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('./system_prompt_selector/system_prompt_selector', () => ({
|
||||
|
@ -126,15 +128,15 @@ describe('SystemPromptSettings', () => {
|
|||
);
|
||||
fireEvent.click(getByTestId('change-multi'));
|
||||
|
||||
expect(setUpdatedConversationSettings).toHaveReturnedWith({
|
||||
[welcomeConvo.id]: {
|
||||
expect(setConversationSettings).toHaveReturnedWith({
|
||||
[welcomeConvo.title]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
defaultSystemPromptId: 'mock-system-prompt-1',
|
||||
},
|
||||
},
|
||||
[alertConvo.id]: {
|
||||
[alertConvo.title]: {
|
||||
...alertConvo,
|
||||
apiConfig: {
|
||||
...alertConvo.apiConfig,
|
||||
|
|
|
@ -22,22 +22,27 @@ import {
|
|||
import { keyBy } from 'lodash/fp';
|
||||
|
||||
import { css } from '@emotion/react';
|
||||
import { ApiConfig } from '@kbn/elastic-assistant-common';
|
||||
import { AIConnector } from '../../../../connectorland/connector_selector';
|
||||
import { Conversation, Prompt } from '../../../../..';
|
||||
import * as i18n from './translations';
|
||||
import { UseAssistantContext } from '../../../../assistant_context';
|
||||
import { ConversationMultiSelector } from './conversation_multi_selector/conversation_multi_selector';
|
||||
import { SystemPromptSelector } from './system_prompt_selector/system_prompt_selector';
|
||||
import { TEST_IDS } from '../../../constants';
|
||||
import { ConversationsBulkActions } from '../../../api';
|
||||
|
||||
interface Props {
|
||||
conversationSettings: UseAssistantContext['conversations'];
|
||||
conversationSettings: Record<string, Conversation>;
|
||||
conversationsSettingsBulkActions: ConversationsBulkActions;
|
||||
onSelectedSystemPromptChange: (systemPrompt?: Prompt) => void;
|
||||
selectedSystemPrompt: Prompt | undefined;
|
||||
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
|
||||
setUpdatedConversationSettings: React.Dispatch<
|
||||
React.SetStateAction<UseAssistantContext['conversations']>
|
||||
>;
|
||||
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
systemPromptSettings: Prompt[];
|
||||
setConversationsSettingsBulkActions: React.Dispatch<
|
||||
React.SetStateAction<ConversationsBulkActions>
|
||||
>;
|
||||
defaultConnector?: AIConnector;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,8 +54,11 @@ export const SystemPromptSettings: React.FC<Props> = React.memo(
|
|||
onSelectedSystemPromptChange,
|
||||
selectedSystemPrompt,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedConversationSettings,
|
||||
setConversationSettings,
|
||||
systemPromptSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
setConversationsSettingsBulkActions,
|
||||
defaultConnector,
|
||||
}) => {
|
||||
// Prompt
|
||||
const promptContent = useMemo(
|
||||
|
@ -92,19 +100,31 @@ export const SystemPromptSettings: React.FC<Props> = React.memo(
|
|||
return selectedSystemPrompt != null
|
||||
? Object.values(conversationSettings).filter(
|
||||
(conversation) =>
|
||||
conversation.apiConfig.defaultSystemPromptId === selectedSystemPrompt.id
|
||||
conversation.apiConfig?.defaultSystemPromptId === selectedSystemPrompt.id
|
||||
)
|
||||
: [];
|
||||
}, [conversationSettings, selectedSystemPrompt]);
|
||||
|
||||
const handleConversationSelectionChange = useCallback(
|
||||
(currentPromptConversations: Conversation[]) => {
|
||||
const currentPromptConversationIds = currentPromptConversations.map((convo) => convo.id);
|
||||
const currentPromptConversationTitles = currentPromptConversations.map(
|
||||
(convo) => convo.title
|
||||
);
|
||||
const getDefaultSystemPromptId = (convo: Conversation) =>
|
||||
currentPromptConversationTitles.includes(convo.title)
|
||||
? selectedSystemPrompt?.id
|
||||
: convo.apiConfig && convo.apiConfig.defaultSystemPromptId === selectedSystemPrompt?.id
|
||||
? // remove the default System Prompt if it is assigned to a conversation
|
||||
// but that conversation is not in the currentPromptConversationList
|
||||
// This means conversation was removed in the current transaction
|
||||
undefined
|
||||
: // leave it as it is .. if that conversation was neither added nor removed.
|
||||
convo.apiConfig?.defaultSystemPromptId;
|
||||
|
||||
if (selectedSystemPrompt != null) {
|
||||
setUpdatedConversationSettings((prev) =>
|
||||
setConversationSettings((prev) =>
|
||||
keyBy(
|
||||
'id',
|
||||
'title',
|
||||
/*
|
||||
* updatedConversationWithPrompts calculates the present of prompt for
|
||||
* each conversation. Based on the values of selected conversation, it goes
|
||||
|
@ -113,24 +133,90 @@ export const SystemPromptSettings: React.FC<Props> = React.memo(
|
|||
* */
|
||||
Object.values(prev).map((convo) => ({
|
||||
...convo,
|
||||
apiConfig: {
|
||||
...convo.apiConfig,
|
||||
defaultSystemPromptId: currentPromptConversationIds.includes(convo.id)
|
||||
? selectedSystemPrompt?.id
|
||||
: convo.apiConfig.defaultSystemPromptId === selectedSystemPrompt?.id
|
||||
? // remove the default System Prompt if it is assigned to a conversation
|
||||
// but that conversation is not in the currentPromptConversationList
|
||||
// This means conversation was removed in the current transaction
|
||||
undefined
|
||||
: // leave it as it is .. if that conversation was neither added nor removed.
|
||||
convo.apiConfig.defaultSystemPromptId,
|
||||
},
|
||||
...(convo.apiConfig
|
||||
? {
|
||||
apiConfig: {
|
||||
...convo.apiConfig,
|
||||
defaultSystemPromptId: getDefaultSystemPromptId(convo),
|
||||
},
|
||||
}
|
||||
: {
|
||||
apiConfig: {
|
||||
defaultSystemPromptId: getDefaultSystemPromptId(convo),
|
||||
connectorId: defaultConnector?.id ?? '',
|
||||
connectorTypeTitle: defaultConnector?.connectorTypeTitle ?? '',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
let updatedConversationsSettingsBulkActions = { ...conversationsSettingsBulkActions };
|
||||
Object.values(conversationSettings).forEach((convo) => {
|
||||
const getApiConfig = (): ApiConfig | {} => {
|
||||
if (convo.apiConfig) {
|
||||
return {
|
||||
apiConfig: {
|
||||
...convo.apiConfig,
|
||||
defaultSystemPromptId: getDefaultSystemPromptId(convo),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const createOperation =
|
||||
convo.id === ''
|
||||
? {
|
||||
create: {
|
||||
...(updatedConversationsSettingsBulkActions.create ?? {}),
|
||||
[convo.id]: {
|
||||
...convo,
|
||||
...(convo.apiConfig
|
||||
? {
|
||||
apiConfig: {
|
||||
...convo.apiConfig,
|
||||
defaultSystemPromptId: getDefaultSystemPromptId(convo),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
const updateOperation =
|
||||
convo.id !== ''
|
||||
? {
|
||||
update: {
|
||||
...(updatedConversationsSettingsBulkActions.update ?? {}),
|
||||
[convo.id]: {
|
||||
...(updatedConversationsSettingsBulkActions.update
|
||||
? updatedConversationsSettingsBulkActions.update[convo.id] ?? {}
|
||||
: {}),
|
||||
...getApiConfig(),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
updatedConversationsSettingsBulkActions = {
|
||||
...updatedConversationsSettingsBulkActions,
|
||||
...createOperation,
|
||||
...updateOperation,
|
||||
};
|
||||
});
|
||||
setConversationsSettingsBulkActions(updatedConversationsSettingsBulkActions);
|
||||
}
|
||||
},
|
||||
[selectedSystemPrompt, setUpdatedConversationSettings]
|
||||
[
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
defaultConnector?.connectorTypeTitle,
|
||||
defaultConnector?.id,
|
||||
selectedSystemPrompt,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
]
|
||||
);
|
||||
|
||||
// Whether this system prompt should be the default for new conversations
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { alertConvo, customConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, act } from '@testing-library/react';
|
||||
import {
|
||||
AssistantSettings,
|
||||
ANONYMIZATION_TAB,
|
||||
|
@ -22,8 +22,8 @@ import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/c
|
|||
import { MOCK_QUICK_PROMPTS } from '../../mock/quick_prompt';
|
||||
|
||||
const mockConversations = {
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[alertConvo.title]: alertConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
};
|
||||
const saveSettings = jest.fn();
|
||||
|
||||
|
@ -41,8 +41,8 @@ const mockContext = {
|
|||
selectedSettingsTab: 'CONVERSATIONS_TAB',
|
||||
};
|
||||
const onClose = jest.fn();
|
||||
const onSave = jest.fn();
|
||||
const setSelectedConversationId = jest.fn();
|
||||
const onSave = jest.fn().mockResolvedValue(() => {});
|
||||
const onConversationSelected = jest.fn();
|
||||
|
||||
const testProps = {
|
||||
defaultConnectorId: '123',
|
||||
|
@ -50,7 +50,8 @@ const testProps = {
|
|||
selectedConversation: welcomeConvo,
|
||||
onClose,
|
||||
onSave,
|
||||
setSelectedConversationId,
|
||||
onConversationSelected,
|
||||
conversations: {},
|
||||
};
|
||||
jest.mock('../../assistant_context');
|
||||
|
||||
|
@ -79,20 +80,25 @@ describe('AssistantSettings', () => {
|
|||
(useAssistantContext as jest.Mock).mockImplementation(() => mockContext);
|
||||
});
|
||||
|
||||
it('saves changes', () => {
|
||||
it('saves changes', async () => {
|
||||
const { getByTestId } = render(<AssistantSettings {...testProps} />);
|
||||
fireEvent.click(getByTestId('save-button'));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(getByTestId('save-button'));
|
||||
});
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(saveSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('saves changes and updates selected conversation when selected conversation has been deleted', () => {
|
||||
it('saves changes and updates selected conversation when selected conversation has been deleted', async () => {
|
||||
const { getByTestId } = render(
|
||||
<AssistantSettings {...testProps} selectedConversation={customConvo} />
|
||||
);
|
||||
fireEvent.click(getByTestId('save-button'));
|
||||
await act(async () => {
|
||||
fireEvent.click(getByTestId('save-button'));
|
||||
});
|
||||
expect(onSave).toHaveBeenCalled();
|
||||
expect(setSelectedConversationId).toHaveBeenCalled();
|
||||
expect(onConversationSelected).toHaveBeenCalled();
|
||||
expect(saveSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
// eslint-disable-next-line @kbn/eslint/module_migration
|
||||
import styled from 'styled-components';
|
||||
import { css } from '@emotion/react';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { Conversation, Prompt, QuickPrompt } from '../../..';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
|
@ -58,14 +58,14 @@ export type SettingsTabs =
|
|||
| typeof KNOWLEDGE_BASE_TAB
|
||||
| typeof EVALUATION_TAB;
|
||||
interface Props {
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
defaultConnector?: AIConnector;
|
||||
onClose: (
|
||||
event?: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLButtonElement>
|
||||
) => void;
|
||||
onSave: () => void;
|
||||
onSave: (success: boolean) => Promise<void>;
|
||||
selectedConversation: Conversation;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
|
||||
conversations: Record<string, Conversation>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,37 +74,44 @@ interface Props {
|
|||
*/
|
||||
export const AssistantSettings: React.FC<Props> = React.memo(
|
||||
({
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
defaultConnector,
|
||||
onClose,
|
||||
onSave,
|
||||
selectedConversation: defaultSelectedConversation,
|
||||
setSelectedConversationId,
|
||||
onConversationSelected,
|
||||
conversations,
|
||||
}) => {
|
||||
const { modelEvaluatorEnabled, http, selectedSettingsTab, setSelectedSettingsTab } =
|
||||
useAssistantContext();
|
||||
const {
|
||||
actionTypeRegistry,
|
||||
modelEvaluatorEnabled,
|
||||
http,
|
||||
selectedSettingsTab,
|
||||
setSelectedSettingsTab,
|
||||
} = useAssistantContext();
|
||||
|
||||
const {
|
||||
conversationSettings,
|
||||
setConversationSettings,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
knowledgeBase,
|
||||
quickPromptSettings,
|
||||
systemPromptSettings,
|
||||
setUpdatedConversationSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
saveSettings,
|
||||
} = useSettingsUpdater();
|
||||
conversationsSettingsBulkActions,
|
||||
setConversationsSettingsBulkActions,
|
||||
} = useSettingsUpdater(conversations);
|
||||
|
||||
// Local state for saving previously selected items so tab switching is friendlier
|
||||
// Conversation Selection State
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation | undefined>(
|
||||
() => {
|
||||
return conversationSettings[defaultSelectedConversation.id];
|
||||
return conversationSettings[defaultSelectedConversation.title];
|
||||
}
|
||||
);
|
||||
const onHandleSelectedConversationChange = useCallback((conversation?: Conversation) => {
|
||||
|
@ -112,7 +119,7 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
}, []);
|
||||
useEffect(() => {
|
||||
if (selectedConversation != null) {
|
||||
setSelectedConversation(conversationSettings[selectedConversation.id]);
|
||||
setSelectedConversation(conversationSettings[selectedConversation.title]);
|
||||
}
|
||||
}, [conversationSettings, selectedConversation]);
|
||||
|
||||
|
@ -140,22 +147,26 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
}
|
||||
}, [selectedSystemPrompt, systemPromptSettings]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const handleSave = useCallback(async () => {
|
||||
// If the selected conversation is deleted, we need to select a new conversation to prevent a crash creating a conversation that already exists
|
||||
const isSelectedConversationDeleted =
|
||||
conversationSettings[defaultSelectedConversation.id] == null;
|
||||
const newSelectedConversationId: string | undefined = Object.keys(conversationSettings)[0];
|
||||
if (isSelectedConversationDeleted && newSelectedConversationId != null) {
|
||||
setSelectedConversationId(conversationSettings[newSelectedConversationId].id);
|
||||
conversationSettings[defaultSelectedConversation.title] == null;
|
||||
const newSelectedConversation: Conversation | undefined =
|
||||
Object.values(conversationSettings)[0];
|
||||
if (isSelectedConversationDeleted && newSelectedConversation != null) {
|
||||
onConversationSelected({
|
||||
cId: newSelectedConversation.id,
|
||||
cTitle: newSelectedConversation.title,
|
||||
});
|
||||
}
|
||||
saveSettings();
|
||||
onSave();
|
||||
const saveResult = await saveSettings();
|
||||
await onSave(saveResult);
|
||||
}, [
|
||||
conversationSettings,
|
||||
defaultSelectedConversation.id,
|
||||
defaultSelectedConversation.title,
|
||||
onConversationSelected,
|
||||
onSave,
|
||||
saveSettings,
|
||||
setSelectedConversationId,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -279,10 +290,12 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
>
|
||||
{selectedSettingsTab === CONVERSATIONS_TAB && (
|
||||
<ConversationSettings
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
defaultConnector={defaultConnector}
|
||||
conversationSettings={conversationSettings}
|
||||
setUpdatedConversationSettings={setUpdatedConversationSettings}
|
||||
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
|
||||
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
|
||||
setConversationSettings={setConversationSettings}
|
||||
allSystemPrompts={systemPromptSettings}
|
||||
selectedConversation={selectedConversation}
|
||||
isDisabled={selectedConversation == null}
|
||||
|
@ -301,10 +314,13 @@ export const AssistantSettings: React.FC<Props> = React.memo(
|
|||
{selectedSettingsTab === SYSTEM_PROMPTS_TAB && (
|
||||
<SystemPromptSettings
|
||||
conversationSettings={conversationSettings}
|
||||
defaultConnector={defaultConnector}
|
||||
systemPromptSettings={systemPromptSettings}
|
||||
onSelectedSystemPromptChange={onHandleSelectedSystemPromptChange}
|
||||
selectedSystemPrompt={selectedSystemPrompt}
|
||||
setUpdatedConversationSettings={setUpdatedConversationSettings}
|
||||
setConversationSettings={setConversationSettings}
|
||||
setConversationsSettingsBulkActions={setConversationsSettingsBulkActions}
|
||||
conversationsSettingsBulkActions={conversationsSettingsBulkActions}
|
||||
setUpdatedSystemPromptSettings={setUpdatedSystemPromptSettings}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -14,14 +14,17 @@ import { welcomeConvo } from '../../mock/conversation';
|
|||
import { CONVERSATIONS_TAB } from './assistant_settings';
|
||||
|
||||
const setIsSettingsModalVisible = jest.fn();
|
||||
const setSelectedConversationId = jest.fn();
|
||||
const onConversationSelected = jest.fn();
|
||||
|
||||
const testProps = {
|
||||
defaultConnectorId: '123',
|
||||
defaultProvider: OpenAiProviderType.OpenAi,
|
||||
isSettingsModalVisible: false,
|
||||
selectedConversation: welcomeConvo,
|
||||
setIsSettingsModalVisible,
|
||||
setSelectedConversationId,
|
||||
onConversationSelected,
|
||||
conversations: {},
|
||||
refetchConversationsState: jest.fn(),
|
||||
};
|
||||
const setSelectedSettingsTab = jest.fn();
|
||||
const mockUseAssistantContext = {
|
||||
|
|
|
@ -7,21 +7,22 @@
|
|||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
|
||||
import { AIConnector } from '../../connectorland/connector_selector';
|
||||
import { Conversation } from '../../..';
|
||||
import { AssistantSettings, CONVERSATIONS_TAB } from './assistant_settings';
|
||||
import * as i18n from './translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
|
||||
interface Props {
|
||||
defaultConnectorId?: string;
|
||||
defaultProvider?: OpenAiProviderType;
|
||||
defaultConnector?: AIConnector;
|
||||
isSettingsModalVisible: boolean;
|
||||
selectedConversation: Conversation;
|
||||
setIsSettingsModalVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setSelectedConversationId: React.Dispatch<React.SetStateAction<string>>;
|
||||
onConversationSelected: ({ cId, cTitle }: { cId: string; cTitle: string }) => void;
|
||||
isDisabled?: boolean;
|
||||
conversations: Record<string, Conversation>;
|
||||
refetchConversationsState: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,13 +30,14 @@ interface Props {
|
|||
*/
|
||||
export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
||||
({
|
||||
defaultConnectorId,
|
||||
defaultProvider,
|
||||
defaultConnector,
|
||||
isDisabled = false,
|
||||
isSettingsModalVisible,
|
||||
setIsSettingsModalVisible,
|
||||
selectedConversation,
|
||||
setSelectedConversationId,
|
||||
onConversationSelected,
|
||||
conversations,
|
||||
refetchConversationsState,
|
||||
}) => {
|
||||
const { toasts, setSelectedSettingsTab } = useAssistantContext();
|
||||
|
||||
|
@ -48,13 +50,19 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
|||
cleanupAndCloseModal();
|
||||
}, [cleanupAndCloseModal]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
cleanupAndCloseModal();
|
||||
toasts?.addSuccess({
|
||||
iconType: 'check',
|
||||
title: i18n.SETTINGS_UPDATED_TOAST_TITLE,
|
||||
});
|
||||
}, [cleanupAndCloseModal, toasts]);
|
||||
const handleSave = useCallback(
|
||||
async (success: boolean) => {
|
||||
cleanupAndCloseModal();
|
||||
await refetchConversationsState();
|
||||
if (success) {
|
||||
toasts?.addSuccess({
|
||||
iconType: 'check',
|
||||
title: i18n.SETTINGS_UPDATED_TOAST_TITLE,
|
||||
});
|
||||
}
|
||||
},
|
||||
[cleanupAndCloseModal, refetchConversationsState, toasts]
|
||||
);
|
||||
|
||||
const handleShowConversationSettings = useCallback(() => {
|
||||
setSelectedSettingsTab(CONVERSATIONS_TAB);
|
||||
|
@ -76,12 +84,12 @@ export const AssistantSettingsButton: React.FC<Props> = React.memo(
|
|||
|
||||
{isSettingsModalVisible && (
|
||||
<AssistantSettings
|
||||
defaultConnectorId={defaultConnectorId}
|
||||
defaultProvider={defaultProvider}
|
||||
defaultConnector={defaultConnector}
|
||||
selectedConversation={selectedConversation}
|
||||
setSelectedConversationId={setSelectedConversationId}
|
||||
onConversationSelected={onConversationSelected}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSave}
|
||||
conversations={conversations}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -53,7 +53,7 @@ interface Props {
|
|||
*/
|
||||
export const EvaluationSettings: React.FC<Props> = React.memo(({ onEvaluationSettingsChange }) => {
|
||||
const { actionTypeRegistry, basePath, http } = useAssistantContext();
|
||||
const { data: connectors } = useLoadConnectors({ http });
|
||||
const { data: connectors } = useLoadConnectors({ actionTypeRegistry, http });
|
||||
const {
|
||||
data: evalResponse,
|
||||
mutate: performEvaluation,
|
||||
|
|
|
@ -15,12 +15,17 @@ import {
|
|||
mockSuperheroSystemPrompt,
|
||||
mockSystemPrompt,
|
||||
} from '../../../mock/system_prompt';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
|
||||
const mockConversations = {
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[alertConvo.title]: alertConvo,
|
||||
[welcomeConvo.title]: welcomeConvo,
|
||||
};
|
||||
|
||||
const mockHttp = {
|
||||
fetch: jest.fn(),
|
||||
} as unknown as HttpSetup;
|
||||
|
||||
const mockSystemPrompts: Prompt[] = [mockSystemPrompt];
|
||||
const mockQuickPrompts: Prompt[] = [defaultSystemPrompt];
|
||||
|
||||
|
@ -29,14 +34,12 @@ const initialDefaultAllowReplacement = ['replacement1'];
|
|||
|
||||
const setAllQuickPromptsMock = jest.fn();
|
||||
const setAllSystemPromptsMock = jest.fn();
|
||||
const setConversationsMock = jest.fn();
|
||||
const setDefaultAllowMock = jest.fn();
|
||||
const setDefaultAllowReplacementMock = jest.fn();
|
||||
const setKnowledgeBaseMock = jest.fn();
|
||||
const reportAssistantSettingToggled = jest.fn();
|
||||
const mockValues = {
|
||||
assistantTelemetry: { reportAssistantSettingToggled },
|
||||
conversations: mockConversations,
|
||||
allSystemPrompts: mockSystemPrompts,
|
||||
allQuickPrompts: mockQuickPrompts,
|
||||
defaultAllow: initialDefaultAllow,
|
||||
|
@ -46,16 +49,17 @@ const mockValues = {
|
|||
isEnabledKnowledgeBase: true,
|
||||
latestAlerts: DEFAULT_LATEST_ALERTS,
|
||||
},
|
||||
baseConversations: {},
|
||||
setAllQuickPrompts: setAllQuickPromptsMock,
|
||||
setConversations: setConversationsMock,
|
||||
setAllSystemPrompts: setAllSystemPromptsMock,
|
||||
setDefaultAllow: setDefaultAllowMock,
|
||||
setDefaultAllowReplacement: setDefaultAllowReplacementMock,
|
||||
setKnowledgeBase: setKnowledgeBaseMock,
|
||||
http: mockHttp,
|
||||
};
|
||||
|
||||
const updatedValues = {
|
||||
conversations: { [customConvo.id]: customConvo },
|
||||
conversations: { [customConvo.title]: customConvo },
|
||||
allSystemPrompts: [mockSuperheroSystemPrompt],
|
||||
allQuickPrompts: [{ title: 'Prompt 2', prompt: 'Prompt 2', color: 'red' }],
|
||||
defaultAllow: ['allow2'],
|
||||
|
@ -81,10 +85,11 @@ describe('useSettingsUpdater', () => {
|
|||
});
|
||||
it('should set all state variables to their initial values when resetSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater());
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
await waitForNextUpdate();
|
||||
const {
|
||||
setUpdatedConversationSettings,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
|
@ -93,7 +98,8 @@ describe('useSettingsUpdater', () => {
|
|||
resetSettings,
|
||||
} = result.current;
|
||||
|
||||
setUpdatedConversationSettings(updatedValues.conversations);
|
||||
setConversationSettings(updatedValues.conversations);
|
||||
setConversationsSettingsBulkActions({});
|
||||
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
|
||||
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
|
||||
setUpdatedDefaultAllow(updatedValues.defaultAllow);
|
||||
|
@ -109,7 +115,7 @@ describe('useSettingsUpdater', () => {
|
|||
|
||||
resetSettings();
|
||||
|
||||
expect(result.current.conversationSettings).toEqual(mockValues.conversations);
|
||||
expect(result.current.conversationSettings).toEqual(mockConversations);
|
||||
expect(result.current.quickPromptSettings).toEqual(mockValues.allQuickPrompts);
|
||||
expect(result.current.systemPromptSettings).toEqual(mockValues.allSystemPrompts);
|
||||
expect(result.current.defaultAllow).toEqual(mockValues.defaultAllow);
|
||||
|
@ -120,10 +126,11 @@ describe('useSettingsUpdater', () => {
|
|||
|
||||
it('should update all state variables to their updated values when saveSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater());
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
await waitForNextUpdate();
|
||||
const {
|
||||
setUpdatedConversationSettings,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
|
@ -131,18 +138,26 @@ describe('useSettingsUpdater', () => {
|
|||
setUpdatedKnowledgeBaseSettings,
|
||||
} = result.current;
|
||||
|
||||
setUpdatedConversationSettings(updatedValues.conversations);
|
||||
setConversationSettings(updatedValues.conversations);
|
||||
setConversationsSettingsBulkActions({ delete: { ids: ['1'] } });
|
||||
setUpdatedQuickPromptSettings(updatedValues.allQuickPrompts);
|
||||
setUpdatedSystemPromptSettings(updatedValues.allSystemPrompts);
|
||||
setUpdatedDefaultAllow(updatedValues.defaultAllow);
|
||||
setUpdatedDefaultAllowReplacement(updatedValues.defaultAllowReplacement);
|
||||
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
|
||||
|
||||
result.current.saveSettings();
|
||||
await result.current.saveSettings();
|
||||
|
||||
expect(mockHttp.fetch).toHaveBeenCalledWith(
|
||||
'/api/elastic_assistant/current_user/conversations/_bulk_action',
|
||||
{
|
||||
method: 'POST',
|
||||
version: '2023-10-31',
|
||||
body: '{"delete":{"ids":["1"]}}',
|
||||
}
|
||||
);
|
||||
expect(setAllQuickPromptsMock).toHaveBeenCalledWith(updatedValues.allQuickPrompts);
|
||||
expect(setAllSystemPromptsMock).toHaveBeenCalledWith(updatedValues.allSystemPrompts);
|
||||
expect(setConversationsMock).toHaveBeenCalledWith(updatedValues.conversations);
|
||||
expect(setDefaultAllowMock).toHaveBeenCalledWith(updatedValues.defaultAllow);
|
||||
expect(setDefaultAllowReplacementMock).toHaveBeenCalledWith(
|
||||
updatedValues.defaultAllowReplacement
|
||||
|
@ -152,13 +167,13 @@ describe('useSettingsUpdater', () => {
|
|||
});
|
||||
it('should track which toggles have been updated when saveSettings is called', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater());
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
await waitForNextUpdate();
|
||||
const { setUpdatedKnowledgeBaseSettings } = result.current;
|
||||
|
||||
setUpdatedKnowledgeBaseSettings(updatedValues.knowledgeBase);
|
||||
|
||||
result.current.saveSettings();
|
||||
await result.current.saveSettings();
|
||||
expect(reportAssistantSettingToggled).toHaveBeenCalledWith({
|
||||
isEnabledKnowledgeBase: false,
|
||||
isEnabledRAGAlerts: false,
|
||||
|
@ -167,7 +182,7 @@ describe('useSettingsUpdater', () => {
|
|||
});
|
||||
it('should track only toggles that updated', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater());
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
await waitForNextUpdate();
|
||||
const { setUpdatedKnowledgeBaseSettings } = result.current;
|
||||
|
||||
|
@ -175,7 +190,7 @@ describe('useSettingsUpdater', () => {
|
|||
...updatedValues.knowledgeBase,
|
||||
isEnabledKnowledgeBase: true,
|
||||
});
|
||||
result.current.saveSettings();
|
||||
await result.current.saveSettings();
|
||||
expect(reportAssistantSettingToggled).toHaveBeenCalledWith({
|
||||
isEnabledRAGAlerts: false,
|
||||
});
|
||||
|
@ -183,12 +198,12 @@ describe('useSettingsUpdater', () => {
|
|||
});
|
||||
it('if no toggles update, do not track anything', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater());
|
||||
const { result, waitForNextUpdate } = renderHook(() => useSettingsUpdater(mockConversations));
|
||||
await waitForNextUpdate();
|
||||
const { setUpdatedKnowledgeBaseSettings } = result.current;
|
||||
|
||||
setUpdatedKnowledgeBaseSettings(mockValues.knowledgeBase);
|
||||
result.current.saveSettings();
|
||||
await result.current.saveSettings();
|
||||
expect(reportAssistantSettingToggled).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,12 +6,17 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Prompt, QuickPrompt } from '../../../..';
|
||||
import { UseAssistantContext, useAssistantContext } from '../../../assistant_context';
|
||||
import { Conversation, Prompt, QuickPrompt } from '../../../..';
|
||||
import { useAssistantContext } from '../../../assistant_context';
|
||||
import type { KnowledgeBaseConfig } from '../../types';
|
||||
import {
|
||||
ConversationsBulkActions,
|
||||
bulkChangeConversations,
|
||||
} from '../../api/conversations/use_bulk_actions_conversations';
|
||||
|
||||
interface UseSettingsUpdater {
|
||||
conversationSettings: UseAssistantContext['conversations'];
|
||||
conversationSettings: Record<string, Conversation>;
|
||||
conversationsSettingsBulkActions: ConversationsBulkActions;
|
||||
defaultAllow: string[];
|
||||
defaultAllowReplacement: string[];
|
||||
knowledgeBase: KnowledgeBaseConfig;
|
||||
|
@ -20,39 +25,44 @@ interface UseSettingsUpdater {
|
|||
systemPromptSettings: Prompt[];
|
||||
setUpdatedDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setUpdatedConversationSettings: React.Dispatch<
|
||||
React.SetStateAction<UseAssistantContext['conversations']>
|
||||
setConversationSettings: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setConversationsSettingsBulkActions: React.Dispatch<
|
||||
React.SetStateAction<ConversationsBulkActions>
|
||||
>;
|
||||
setUpdatedKnowledgeBaseSettings: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig>>;
|
||||
setUpdatedQuickPromptSettings: React.Dispatch<React.SetStateAction<QuickPrompt[]>>;
|
||||
setUpdatedSystemPromptSettings: React.Dispatch<React.SetStateAction<Prompt[]>>;
|
||||
saveSettings: () => void;
|
||||
saveSettings: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const useSettingsUpdater = (): UseSettingsUpdater => {
|
||||
export const useSettingsUpdater = (
|
||||
conversations: Record<string, Conversation>
|
||||
): UseSettingsUpdater => {
|
||||
// Initial state from assistant context
|
||||
const {
|
||||
allQuickPrompts,
|
||||
allSystemPrompts,
|
||||
assistantTelemetry,
|
||||
conversations,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
knowledgeBase,
|
||||
setAllQuickPrompts,
|
||||
setAllSystemPrompts,
|
||||
setConversations,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setKnowledgeBase,
|
||||
http,
|
||||
toasts,
|
||||
} = useAssistantContext();
|
||||
|
||||
/**
|
||||
* Pending updating state
|
||||
*/
|
||||
// Conversations
|
||||
const [updatedConversationSettings, setUpdatedConversationSettings] =
|
||||
useState<UseAssistantContext['conversations']>(conversations);
|
||||
const [conversationSettings, setConversationSettings] =
|
||||
useState<Record<string, Conversation>>(conversations);
|
||||
const [conversationsSettingsBulkActions, setConversationsSettingsBulkActions] =
|
||||
useState<ConversationsBulkActions>({});
|
||||
// Quick Prompts
|
||||
const [updatedQuickPromptSettings, setUpdatedQuickPromptSettings] =
|
||||
useState<QuickPrompt[]>(allQuickPrompts);
|
||||
|
@ -71,7 +81,8 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
|
|||
* Reset all pending settings
|
||||
*/
|
||||
const resetSettings = useCallback((): void => {
|
||||
setUpdatedConversationSettings(conversations);
|
||||
setConversationSettings(conversations);
|
||||
setConversationsSettingsBulkActions({});
|
||||
setUpdatedQuickPromptSettings(allQuickPrompts);
|
||||
setUpdatedKnowledgeBaseSettings(knowledgeBase);
|
||||
setUpdatedSystemPromptSettings(allSystemPrompts);
|
||||
|
@ -89,10 +100,18 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
|
|||
/**
|
||||
* Save all pending settings
|
||||
*/
|
||||
const saveSettings = useCallback((): void => {
|
||||
const saveSettings = useCallback(async (): Promise<boolean> => {
|
||||
setAllQuickPrompts(updatedQuickPromptSettings);
|
||||
setAllSystemPrompts(updatedSystemPromptSettings);
|
||||
setConversations(updatedConversationSettings);
|
||||
|
||||
const hasBulkConversations =
|
||||
conversationsSettingsBulkActions.create ||
|
||||
conversationsSettingsBulkActions.update ||
|
||||
conversationsSettingsBulkActions.delete;
|
||||
const bulkResult = hasBulkConversations
|
||||
? await bulkChangeConversations(http, conversationsSettingsBulkActions, toasts)
|
||||
: undefined;
|
||||
|
||||
const didUpdateKnowledgeBase =
|
||||
knowledgeBase.isEnabledKnowledgeBase !== updatedKnowledgeBaseSettings.isEnabledKnowledgeBase;
|
||||
const didUpdateRAGAlerts =
|
||||
|
@ -110,26 +129,30 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
|
|||
setKnowledgeBase(updatedKnowledgeBaseSettings);
|
||||
setDefaultAllow(updatedDefaultAllow);
|
||||
setDefaultAllowReplacement(updatedDefaultAllowReplacement);
|
||||
|
||||
return bulkResult?.success ?? true;
|
||||
}, [
|
||||
assistantTelemetry,
|
||||
knowledgeBase.isEnabledRAGAlerts,
|
||||
knowledgeBase.isEnabledKnowledgeBase,
|
||||
setAllQuickPrompts,
|
||||
setAllSystemPrompts,
|
||||
setConversations,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setKnowledgeBase,
|
||||
updatedConversationSettings,
|
||||
updatedDefaultAllow,
|
||||
updatedDefaultAllowReplacement,
|
||||
updatedKnowledgeBaseSettings,
|
||||
updatedQuickPromptSettings,
|
||||
setAllSystemPrompts,
|
||||
updatedSystemPromptSettings,
|
||||
http,
|
||||
conversationsSettingsBulkActions,
|
||||
toasts,
|
||||
knowledgeBase.isEnabledKnowledgeBase,
|
||||
knowledgeBase.isEnabledRAGAlerts,
|
||||
updatedKnowledgeBaseSettings,
|
||||
setKnowledgeBase,
|
||||
setDefaultAllow,
|
||||
updatedDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
updatedDefaultAllowReplacement,
|
||||
assistantTelemetry,
|
||||
]);
|
||||
|
||||
return {
|
||||
conversationSettings: updatedConversationSettings,
|
||||
conversationSettings,
|
||||
conversationsSettingsBulkActions,
|
||||
defaultAllow: updatedDefaultAllow,
|
||||
defaultAllowReplacement: updatedDefaultAllowReplacement,
|
||||
knowledgeBase: updatedKnowledgeBaseSettings,
|
||||
|
@ -139,9 +162,10 @@ export const useSettingsUpdater = (): UseSettingsUpdater => {
|
|||
saveSettings,
|
||||
setUpdatedDefaultAllow,
|
||||
setUpdatedDefaultAllowReplacement,
|
||||
setUpdatedConversationSettings,
|
||||
setUpdatedKnowledgeBaseSettings,
|
||||
setUpdatedQuickPromptSettings,
|
||||
setUpdatedSystemPromptSettings,
|
||||
setConversationSettings,
|
||||
setConversationsSettingsBulkActions,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -96,7 +96,7 @@ describe('useAssistantOverlay', () => {
|
|||
expect(mockUseAssistantContext.showAssistantOverlay).toHaveBeenCalledWith({
|
||||
showOverlay: true,
|
||||
promptContextId: 'id',
|
||||
conversationId: 'conversation-id',
|
||||
conversationTitle: 'conversation-id',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ interface UseAssistantOverlay {
|
|||
* `useAssistantOverlay` is a hook that registers context with the assistant overlay, and
|
||||
* returns an optional `showAssistantOverlay` function to display the assistant overlay.
|
||||
* As an alterative to using the `showAssistantOverlay` returned from this hook, you may
|
||||
* use the `NewChatById` component and pass it the `promptContextId` returned by this hook.
|
||||
* use the `NewChatByTitle` component and pass it the `promptContextId` returned by this hook.
|
||||
*
|
||||
* USE THIS WHEN: You want to register context in one part of the tree, and then show
|
||||
* a _New chat_ button in another part of the tree without passing around the data, or when
|
||||
|
@ -38,7 +38,7 @@ export const useAssistantOverlay = (
|
|||
/**
|
||||
* optionally automatically add this context to a specific conversation when the assistant is displayed
|
||||
*/
|
||||
conversationId: string | null,
|
||||
conversationTitle: string | null,
|
||||
|
||||
/**
|
||||
* The assistant will display this **short**, static description
|
||||
|
@ -98,11 +98,11 @@ export const useAssistantOverlay = (
|
|||
assistantContextShowOverlay({
|
||||
showOverlay,
|
||||
promptContextId,
|
||||
conversationId: conversationId ?? undefined,
|
||||
conversationTitle: conversationTitle ?? undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
[assistantContextShowOverlay, conversationId, promptContextId]
|
||||
[assistantContextShowOverlay, conversationTitle, promptContextId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -92,10 +92,15 @@ describe('useConversation helpers', () => {
|
|||
);
|
||||
const conversation: Conversation = {
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: '3',
|
||||
},
|
||||
category: 'assistant',
|
||||
id: '1',
|
||||
messages: [],
|
||||
replacements: [],
|
||||
title: '1',
|
||||
};
|
||||
|
||||
test('should return the conversation system prompt if it exists', () => {
|
||||
|
@ -106,9 +111,12 @@ describe('useConversation helpers', () => {
|
|||
|
||||
test('should return the default (starred) isNewConversationDefault system prompt if conversation system prompt does not exist', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
id: '1',
|
||||
messages: [],
|
||||
title: '1',
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
|
@ -120,9 +128,12 @@ describe('useConversation helpers', () => {
|
|||
|
||||
test('should return the default (starred) isNewConversationDefault system prompt if conversation system prompt does not exist within all system prompts', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
id: '4', // this id does not exist within allSystemPrompts
|
||||
messages: [],
|
||||
title: '4',
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
|
@ -134,9 +145,12 @@ describe('useConversation helpers', () => {
|
|||
|
||||
test('should return the first prompt if both conversation system prompt and default new system prompt do not exist', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
id: '1',
|
||||
messages: [],
|
||||
title: '1',
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: allSystemPromptsNoDefault,
|
||||
|
@ -148,9 +162,12 @@ describe('useConversation helpers', () => {
|
|||
|
||||
test('should return undefined if conversation system prompt does not exist and there are no system prompts', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
id: '1',
|
||||
messages: [],
|
||||
title: '1',
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: [],
|
||||
|
@ -162,9 +179,12 @@ describe('useConversation helpers', () => {
|
|||
|
||||
test('should return undefined if conversation system prompt does not exist within all system prompts', () => {
|
||||
const conversationWithoutSystemPrompt: Conversation = {
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
id: '4', // this id does not exist within allSystemPrompts
|
||||
messages: [],
|
||||
title: '1',
|
||||
};
|
||||
const result = getDefaultSystemPrompt({
|
||||
allSystemPrompts: allSystemPromptsNoDefault,
|
||||
|
|
|
@ -8,9 +8,17 @@
|
|||
import { useConversation } from '.';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { alertConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import React from 'react';
|
||||
import { ConversationRole } from '../../assistant_context/types';
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { WELCOME_CONVERSATION } from './sample_conversations';
|
||||
import {
|
||||
deleteConversation,
|
||||
getConversationById as _getConversationById,
|
||||
createConversation as _createConversationApi,
|
||||
} from '../api/conversations';
|
||||
|
||||
jest.mock('../api/conversations');
|
||||
const message = {
|
||||
content: 'You are a robot',
|
||||
role: 'user' as ConversationRole,
|
||||
|
@ -24,106 +32,48 @@ const anotherMessage = {
|
|||
|
||||
const mockConvo = {
|
||||
id: 'new-convo',
|
||||
title: 'new-convo',
|
||||
messages: [message, anotherMessage],
|
||||
apiConfig: { defaultSystemPromptId: 'default-system-prompt' },
|
||||
theme: {
|
||||
title: 'Elastic AI Assistant',
|
||||
titleIcon: 'logoSecurity',
|
||||
assistant: { name: 'Assistant', icon: 'logoSecurity' },
|
||||
system: { icon: 'logoElastic' },
|
||||
user: {},
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
},
|
||||
};
|
||||
|
||||
const getConversationById = _getConversationById as jest.Mock;
|
||||
const createConversation = _createConversationApi as jest.Mock;
|
||||
|
||||
describe('useConversation', () => {
|
||||
let httpMock: ReturnType<typeof httpServiceMock.createSetupContract>;
|
||||
|
||||
beforeEach(() => {
|
||||
httpMock = httpServiceMock.createSetupContract();
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should append a message to an existing conversation when called with valid conversationId and message', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const appendResult = result.current.appendMessage({
|
||||
conversationId: welcomeConvo.id,
|
||||
message,
|
||||
});
|
||||
expect(appendResult).toHaveLength(3);
|
||||
expect(appendResult[2]).toEqual(message);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report telemetry when a message has been sent', async () => {
|
||||
await act(async () => {
|
||||
const reportAssistantMessageSent = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
assistantTelemetry: {
|
||||
reportAssistantInvoked: () => {},
|
||||
reportAssistantQuickPrompt: () => {},
|
||||
reportAssistantSettingToggled: () => {},
|
||||
reportAssistantMessageSent,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
result.current.appendMessage({
|
||||
conversationId: welcomeConvo.id,
|
||||
message,
|
||||
});
|
||||
expect(reportAssistantMessageSent).toHaveBeenCalledWith({
|
||||
conversationId: 'Welcome',
|
||||
isEnabledKnowledgeBase: false,
|
||||
isEnabledRAGAlerts: false,
|
||||
role: 'user',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new conversation when called with valid conversationId and message', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
<TestProviders providerContext={{ http: httpMock }}>{children}</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
createConversation.mockResolvedValue(mockConvo);
|
||||
|
||||
const createResult = result.current.createConversation({
|
||||
conversationId: mockConvo.id,
|
||||
const createResult = await result.current.createConversation({
|
||||
id: mockConvo.id,
|
||||
messages: mockConvo.messages,
|
||||
replacements: [],
|
||||
apiConfig: {
|
||||
connectorId: '123',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
},
|
||||
title: mockConvo.title,
|
||||
category: 'assistant',
|
||||
});
|
||||
|
||||
expect(createResult).toEqual(mockConvo);
|
||||
|
@ -134,243 +84,55 @@ describe('useConversation', () => {
|
|||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[mockConvo.id]: mockConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
<TestProviders providerContext={{ http: httpMock }}>{children}</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const deleteResult = result.current.deleteConversation('new-convo');
|
||||
await result.current.deleteConversation('new-convo');
|
||||
|
||||
expect(deleteResult).toEqual(mockConvo);
|
||||
expect(deleteConversation).toHaveBeenCalledWith({
|
||||
http: httpMock,
|
||||
id: 'new-convo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update the apiConfig for an existing conversation when called with a valid conversationId and apiConfig', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
<TestProviders providerContext={{ http: httpMock }}>{children}</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.setApiConfig({
|
||||
conversationId: welcomeConvo.id,
|
||||
await result.current.setApiConfig({
|
||||
conversation: WELCOME_CONVERSATION,
|
||||
apiConfig: mockConvo.apiConfig,
|
||||
});
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[welcomeConvo.id]: { ...welcomeConvo, apiConfig: mockConvo.apiConfig },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('overwrites a conversation', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.setConversation({
|
||||
conversation: {
|
||||
...mockConvo,
|
||||
id: welcomeConvo.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: { ...mockConvo, id: welcomeConvo.id },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('clears a conversation', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.clearConversation(welcomeConvo.id);
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
apiConfig: {
|
||||
...welcomeConvo.apiConfig,
|
||||
defaultSystemPromptId: 'default-system-prompt',
|
||||
},
|
||||
messages: [],
|
||||
replacements: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('appends replacements', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.appendReplacements({
|
||||
conversationId: welcomeConvo.id,
|
||||
replacements: {
|
||||
'1.0.0.721': '127.0.0.1',
|
||||
'1.0.0.01': '10.0.0.1',
|
||||
'tsoh-tset': 'test-host',
|
||||
},
|
||||
});
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: {
|
||||
...welcomeConvo,
|
||||
replacements: {
|
||||
'1.0.0.721': '127.0.0.1',
|
||||
'1.0.0.01': '10.0.0.1',
|
||||
'tsoh-tset': 'test-host',
|
||||
},
|
||||
},
|
||||
expect(createConversation).toHaveBeenCalledWith({
|
||||
http: httpMock,
|
||||
conversation: { ...WELCOME_CONVERSATION, apiConfig: mockConvo.apiConfig, id: '' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove the last message from a conversation when called with valid conversationId', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[mockConvo.id]: mockConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
<TestProviders providerContext={{ http: httpMock }}>{children}</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
const removeResult = result.current.removeLastMessage('new-convo');
|
||||
getConversationById.mockResolvedValue(mockConvo);
|
||||
|
||||
const removeResult = await result.current.removeLastMessage('new-convo');
|
||||
|
||||
expect(removeResult).toEqual([message]);
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[mockConvo.id]: { ...mockConvo, messages: [message] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('amendMessage updates the last message of conversation[] for a given conversationId with provided content', async () => {
|
||||
await act(async () => {
|
||||
const setConversations = jest.fn();
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConversation(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[mockConvo.id]: mockConvo,
|
||||
}),
|
||||
setConversations,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
|
||||
result.current.amendMessage({
|
||||
conversationId: 'new-convo',
|
||||
content: 'hello world',
|
||||
});
|
||||
|
||||
expect(setConversations).toHaveBeenCalledWith({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
[mockConvo.id]: {
|
||||
...mockConvo,
|
||||
messages: [
|
||||
message,
|
||||
{
|
||||
...anotherMessage,
|
||||
content: 'hello world',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,343 +7,181 @@
|
|||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { ApiConfig } from '@kbn/elastic-assistant-common';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { Conversation, Message } from '../../assistant_context/types';
|
||||
import * as i18n from './translations';
|
||||
import { ELASTIC_AI_ASSISTANT, ELASTIC_AI_ASSISTANT_TITLE } from './translations';
|
||||
import { getDefaultSystemPrompt } from './helpers';
|
||||
import {
|
||||
createConversation as createConversationApi,
|
||||
deleteConversation as deleteConversationApi,
|
||||
getConversationById,
|
||||
updateConversation,
|
||||
} from '../api/conversations';
|
||||
import { WELCOME_CONVERSATION } from './sample_conversations';
|
||||
|
||||
export const DEFAULT_CONVERSATION_STATE: Conversation = {
|
||||
id: i18n.DEFAULT_CONVERSATION_TITLE,
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
theme: {
|
||||
title: ELASTIC_AI_ASSISTANT_TITLE,
|
||||
titleIcon: 'logoSecurity',
|
||||
assistant: {
|
||||
name: ELASTIC_AI_ASSISTANT,
|
||||
icon: 'logoSecurity',
|
||||
},
|
||||
system: {
|
||||
icon: 'logoElastic',
|
||||
},
|
||||
user: {},
|
||||
},
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
title: i18n.DEFAULT_CONVERSATION_TITLE,
|
||||
};
|
||||
|
||||
interface AppendMessageProps {
|
||||
conversationId: string;
|
||||
message: Message;
|
||||
}
|
||||
interface AmendMessageProps {
|
||||
conversationId: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface AppendReplacementsProps {
|
||||
conversationId: string;
|
||||
replacements: Record<string, string>;
|
||||
}
|
||||
|
||||
interface CreateConversationProps {
|
||||
conversationId: string;
|
||||
cTitle: string;
|
||||
messages?: Message[];
|
||||
}
|
||||
|
||||
interface SetApiConfigProps {
|
||||
conversationId: string;
|
||||
apiConfig: Conversation['apiConfig'];
|
||||
}
|
||||
|
||||
interface SetConversationProps {
|
||||
conversation: Conversation;
|
||||
apiConfig: ApiConfig;
|
||||
}
|
||||
|
||||
interface UseConversation {
|
||||
appendMessage: ({ conversationId, message }: AppendMessageProps) => Message[];
|
||||
amendMessage: ({ conversationId, content }: AmendMessageProps) => void;
|
||||
appendReplacements: ({
|
||||
conversationId,
|
||||
replacements,
|
||||
}: AppendReplacementsProps) => Record<string, string>;
|
||||
clearConversation: (conversationId: string) => void;
|
||||
createConversation: ({ conversationId, messages }: CreateConversationProps) => Conversation;
|
||||
clearConversation: (conversationId: string) => Promise<void>;
|
||||
getDefaultConversation: ({ cTitle, messages }: CreateConversationProps) => Conversation;
|
||||
deleteConversation: (conversationId: string) => void;
|
||||
removeLastMessage: (conversationId: string) => Message[];
|
||||
setApiConfig: ({ conversationId, apiConfig }: SetApiConfigProps) => void;
|
||||
setConversation: ({ conversation }: SetConversationProps) => void;
|
||||
removeLastMessage: (conversationId: string) => Promise<Message[] | undefined>;
|
||||
setApiConfig: ({
|
||||
conversation,
|
||||
apiConfig,
|
||||
}: SetApiConfigProps) => Promise<Conversation | undefined>;
|
||||
createConversation: (conversation: Conversation) => Promise<Conversation | undefined>;
|
||||
getConversation: (conversationId: string) => Promise<Conversation | undefined>;
|
||||
}
|
||||
|
||||
export const useConversation = (): UseConversation => {
|
||||
const {
|
||||
allSystemPrompts,
|
||||
assistantTelemetry,
|
||||
knowledgeBase: { isEnabledKnowledgeBase, isEnabledRAGAlerts },
|
||||
setConversations,
|
||||
} = useAssistantContext();
|
||||
const { allSystemPrompts, http, toasts } = useAssistantContext();
|
||||
|
||||
const getConversation = useCallback(
|
||||
async (conversationId: string) => {
|
||||
return getConversationById({ http, id: conversationId, toasts });
|
||||
},
|
||||
[http, toasts]
|
||||
);
|
||||
|
||||
/**
|
||||
* Removes the last message of conversation[] for a given conversationId
|
||||
*/
|
||||
const removeLastMessage = useCallback(
|
||||
(conversationId: string) => {
|
||||
async (conversationId: string) => {
|
||||
let messages: Message[] = [];
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
|
||||
if (prevConversation != null) {
|
||||
messages = prevConversation.messages.slice(0, prevConversation.messages.length - 1);
|
||||
const newConversation = {
|
||||
...prevConversation,
|
||||
messages,
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
[conversationId]: newConversation,
|
||||
};
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
const prevConversation = await getConversationById({ http, id: conversationId, toasts });
|
||||
if (prevConversation != null) {
|
||||
messages = prevConversation.messages.slice(0, prevConversation.messages.length - 1);
|
||||
await updateConversation({
|
||||
http,
|
||||
conversationId,
|
||||
messages,
|
||||
toasts,
|
||||
});
|
||||
}
|
||||
return messages;
|
||||
},
|
||||
[setConversations]
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the last message of conversation[] for a given conversationId with provided content
|
||||
*/
|
||||
const amendMessage = useCallback(
|
||||
({ conversationId, content }: AmendMessageProps) => {
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
|
||||
if (prevConversation != null) {
|
||||
const { messages, ...rest } = prevConversation;
|
||||
const message = messages[messages.length - 1];
|
||||
const updatedMessages = message
|
||||
? [...messages.slice(0, -1), { ...message, content }]
|
||||
: [...messages];
|
||||
const newConversation = {
|
||||
...rest,
|
||||
messages: updatedMessages,
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
[conversationId]: newConversation,
|
||||
};
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
},
|
||||
[setConversations]
|
||||
);
|
||||
|
||||
/**
|
||||
* Append a message to the conversation[] for a given conversationId
|
||||
*/
|
||||
const appendMessage = useCallback(
|
||||
({ conversationId, message }: AppendMessageProps): Message[] => {
|
||||
assistantTelemetry?.reportAssistantMessageSent({
|
||||
conversationId,
|
||||
role: message.role,
|
||||
isEnabledKnowledgeBase,
|
||||
isEnabledRAGAlerts,
|
||||
});
|
||||
let messages: Message[] = [];
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
|
||||
if (prevConversation != null) {
|
||||
messages = [...prevConversation.messages, message];
|
||||
const newConversation = {
|
||||
...prevConversation,
|
||||
messages,
|
||||
};
|
||||
return {
|
||||
...prev,
|
||||
[conversationId]: newConversation,
|
||||
};
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
return messages;
|
||||
},
|
||||
[isEnabledKnowledgeBase, isEnabledRAGAlerts, assistantTelemetry, setConversations]
|
||||
);
|
||||
|
||||
const appendReplacements = useCallback(
|
||||
({ conversationId, replacements }: AppendReplacementsProps): Record<string, string> => {
|
||||
let allReplacements = replacements;
|
||||
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
|
||||
if (prevConversation != null) {
|
||||
allReplacements = {
|
||||
...prevConversation.replacements,
|
||||
...replacements,
|
||||
};
|
||||
|
||||
const newConversation = {
|
||||
...prevConversation,
|
||||
replacements: allReplacements,
|
||||
};
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[conversationId]: newConversation,
|
||||
};
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
|
||||
return allReplacements;
|
||||
},
|
||||
[setConversations]
|
||||
[http, toasts]
|
||||
);
|
||||
|
||||
const clearConversation = useCallback(
|
||||
(conversationId: string) => {
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
async (conversationId: string) => {
|
||||
const conversation = await getConversationById({ http, id: conversationId, toasts });
|
||||
if (conversation && conversation.apiConfig) {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: prevConversation,
|
||||
conversation,
|
||||
})?.id;
|
||||
|
||||
if (prevConversation != null) {
|
||||
const newConversation: Conversation = {
|
||||
...prevConversation,
|
||||
apiConfig: {
|
||||
...prevConversation.apiConfig,
|
||||
defaultSystemPromptId,
|
||||
},
|
||||
messages: [],
|
||||
replacements: undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[conversationId]: newConversation,
|
||||
};
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
await updateConversation({
|
||||
http,
|
||||
toasts,
|
||||
conversationId,
|
||||
apiConfig: { ...conversation.apiConfig, defaultSystemPromptId },
|
||||
messages: [],
|
||||
replacements: [],
|
||||
});
|
||||
}
|
||||
},
|
||||
[allSystemPrompts, setConversations]
|
||||
[allSystemPrompts, http, toasts]
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new conversation with the given conversationId, and optionally add messages
|
||||
*/
|
||||
const createConversation = useCallback(
|
||||
({ conversationId, messages }: CreateConversationProps): Conversation => {
|
||||
const defaultSystemPromptId = getDefaultSystemPrompt({
|
||||
allSystemPrompts,
|
||||
conversation: undefined,
|
||||
})?.id;
|
||||
|
||||
const newConversation: Conversation = {
|
||||
...DEFAULT_CONVERSATION_STATE,
|
||||
apiConfig: {
|
||||
...DEFAULT_CONVERSATION_STATE.apiConfig,
|
||||
defaultSystemPromptId,
|
||||
},
|
||||
id: conversationId,
|
||||
messages: messages != null ? messages : [],
|
||||
};
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
if (prevConversation != null) {
|
||||
throw new Error('Conversation already exists!');
|
||||
} else {
|
||||
return {
|
||||
...prev,
|
||||
[conversationId]: {
|
||||
...newConversation,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
const getDefaultConversation = useCallback(
|
||||
({ cTitle, messages }: CreateConversationProps): Conversation => {
|
||||
const newConversation: Conversation =
|
||||
cTitle === i18n.WELCOME_CONVERSATION_TITLE
|
||||
? WELCOME_CONVERSATION
|
||||
: {
|
||||
...DEFAULT_CONVERSATION_STATE,
|
||||
id: '',
|
||||
title: cTitle,
|
||||
messages: messages != null ? messages : [],
|
||||
};
|
||||
return newConversation;
|
||||
},
|
||||
[allSystemPrompts, setConversations]
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new conversation with the given conversation
|
||||
*/
|
||||
const createConversation = useCallback(
|
||||
async (conversation: Conversation): Promise<Conversation | undefined> => {
|
||||
return createConversationApi({ http, conversation, toasts });
|
||||
},
|
||||
[http, toasts]
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete the conversation with the given conversationId
|
||||
*/
|
||||
const deleteConversation = useCallback(
|
||||
(conversationId: string): Conversation | undefined => {
|
||||
let deletedConversation: Conversation | undefined;
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const { [conversationId]: prevConversation, ...updatedConversations } = prev;
|
||||
deletedConversation = prevConversation;
|
||||
if (prevConversation != null) {
|
||||
return updatedConversations;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
return deletedConversation;
|
||||
async (conversationId: string): Promise<void> => {
|
||||
await deleteConversationApi({ http, id: conversationId, toasts });
|
||||
},
|
||||
[setConversations]
|
||||
[http, toasts]
|
||||
);
|
||||
|
||||
/**
|
||||
* Update the apiConfig for a given conversationId
|
||||
* Create/Update the apiConfig for a given conversationId
|
||||
*/
|
||||
const setApiConfig = useCallback(
|
||||
({ conversationId, apiConfig }: SetApiConfigProps): void => {
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
const prevConversation: Conversation | undefined = prev[conversationId];
|
||||
|
||||
if (prevConversation != null) {
|
||||
const updatedConversation = {
|
||||
...prevConversation,
|
||||
async ({ conversation, apiConfig }: SetApiConfigProps) => {
|
||||
if (conversation.id === '') {
|
||||
return createConversationApi({
|
||||
http,
|
||||
conversation: {
|
||||
apiConfig,
|
||||
};
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[conversationId]: updatedConversation,
|
||||
};
|
||||
} else {
|
||||
return prev;
|
||||
}
|
||||
});
|
||||
category: 'assistant',
|
||||
title: conversation.title,
|
||||
replacements: conversation.replacements,
|
||||
excludeFromLastConversationStorage: conversation.excludeFromLastConversationStorage,
|
||||
isDefault: conversation.isDefault,
|
||||
id: '',
|
||||
messages: conversation.messages ?? [],
|
||||
},
|
||||
toasts,
|
||||
});
|
||||
} else {
|
||||
return updateConversation({
|
||||
http,
|
||||
conversationId: conversation.id,
|
||||
apiConfig,
|
||||
toasts,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setConversations]
|
||||
);
|
||||
|
||||
/**
|
||||
* Set/overwrite an existing conversation (behaves as createConversation if not already existing)
|
||||
*/
|
||||
const setConversation = useCallback(
|
||||
({ conversation }: SetConversationProps): void => {
|
||||
setConversations((prev: Record<string, Conversation>) => {
|
||||
return {
|
||||
...prev,
|
||||
[conversation.id]: conversation,
|
||||
};
|
||||
});
|
||||
},
|
||||
[setConversations]
|
||||
[http, toasts]
|
||||
);
|
||||
|
||||
return {
|
||||
amendMessage,
|
||||
appendMessage,
|
||||
appendReplacements,
|
||||
clearConversation,
|
||||
createConversation,
|
||||
getDefaultConversation,
|
||||
deleteConversation,
|
||||
removeLastMessage,
|
||||
setApiConfig,
|
||||
setConversation,
|
||||
createConversation,
|
||||
getConversation,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -7,26 +7,12 @@
|
|||
|
||||
import { Conversation, Message } from '../../assistant_context/types';
|
||||
import * as i18n from '../../content/prompts/welcome/translations';
|
||||
import {
|
||||
ELASTIC_AI_ASSISTANT,
|
||||
ELASTIC_AI_ASSISTANT_TITLE,
|
||||
WELCOME_CONVERSATION_TITLE,
|
||||
} from './translations';
|
||||
import { WELCOME_CONVERSATION_TITLE } from './translations';
|
||||
|
||||
export const WELCOME_CONVERSATION: Conversation = {
|
||||
id: WELCOME_CONVERSATION_TITLE,
|
||||
theme: {
|
||||
title: ELASTIC_AI_ASSISTANT_TITLE,
|
||||
titleIcon: 'logoSecurity',
|
||||
assistant: {
|
||||
name: ELASTIC_AI_ASSISTANT,
|
||||
icon: 'logoSecurity',
|
||||
},
|
||||
system: {
|
||||
icon: 'logoElastic',
|
||||
},
|
||||
user: {},
|
||||
},
|
||||
id: '',
|
||||
title: WELCOME_CONVERSATION_TITLE,
|
||||
category: 'assistant',
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
|
@ -56,7 +42,7 @@ export const WELCOME_CONVERSATION: Conversation = {
|
|||
},
|
||||
},
|
||||
],
|
||||
apiConfig: {},
|
||||
replacements: [],
|
||||
};
|
||||
|
||||
export const enterpriseMessaging: Message[] = [
|
||||
|
|
|
@ -7,31 +7,30 @@
|
|||
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { ApiConfig, Replacement } from '@kbn/elastic-assistant-common';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { Conversation, Message } from '../../assistant_context/types';
|
||||
import { fetchConnectorExecuteAction, FetchConnectorExecuteResponse } from '../api';
|
||||
|
||||
interface SendMessagesProps {
|
||||
interface SendMessageProps {
|
||||
allow?: string[];
|
||||
allowReplacement?: string[];
|
||||
apiConfig: Conversation['apiConfig'];
|
||||
apiConfig: ApiConfig;
|
||||
http: HttpSetup;
|
||||
messages: Message[];
|
||||
onNewReplacements: (newReplacements: Record<string, string>) => void;
|
||||
replacements?: Record<string, string>;
|
||||
message?: string;
|
||||
conversationId: string;
|
||||
replacements: Replacement[];
|
||||
}
|
||||
|
||||
interface UseSendMessages {
|
||||
interface UseSendMessage {
|
||||
isLoading: boolean;
|
||||
sendMessages: ({
|
||||
sendMessage: ({
|
||||
apiConfig,
|
||||
http,
|
||||
messages,
|
||||
}: SendMessagesProps) => Promise<FetchConnectorExecuteResponse>;
|
||||
message,
|
||||
}: SendMessageProps) => Promise<FetchConnectorExecuteResponse>;
|
||||
}
|
||||
|
||||
export const useSendMessages = (): UseSendMessages => {
|
||||
export const useSendMessage = (): UseSendMessage => {
|
||||
const {
|
||||
alertsIndexPattern,
|
||||
assistantStreamingEnabled,
|
||||
|
@ -41,12 +40,13 @@ export const useSendMessages = (): UseSendMessages => {
|
|||
} = useAssistantContext();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const sendMessages = useCallback(
|
||||
async ({ apiConfig, http, messages, onNewReplacements, replacements }: SendMessagesProps) => {
|
||||
const sendMessage = useCallback(
|
||||
async ({ apiConfig, http, message, conversationId, replacements }: SendMessageProps) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
return await fetchConnectorExecuteAction({
|
||||
conversationId,
|
||||
isEnabledRAGAlerts: knowledgeBase.isEnabledRAGAlerts, // settings toggle
|
||||
alertsIndexPattern,
|
||||
allow: defaultAllow,
|
||||
|
@ -55,10 +55,9 @@ export const useSendMessages = (): UseSendMessages => {
|
|||
isEnabledKnowledgeBase: knowledgeBase.isEnabledKnowledgeBase,
|
||||
assistantStreamingEnabled,
|
||||
http,
|
||||
message,
|
||||
replacements,
|
||||
messages,
|
||||
size: knowledgeBase.latestAlerts,
|
||||
onNewReplacements,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
@ -75,5 +74,5 @@ export const useSendMessages = (): UseSendMessages => {
|
|||
]
|
||||
);
|
||||
|
||||
return { isLoading, sendMessages };
|
||||
return { isLoading, sendMessage };
|
||||
};
|
|
@ -10,7 +10,7 @@ import { KnowledgeBaseConfig } from '../assistant/types';
|
|||
export const DEFAULT_ASSISTANT_NAMESPACE = 'elasticAssistantDefault';
|
||||
export const QUICK_PROMPT_LOCAL_STORAGE_KEY = 'quickPrompts';
|
||||
export const SYSTEM_PROMPT_LOCAL_STORAGE_KEY = 'systemPrompts';
|
||||
export const LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY = 'lastConversationId';
|
||||
export const LAST_CONVERSATION_TITLE_LOCAL_STORAGE_KEY = 'lastConversationTitle';
|
||||
export const KNOWLEDGE_BASE_LOCAL_STORAGE_KEY = 'knowledgeBase';
|
||||
|
||||
/** The default `n` latest alerts, ordered by risk score, sent as context to the assistant */
|
||||
|
|
|
@ -35,22 +35,22 @@ describe('AssistantContext', () => {
|
|||
expect(result.current.http.fetch).toBeCalledWith(path);
|
||||
});
|
||||
|
||||
test('getConversationId defaults to provided id', async () => {
|
||||
test('getLastConversationTitle defaults to provided id', async () => {
|
||||
const { result } = renderHook(useAssistantContext, { wrapper: TestProviders });
|
||||
const id = result.current.getConversationId('123');
|
||||
const id = result.current.getLastConversationTitle('123');
|
||||
expect(id).toEqual('123');
|
||||
});
|
||||
|
||||
test('getConversationId uses local storage id when no id is provided ', async () => {
|
||||
test('getLastConversationTitle uses local storage id when no id is provided ', async () => {
|
||||
const { result } = renderHook(useAssistantContext, { wrapper: TestProviders });
|
||||
const id = result.current.getConversationId();
|
||||
const id = result.current.getLastConversationTitle();
|
||||
expect(id).toEqual('456');
|
||||
});
|
||||
|
||||
test('getConversationId defaults to Welcome when no local storage id and no id is provided ', async () => {
|
||||
test('getLastConversationTitle defaults to Welcome when no local storage id and no id is provided ', async () => {
|
||||
(useLocalStorage as jest.Mock).mockReturnValue([undefined, jest.fn()]);
|
||||
const { result } = renderHook(useAssistantContext, { wrapper: TestProviders });
|
||||
const id = result.current.getConversationId();
|
||||
const id = result.current.getLastConversationTitle();
|
||||
expect(id).toEqual('Welcome');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,13 +8,12 @@
|
|||
import { EuiCommentProps } from '@elastic/eui';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { omit, uniq } from 'lodash/fp';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
import { defaultAssistantFeatures } from '@kbn/elastic-assistant-common';
|
||||
import { WELCOME_CONVERSATION_TITLE } from '../assistant/use_conversation/translations';
|
||||
import { updatePromptContexts } from './helpers';
|
||||
import type {
|
||||
PromptContext,
|
||||
|
@ -32,31 +31,35 @@ import {
|
|||
DEFAULT_ASSISTANT_NAMESPACE,
|
||||
DEFAULT_KNOWLEDGE_BASE_SETTINGS,
|
||||
KNOWLEDGE_BASE_LOCAL_STORAGE_KEY,
|
||||
LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY,
|
||||
LAST_CONVERSATION_TITLE_LOCAL_STORAGE_KEY,
|
||||
QUICK_PROMPT_LOCAL_STORAGE_KEY,
|
||||
SYSTEM_PROMPT_LOCAL_STORAGE_KEY,
|
||||
} from './constants';
|
||||
import { CONVERSATIONS_TAB, SettingsTabs } from '../assistant/settings/assistant_settings';
|
||||
import { AssistantAvailability, AssistantTelemetry } from './types';
|
||||
import { useCapabilities } from '../assistant/api/capabilities/use_capabilities';
|
||||
import { WELCOME_CONVERSATION_TITLE } from '../assistant/use_conversation/translations';
|
||||
|
||||
export interface ShowAssistantOverlayProps {
|
||||
showOverlay: boolean;
|
||||
promptContextId?: string;
|
||||
conversationId?: string;
|
||||
conversationTitle?: string;
|
||||
}
|
||||
|
||||
type ShowAssistantOverlay = ({
|
||||
showOverlay,
|
||||
promptContextId,
|
||||
conversationId,
|
||||
conversationTitle,
|
||||
}: ShowAssistantOverlayProps) => void;
|
||||
export interface AssistantProviderProps {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
alertsIndexPattern?: string;
|
||||
assistantAvailability: AssistantAvailability;
|
||||
assistantTelemetry?: AssistantTelemetry;
|
||||
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
|
||||
augmentMessageCodeBlocks: (
|
||||
currentConversation: Conversation,
|
||||
showAnonymizedValues: boolean
|
||||
) => CodeBlockDetails[][];
|
||||
baseAllow: string[];
|
||||
baseAllowReplacement: string[];
|
||||
defaultAllow: string[];
|
||||
|
@ -68,28 +71,21 @@ export interface AssistantProviderProps {
|
|||
docLinks: Omit<DocLinksStart, 'links'>;
|
||||
children: React.ReactNode;
|
||||
getComments: ({
|
||||
amendMessage,
|
||||
currentConversation,
|
||||
isFetchingResponse,
|
||||
refetchCurrentConversation,
|
||||
regenerateMessage,
|
||||
showAnonymizedValues,
|
||||
}: {
|
||||
amendMessage: ({
|
||||
conversationId,
|
||||
content,
|
||||
}: {
|
||||
conversationId: string;
|
||||
content: string;
|
||||
}) => void;
|
||||
currentConversation: Conversation;
|
||||
isFetchingResponse: boolean;
|
||||
refetchCurrentConversation: () => void;
|
||||
regenerateMessage: (conversationId: string) => void;
|
||||
showAnonymizedValues: boolean;
|
||||
}) => EuiCommentProps[];
|
||||
http: HttpSetup;
|
||||
getInitialConversations: () => Record<string, Conversation>;
|
||||
baseConversations: Record<string, Conversation>;
|
||||
nameSpace?: string;
|
||||
setConversations: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
title?: string;
|
||||
|
@ -102,7 +98,10 @@ export interface UseAssistantContext {
|
|||
assistantAvailability: AssistantAvailability;
|
||||
assistantStreamingEnabled: boolean;
|
||||
assistantTelemetry?: AssistantTelemetry;
|
||||
augmentMessageCodeBlocks: (currentConversation: Conversation) => CodeBlockDetails[][];
|
||||
augmentMessageCodeBlocks: (
|
||||
currentConversation: Conversation,
|
||||
showAnonymizedValues: boolean
|
||||
) => CodeBlockDetails[][];
|
||||
allQuickPrompts: QuickPrompt[];
|
||||
allSystemPrompts: Prompt[];
|
||||
baseAllow: string[];
|
||||
|
@ -114,29 +113,22 @@ export interface UseAssistantContext {
|
|||
basePromptContexts: PromptContextTemplate[];
|
||||
baseQuickPrompts: QuickPrompt[];
|
||||
baseSystemPrompts: Prompt[];
|
||||
conversationIds: string[];
|
||||
conversations: Record<string, Conversation>;
|
||||
baseConversations: Record<string, Conversation>;
|
||||
getComments: ({
|
||||
currentConversation,
|
||||
showAnonymizedValues,
|
||||
amendMessage,
|
||||
refetchCurrentConversation,
|
||||
isFetchingResponse,
|
||||
}: {
|
||||
currentConversation: Conversation;
|
||||
isFetchingResponse: boolean;
|
||||
amendMessage: ({
|
||||
conversationId,
|
||||
content,
|
||||
}: {
|
||||
conversationId: string;
|
||||
content: string;
|
||||
}) => void;
|
||||
refetchCurrentConversation: () => void;
|
||||
regenerateMessage: () => void;
|
||||
showAnonymizedValues: boolean;
|
||||
}) => EuiCommentProps[];
|
||||
http: HttpSetup;
|
||||
knowledgeBase: KnowledgeBaseConfig;
|
||||
getConversationId: (id?: string) => string;
|
||||
getLastConversationTitle: (conversationTitle?: string) => string;
|
||||
promptContexts: Record<string, PromptContext>;
|
||||
modelEvaluatorEnabled: boolean;
|
||||
nameSpace: string;
|
||||
|
@ -144,11 +136,10 @@ export interface UseAssistantContext {
|
|||
selectedSettingsTab: SettingsTabs;
|
||||
setAllQuickPrompts: React.Dispatch<React.SetStateAction<QuickPrompt[] | undefined>>;
|
||||
setAllSystemPrompts: React.Dispatch<React.SetStateAction<Prompt[] | undefined>>;
|
||||
setConversations: React.Dispatch<React.SetStateAction<Record<string, Conversation>>>;
|
||||
setDefaultAllow: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setDefaultAllowReplacement: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
setKnowledgeBase: React.Dispatch<React.SetStateAction<KnowledgeBaseConfig | undefined>>;
|
||||
setLastConversationId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setLastConversationTitle: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
setSelectedSettingsTab: React.Dispatch<React.SetStateAction<SettingsTabs>>;
|
||||
setShowAssistantOverlay: (showAssistantOverlay: ShowAssistantOverlay) => void;
|
||||
showAssistantOverlay: ShowAssistantOverlay;
|
||||
|
@ -177,9 +168,8 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
children,
|
||||
getComments,
|
||||
http,
|
||||
getInitialConversations,
|
||||
baseConversations,
|
||||
nameSpace = DEFAULT_ASSISTANT_NAMESPACE,
|
||||
setConversations,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
title = DEFAULT_ASSISTANT_TITLE,
|
||||
|
@ -201,8 +191,8 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
baseSystemPrompts
|
||||
);
|
||||
|
||||
const [localStorageLastConversationId, setLocalStorageLastConversationId] =
|
||||
useLocalStorage<string>(`${nameSpace}.${LAST_CONVERSATION_ID_LOCAL_STORAGE_KEY}`);
|
||||
const [localStorageLastConversationTitle, setLocalStorageLastConversationTitle] =
|
||||
useLocalStorage<string>(`${nameSpace}.${LAST_CONVERSATION_TITLE_LOCAL_STORAGE_KEY}`);
|
||||
|
||||
/**
|
||||
* Local storage for knowledge base configuration, prefixed by assistant nameSpace
|
||||
|
@ -257,43 +247,13 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
*/
|
||||
const [selectedSettingsTab, setSelectedSettingsTab] = useState<SettingsTabs>(CONVERSATIONS_TAB);
|
||||
|
||||
const [conversations, setConversationsInternal] = useState(getInitialConversations());
|
||||
const conversationIds = useMemo(() => Object.keys(conversations).sort(), [conversations]);
|
||||
|
||||
// TODO: This is a fix for conversations not loading out of localstorage. Also re-introduces our cascading render issue (as it loops back in localstorage)
|
||||
useEffect(() => {
|
||||
setConversationsInternal(getInitialConversations());
|
||||
}, [getInitialConversations]);
|
||||
|
||||
const onConversationsUpdated = useCallback<
|
||||
React.Dispatch<React.SetStateAction<Record<string, Conversation>>>
|
||||
>(
|
||||
(
|
||||
newConversations:
|
||||
| Record<string, Conversation>
|
||||
| ((prev: Record<string, Conversation>) => Record<string, Conversation>)
|
||||
) => {
|
||||
if (typeof newConversations === 'function') {
|
||||
const updater = newConversations;
|
||||
setConversationsInternal((prevValue) => {
|
||||
const newValue = updater(prevValue);
|
||||
setConversations(newValue);
|
||||
return newValue;
|
||||
});
|
||||
} else {
|
||||
setConversations(newConversations);
|
||||
setConversationsInternal(newConversations);
|
||||
}
|
||||
},
|
||||
[setConversations]
|
||||
);
|
||||
|
||||
const getConversationId = useCallback(
|
||||
const getLastConversationTitle = useCallback(
|
||||
// if a conversationId has been provided, use that
|
||||
// if not, check local storage
|
||||
// last resort, go to welcome conversation
|
||||
(id?: string) => id ?? localStorageLastConversationId ?? WELCOME_CONVERSATION_TITLE,
|
||||
[localStorageLastConversationId]
|
||||
(conversationTitle?: string) =>
|
||||
conversationTitle ?? localStorageLastConversationTitle ?? WELCOME_CONVERSATION_TITLE,
|
||||
[localStorageLastConversationTitle]
|
||||
);
|
||||
|
||||
// Fetch assistant capabilities
|
||||
|
@ -317,8 +277,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
basePromptContexts,
|
||||
baseQuickPrompts,
|
||||
baseSystemPrompts,
|
||||
conversationIds,
|
||||
conversations,
|
||||
defaultAllow: uniq(defaultAllow),
|
||||
defaultAllowReplacement: uniq(defaultAllowReplacement),
|
||||
docLinks,
|
||||
|
@ -332,7 +290,6 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
selectedSettingsTab,
|
||||
setAllQuickPrompts: setLocalStorageQuickPrompts,
|
||||
setAllSystemPrompts: setLocalStorageSystemPrompts,
|
||||
setConversations: onConversationsUpdated,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setKnowledgeBase: setLocalStorageKnowledgeBase,
|
||||
|
@ -342,8 +299,9 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
title,
|
||||
toasts,
|
||||
unRegisterPromptContext,
|
||||
getConversationId,
|
||||
setLastConversationId: setLocalStorageLastConversationId,
|
||||
getLastConversationTitle,
|
||||
setLastConversationTitle: setLocalStorageLastConversationTitle,
|
||||
baseConversations,
|
||||
}),
|
||||
[
|
||||
actionTypeRegistry,
|
||||
|
@ -352,39 +310,37 @@ export const AssistantProvider: React.FC<AssistantProviderProps> = ({
|
|||
assistantStreamingEnabled,
|
||||
assistantTelemetry,
|
||||
augmentMessageCodeBlocks,
|
||||
localStorageQuickPrompts,
|
||||
localStorageSystemPrompts,
|
||||
baseAllow,
|
||||
baseAllowReplacement,
|
||||
basePath,
|
||||
basePromptContexts,
|
||||
baseQuickPrompts,
|
||||
baseSystemPrompts,
|
||||
conversationIds,
|
||||
conversations,
|
||||
defaultAllow,
|
||||
defaultAllowReplacement,
|
||||
docLinks,
|
||||
getComments,
|
||||
http,
|
||||
localStorageKnowledgeBase,
|
||||
getConversationId,
|
||||
localStorageQuickPrompts,
|
||||
localStorageSystemPrompts,
|
||||
modelEvaluatorEnabled,
|
||||
nameSpace,
|
||||
onConversationsUpdated,
|
||||
promptContexts,
|
||||
nameSpace,
|
||||
registerPromptContext,
|
||||
selectedSettingsTab,
|
||||
setLocalStorageQuickPrompts,
|
||||
setLocalStorageSystemPrompts,
|
||||
setDefaultAllow,
|
||||
setDefaultAllowReplacement,
|
||||
setLocalStorageKnowledgeBase,
|
||||
setLocalStorageLastConversationId,
|
||||
setLocalStorageQuickPrompts,
|
||||
setLocalStorageSystemPrompts,
|
||||
showAssistantOverlay,
|
||||
title,
|
||||
toasts,
|
||||
unRegisterPromptContext,
|
||||
getLastConversationTitle,
|
||||
setLocalStorageLastConversationTitle,
|
||||
baseConversations,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { ApiConfig, Replacement } from '@kbn/elastic-assistant-common';
|
||||
|
||||
export type ConversationRole = 'system' | 'user' | 'assistant';
|
||||
|
||||
|
@ -13,10 +13,11 @@ export interface MessagePresentation {
|
|||
delay?: number;
|
||||
stream?: boolean;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: ConversationRole;
|
||||
reader?: ReadableStreamDefaultReader<Uint8Array>;
|
||||
replacements?: Record<string, string>;
|
||||
replacements?: Replacement[];
|
||||
content?: string;
|
||||
timestamp: string;
|
||||
isError?: boolean;
|
||||
|
@ -43,24 +44,25 @@ export interface ConversationTheme {
|
|||
icon?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete state to reconstruct a conversation instance.
|
||||
* Includes all messages, connector configured, and relevant UI state.
|
||||
*
|
||||
*/
|
||||
export interface Conversation {
|
||||
apiConfig: {
|
||||
connectorId?: string;
|
||||
connectorTypeTitle?: string;
|
||||
defaultSystemPromptId?: string;
|
||||
provider?: OpenAiProviderType;
|
||||
model?: string;
|
||||
'@timestamp'?: string;
|
||||
apiConfig?: ApiConfig;
|
||||
user?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
category: string;
|
||||
id: string;
|
||||
title: string;
|
||||
messages: Message[];
|
||||
replacements?: Record<string, string>;
|
||||
theme?: ConversationTheme;
|
||||
updatedAt?: Date;
|
||||
createdAt?: Date;
|
||||
replacements: Replacement[];
|
||||
isDefault?: boolean;
|
||||
excludeFromLastConversationStorage?: boolean;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||
|
||||
import { ActionConnector, ActionType } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { useLoadConnectors } from '../use_load_connectors';
|
||||
import * as i18n from '../translations';
|
||||
import { useLoadActionTypes } from '../use_load_action_types';
|
||||
|
@ -29,7 +30,10 @@ interface Props {
|
|||
}
|
||||
|
||||
export type AIConnector = ActionConnector & {
|
||||
// ex: Bedrock, OpenAI
|
||||
connectorTypeTitle: string;
|
||||
// related to OpenAI connectors, ex: Azure OpenAI, OpenAI
|
||||
apiProvider?: OpenAiProviderType;
|
||||
};
|
||||
|
||||
export const ConnectorSelector: React.FC<Props> = React.memo(
|
||||
|
@ -49,22 +53,11 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
|
||||
|
||||
const {
|
||||
data: connectorsWithoutActionContext,
|
||||
data: aiConnectors,
|
||||
isLoading: isLoadingConnectors,
|
||||
isFetching: isFetchingConnectors,
|
||||
refetch: refetchConnectors,
|
||||
} = useLoadConnectors({ http });
|
||||
|
||||
const aiConnectors: AIConnector[] = useMemo(
|
||||
() =>
|
||||
connectorsWithoutActionContext
|
||||
? connectorsWithoutActionContext.map((c) => ({
|
||||
...c,
|
||||
connectorTypeTitle: getActionTypeTitle(actionTypeRegistry.get(c.actionTypeId)),
|
||||
}))
|
||||
: [],
|
||||
[actionTypeRegistry, connectorsWithoutActionContext]
|
||||
);
|
||||
} = useLoadConnectors({ actionTypeRegistry, http });
|
||||
|
||||
const isLoading = isLoadingConnectors || isFetchingConnectors;
|
||||
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
|
||||
|
@ -96,7 +89,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
|
||||
const connectorOptions = useMemo(
|
||||
() =>
|
||||
aiConnectors.map((connector) => {
|
||||
(aiConnectors ?? []).map((connector) => {
|
||||
const connectorTypeTitle =
|
||||
getGenAiConfig(connector)?.apiProvider ?? connector.connectorTypeTitle;
|
||||
const connectorDetails = connector.isPreconfigured
|
||||
|
@ -146,7 +139,7 @@ export const ConnectorSelector: React.FC<Props> = React.memo(
|
|||
return;
|
||||
}
|
||||
|
||||
const connector = aiConnectors.find((c) => c.id === connectorId);
|
||||
const connector = (aiConnectors ?? []).find((c) => c.id === connectorId);
|
||||
if (connector) {
|
||||
onConnectorSelectionChange(connector);
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ describe('ConnectorSelectorInline', () => {
|
|||
isDisabled={false}
|
||||
selectedConnectorId={undefined}
|
||||
selectedConversation={undefined}
|
||||
onConnectorSelected={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -74,8 +75,11 @@ describe('ConnectorSelectorInline', () => {
|
|||
it('renders empty view if selectedConnectorId is NOT in list of connectors', () => {
|
||||
const conversation: Conversation = {
|
||||
id: 'conversation_id',
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
title: 'conversation_id',
|
||||
};
|
||||
const { getByText } = render(
|
||||
<TestProviders>
|
||||
|
@ -83,6 +87,7 @@ describe('ConnectorSelectorInline', () => {
|
|||
isDisabled={false}
|
||||
selectedConnectorId={'missing-connector-id'}
|
||||
selectedConversation={conversation}
|
||||
onConnectorSelected={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -91,8 +96,11 @@ describe('ConnectorSelectorInline', () => {
|
|||
it('Clicking add connector button opens the connector selector', () => {
|
||||
const conversation: Conversation = {
|
||||
id: 'conversation_id',
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
title: 'conversation_id',
|
||||
};
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
|
@ -100,6 +108,7 @@ describe('ConnectorSelectorInline', () => {
|
|||
isDisabled={false}
|
||||
selectedConnectorId={'missing-connector-id'}
|
||||
selectedConversation={conversation}
|
||||
onConnectorSelected={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -111,8 +120,11 @@ describe('ConnectorSelectorInline', () => {
|
|||
const connectorTwo = mockConnectors[1];
|
||||
const conversation: Conversation = {
|
||||
id: 'conversation_id',
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
title: 'conversation_id',
|
||||
};
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<TestProviders>
|
||||
|
@ -120,6 +132,7 @@ describe('ConnectorSelectorInline', () => {
|
|||
isDisabled={false}
|
||||
selectedConnectorId={'missing-connector-id'}
|
||||
selectedConversation={conversation}
|
||||
onConnectorSelected={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
@ -134,14 +147,24 @@ describe('ConnectorSelectorInline', () => {
|
|||
model: undefined,
|
||||
provider: 'OpenAI',
|
||||
},
|
||||
conversationId: 'conversation_id',
|
||||
conversation: {
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
category: 'assistant',
|
||||
id: 'conversation_id',
|
||||
messages: [],
|
||||
title: 'conversation_id',
|
||||
},
|
||||
});
|
||||
});
|
||||
it('On connector change to add new connector, onchange event does nothing', () => {
|
||||
const conversation: Conversation = {
|
||||
id: 'conversation_id',
|
||||
category: 'assistant',
|
||||
messages: [],
|
||||
apiConfig: {},
|
||||
apiConfig: { connectorId: '123', connectorTypeTitle: 'OpenAI' },
|
||||
replacements: [],
|
||||
title: 'conversation_id',
|
||||
};
|
||||
const { getByTestId } = render(
|
||||
<TestProviders>
|
||||
|
@ -149,6 +172,7 @@ describe('ConnectorSelectorInline', () => {
|
|||
isDisabled={false}
|
||||
selectedConnectorId={'missing-connector-id'}
|
||||
selectedConversation={conversation}
|
||||
onConnectorSelected={jest.fn()}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
import { AIConnector, ConnectorSelector } from '../connector_selector';
|
||||
|
@ -15,7 +15,7 @@ import { useLoadConnectors } from '../use_load_connectors';
|
|||
import * as i18n from '../translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { useConversation } from '../../assistant/use_conversation';
|
||||
import { getActionTypeTitle, getGenAiConfig } from '../helpers';
|
||||
import { getGenAiConfig } from '../helpers';
|
||||
|
||||
export const ADD_NEW_CONNECTOR = 'ADD_NEW_CONNECTOR';
|
||||
|
||||
|
@ -23,6 +23,7 @@ interface Props {
|
|||
isDisabled?: boolean;
|
||||
selectedConnectorId?: string;
|
||||
selectedConversation?: Conversation;
|
||||
onConnectorSelected: (conversation: Conversation) => void;
|
||||
}
|
||||
|
||||
const inputContainerClassName = css`
|
||||
|
@ -65,26 +66,18 @@ const placeholderButtonClassName = css`
|
|||
* A compact wrapper of the ConnectorSelector component used in the Settings modal.
|
||||
*/
|
||||
export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
||||
({ isDisabled = false, selectedConnectorId, selectedConversation }) => {
|
||||
({ isDisabled = false, selectedConnectorId, selectedConversation, onConnectorSelected }) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const { actionTypeRegistry, assistantAvailability, http } = useAssistantContext();
|
||||
const { setApiConfig } = useConversation();
|
||||
|
||||
const { data: connectorsWithoutActionContext } = useLoadConnectors({ http });
|
||||
|
||||
const aiConnectors: AIConnector[] = useMemo(
|
||||
() =>
|
||||
connectorsWithoutActionContext
|
||||
? connectorsWithoutActionContext.map((c) => ({
|
||||
...c,
|
||||
connectorTypeTitle: getActionTypeTitle(actionTypeRegistry.get(c.actionTypeId)),
|
||||
}))
|
||||
: [],
|
||||
[actionTypeRegistry, connectorsWithoutActionContext]
|
||||
);
|
||||
const { data: aiConnectors } = useLoadConnectors({
|
||||
actionTypeRegistry,
|
||||
http,
|
||||
});
|
||||
|
||||
const selectedConnectorName =
|
||||
aiConnectors.find((c) => c.id === selectedConnectorId)?.name ??
|
||||
(aiConnectors ?? []).find((c) => c.id === selectedConnectorId)?.name ??
|
||||
i18n.INLINE_CONNECTOR_PLACEHOLDER;
|
||||
const localIsDisabled = isDisabled || !assistantAvailability.hasConnectorsReadPrivilege;
|
||||
|
||||
|
@ -93,7 +86,7 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
}, [isOpen]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(connector: AIConnector) => {
|
||||
async (connector: AIConnector) => {
|
||||
const connectorId = connector.id;
|
||||
if (connectorId === ADD_NEW_CONNECTOR) {
|
||||
return;
|
||||
|
@ -105,8 +98,8 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
setIsOpen(false);
|
||||
|
||||
if (selectedConversation != null) {
|
||||
setApiConfig({
|
||||
conversationId: selectedConversation.id,
|
||||
const conversation = await setApiConfig({
|
||||
conversation: selectedConversation,
|
||||
apiConfig: {
|
||||
...selectedConversation.apiConfig,
|
||||
connectorId,
|
||||
|
@ -116,9 +109,13 @@ export const ConnectorSelectorInline: React.FC<Props> = React.memo(
|
|||
model: model ?? config?.defaultModel,
|
||||
},
|
||||
});
|
||||
|
||||
if (conversation) {
|
||||
onConnectorSelected(conversation);
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedConversation, setApiConfig]
|
||||
[selectedConversation, setApiConfig, onConnectorSelected]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -9,14 +9,17 @@ import React from 'react';
|
|||
import { useConnectorSetup } from '.';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { alertConvo, welcomeConvo } from '../../mock/conversation';
|
||||
import { welcomeConvo } from '../../mock/conversation';
|
||||
import { TestProviders } from '../../mock/test_providers/test_providers';
|
||||
import { EuiCommentList } from '@elastic/eui';
|
||||
|
||||
const onSetupComplete = jest.fn();
|
||||
const onConversationUpdate = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
conversation: welcomeConvo,
|
||||
onSetupComplete,
|
||||
onConversationUpdate,
|
||||
};
|
||||
const newConnector = { actionTypeId: '.gen-ai', name: 'cool name' };
|
||||
jest.mock('../add_connector_modal', () => ({
|
||||
|
@ -32,8 +35,7 @@ jest.mock('../add_connector_modal', () => ({
|
|||
),
|
||||
}));
|
||||
|
||||
const setConversation = jest.fn();
|
||||
const setApiConfig = jest.fn();
|
||||
const setApiConfig = jest.fn().mockResolvedValue(welcomeConvo);
|
||||
const mockConversation = {
|
||||
appendMessage: jest.fn(),
|
||||
appendReplacements: jest.fn(),
|
||||
|
@ -41,7 +43,6 @@ const mockConversation = {
|
|||
createConversation: jest.fn(),
|
||||
deleteConversation: jest.fn(),
|
||||
setApiConfig,
|
||||
setConversation,
|
||||
};
|
||||
|
||||
jest.mock('../../assistant/use_conversation', () => ({
|
||||
|
@ -56,18 +57,7 @@ describe('useConnectorSetup', () => {
|
|||
it('should render comments and prompts', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
expect(
|
||||
|
@ -78,7 +68,7 @@ describe('useConnectorSetup', () => {
|
|||
timestamp: 'at: 7/17/2023, 1:00:36 PM',
|
||||
},
|
||||
{
|
||||
username: 'Elastic AI Assistant',
|
||||
username: 'Assistant',
|
||||
timestamp: 'at: 7/17/2023, 1:00:40 PM',
|
||||
},
|
||||
]);
|
||||
|
@ -89,18 +79,7 @@ describe('useConnectorSetup', () => {
|
|||
it('should set api config for each conversation when new connector is saved', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
const { getByTestId, queryByTestId, rerender } = render(result.current.prompt, {
|
||||
|
@ -112,7 +91,7 @@ describe('useConnectorSetup', () => {
|
|||
|
||||
rerender(result.current.prompt);
|
||||
fireEvent.click(getByTestId('modal-mock'));
|
||||
expect(setApiConfig).toHaveBeenCalledTimes(2);
|
||||
expect(setApiConfig).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -147,21 +126,10 @@ describe('useConnectorSetup', () => {
|
|||
expect(queryByTestId('connectorButton')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('should call onSetupComplete and setConversation when onHandleMessageStreamingComplete', async () => {
|
||||
it('should call onSetupComplete and setConversations when onHandleMessageStreamingComplete', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useConnectorSetup(defaultProps), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestProviders
|
||||
providerContext={{
|
||||
getInitialConversations: () => ({
|
||||
[alertConvo.id]: alertConvo,
|
||||
[welcomeConvo.id]: welcomeConvo,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TestProviders>
|
||||
),
|
||||
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
render(<EuiCommentList comments={result.current.comments} />, {
|
||||
|
@ -170,7 +138,6 @@ describe('useConnectorSetup', () => {
|
|||
|
||||
expect(clearTimeout).toHaveBeenCalled();
|
||||
expect(onSetupComplete).toHaveBeenCalled();
|
||||
expect(setConversation).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ import { useLoadActionTypes } from '../use_load_action_types';
|
|||
import { StreamingText } from '../../assistant/streaming_text';
|
||||
import { ConnectorButton } from '../connector_button';
|
||||
import { useConversation } from '../../assistant/use_conversation';
|
||||
import { clearPresentationData, conversationHasNoPresentationData } from './helpers';
|
||||
import { conversationHasNoPresentationData } from './helpers';
|
||||
import * as i18n from '../translations';
|
||||
import { useAssistantContext } from '../../assistant_context';
|
||||
import { useLoadConnectors } from '../use_load_connectors';
|
||||
|
@ -38,24 +38,27 @@ const SkipEuiText = styled(EuiText)`
|
|||
export interface ConnectorSetupProps {
|
||||
conversation?: Conversation;
|
||||
onSetupComplete?: () => void;
|
||||
onConversationUpdate: ({ cId, cTitle }: { cId: string; cTitle: string }) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useConnectorSetup = ({
|
||||
conversation = WELCOME_CONVERSATION,
|
||||
onSetupComplete,
|
||||
onConversationUpdate,
|
||||
}: ConnectorSetupProps): {
|
||||
comments: EuiCommentProps[];
|
||||
prompt: React.ReactElement;
|
||||
} => {
|
||||
const { appendMessage, setApiConfig, setConversation } = useConversation();
|
||||
const { setApiConfig } = useConversation();
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null);
|
||||
// Access all conversations so we can add connector to all on initial setup
|
||||
const { actionTypeRegistry, conversations, http } = useAssistantContext();
|
||||
const { actionTypeRegistry, http } = useAssistantContext();
|
||||
|
||||
const {
|
||||
data: connectors,
|
||||
isSuccess: areConnectorsFetched,
|
||||
refetch: refetchConnectors,
|
||||
} = useLoadConnectors({ http });
|
||||
} = useLoadConnectors({ actionTypeRegistry, http });
|
||||
const isConnectorConfigured = areConnectorsFetched && !!connectors?.length;
|
||||
|
||||
const [isConnectorModalVisible, setIsConnectorModalVisible] = useState<boolean>(false);
|
||||
|
@ -67,15 +70,6 @@ export const useConnectorSetup = ({
|
|||
|
||||
const [selectedActionType, setSelectedActionType] = useState<ActionType | null>(null);
|
||||
|
||||
// User constants
|
||||
const userName = useMemo(
|
||||
() => conversation.theme?.user?.name ?? i18n.CONNECTOR_SETUP_USER_YOU,
|
||||
[conversation.theme?.user?.name]
|
||||
);
|
||||
const assistantName = useMemo(
|
||||
() => conversation.theme?.assistant?.name ?? i18n.CONNECTOR_SETUP_USER_ASSISTANT,
|
||||
[conversation.theme?.assistant?.name]
|
||||
);
|
||||
const lastConversationMessageIndex = useMemo(
|
||||
() => conversation.messages.length - 1,
|
||||
[conversation.messages.length]
|
||||
|
@ -108,8 +102,7 @@ export const useConnectorSetup = ({
|
|||
setShowAddConnectorButton(true);
|
||||
bottomRef.current?.scrollIntoView({ block: 'end' });
|
||||
onSetupComplete?.();
|
||||
setConversation({ conversation: clearPresentationData(conversation) });
|
||||
}, [conversation, onSetupComplete, setConversation]);
|
||||
}, [onSetupComplete]);
|
||||
|
||||
// Show button to add connector after last message has finished streaming
|
||||
const handleSkipSetup = useCallback(() => {
|
||||
|
@ -160,7 +153,7 @@ export const useConnectorSetup = ({
|
|||
const isUser = message.role === 'user';
|
||||
|
||||
const commentProps: EuiCommentProps = {
|
||||
username: isUser ? userName : assistantName,
|
||||
username: isUser ? i18n.CONNECTOR_SETUP_USER_YOU : i18n.CONNECTOR_SETUP_USER_ASSISTANT,
|
||||
children: commentBody(message, index, conversation.messages.length),
|
||||
timelineAvatar: (
|
||||
<EuiAvatar
|
||||
|
@ -174,46 +167,35 @@ export const useConnectorSetup = ({
|
|||
};
|
||||
return commentProps;
|
||||
}),
|
||||
[assistantName, commentBody, conversation.messages, currentMessageIndex, userName]
|
||||
[commentBody, conversation.messages, currentMessageIndex]
|
||||
);
|
||||
|
||||
const onSaveConnector = useCallback(
|
||||
(connector: ActionConnector) => {
|
||||
async (connector: ActionConnector) => {
|
||||
const config = getGenAiConfig(connector);
|
||||
// add action type title to new connector
|
||||
const connectorTypeTitle = getActionTypeTitle(actionTypeRegistry.get(connector.actionTypeId));
|
||||
Object.values(conversations).forEach((c) => {
|
||||
setApiConfig({
|
||||
conversationId: c.id,
|
||||
apiConfig: {
|
||||
...c.apiConfig,
|
||||
connectorId: connector.id,
|
||||
connectorTypeTitle,
|
||||
provider: config?.apiProvider,
|
||||
model: config?.defaultModel,
|
||||
},
|
||||
});
|
||||
});
|
||||
// persist only the active conversation
|
||||
|
||||
refetchConnectors?.();
|
||||
setIsConnectorModalVisible(false);
|
||||
appendMessage({
|
||||
conversationId: conversation.id,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: i18n.CONNECTOR_SETUP_COMPLETE,
|
||||
timestamp: new Date().toLocaleString(),
|
||||
const updatedConversation = await setApiConfig({
|
||||
conversation,
|
||||
apiConfig: {
|
||||
...conversation.apiConfig,
|
||||
connectorId: connector.id,
|
||||
connectorTypeTitle,
|
||||
provider: config?.apiProvider,
|
||||
model: config?.defaultModel,
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedConversation) {
|
||||
onConversationUpdate({ cId: updatedConversation.id, cTitle: updatedConversation.title });
|
||||
|
||||
refetchConnectors?.();
|
||||
setIsConnectorModalVisible(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
actionTypeRegistry,
|
||||
appendMessage,
|
||||
conversation.id,
|
||||
conversations,
|
||||
refetchConnectors,
|
||||
setApiConfig,
|
||||
]
|
||||
[actionTypeRegistry, conversation, onConversationUpdate, refetchConnectors, setApiConfig]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -41,7 +41,7 @@ export const PRECONFIGURED_CONNECTOR = i18n.translate(
|
|||
export const CONNECTOR_SELECTOR_TITLE = i18n.translate(
|
||||
'xpack.elasticAssistant.assistant.connectors.connectorSelector.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Conversation Selector',
|
||||
defaultMessage: 'Connector Selector',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -82,7 +82,10 @@ describe('useLoadConnectors', () => {
|
|||
const { result, waitForNextUpdate } = renderHook(() => useLoadConnectors(defaultProps));
|
||||
await waitForNextUpdate();
|
||||
|
||||
await expect(result.current).resolves.toStrictEqual(loadConnectorsResult);
|
||||
await expect(result.current).resolves.toStrictEqual(
|
||||
// @ts-ignore ts does not like config, but we define it in the mock data
|
||||
loadConnectorsResult.map((c) => ({ ...c, apiProvider: c.config.apiProvider }))
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should display error toast when api throws error', async () => {
|
||||
|
|
|
@ -10,9 +10,13 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import type { ServerError } from '@kbn/cases-plugin/public/types';
|
||||
import { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { IToasts } from '@kbn/core-notifications-browser';
|
||||
import { useMemo } from 'react';
|
||||
import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/constants';
|
||||
import { AIConnector } from '../connector_selector';
|
||||
import { getActionTypeTitle } from '../helpers';
|
||||
import * as i18n from '../translations';
|
||||
|
||||
/**
|
||||
|
@ -22,21 +26,62 @@ import * as i18n from '../translations';
|
|||
const QUERY_KEY = ['elastic-assistant, load-connectors'];
|
||||
|
||||
export interface Props {
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
http: HttpSetup;
|
||||
toasts?: IToasts;
|
||||
}
|
||||
|
||||
const actionTypeKey = {
|
||||
bedrock: '.bedrock',
|
||||
openai: '.gen-ai',
|
||||
};
|
||||
|
||||
export const useLoadConnectors = ({
|
||||
actionTypeRegistry,
|
||||
http,
|
||||
toasts,
|
||||
}: Props): UseQueryResult<ActionConnector[], IHttpFetchError> => {
|
||||
}: Props): UseQueryResult<AIConnector[], IHttpFetchError> => {
|
||||
const connectorDetails = useMemo(
|
||||
() =>
|
||||
actionTypeRegistry
|
||||
? {
|
||||
[actionTypeKey.bedrock]: getActionTypeTitle(
|
||||
actionTypeRegistry.get(actionTypeKey.bedrock)
|
||||
),
|
||||
[actionTypeKey.openai]: getActionTypeTitle(
|
||||
actionTypeRegistry.get(actionTypeKey.openai)
|
||||
),
|
||||
}
|
||||
: {
|
||||
[actionTypeKey.bedrock]: 'Amazon Bedrock',
|
||||
[actionTypeKey.openai]: 'OpenAI',
|
||||
},
|
||||
[actionTypeRegistry]
|
||||
);
|
||||
return useQuery(
|
||||
QUERY_KEY,
|
||||
async () => {
|
||||
const queryResult = await loadConnectors({ http });
|
||||
return queryResult.filter(
|
||||
(connector) =>
|
||||
!connector.isMissingSecrets && ['.bedrock', '.gen-ai'].includes(connector.actionTypeId)
|
||||
return queryResult.reduce(
|
||||
(acc: AIConnector[], connector) => [
|
||||
...acc,
|
||||
...(!connector.isMissingSecrets &&
|
||||
[actionTypeKey.bedrock, actionTypeKey.openai].includes(connector.actionTypeId)
|
||||
? [
|
||||
{
|
||||
...connector,
|
||||
connectorTypeTitle: connectorDetails[connector.actionTypeId],
|
||||
apiProvider:
|
||||
!connector.isPreconfigured &&
|
||||
!connector.isSystemAction &&
|
||||
connector?.config?.apiProvider
|
||||
? (connector?.config?.apiProvider as OpenAiProviderType)
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[]
|
||||
);
|
||||
},
|
||||
{
|
||||
|
|
|
@ -58,6 +58,8 @@ describe('useDeleteKnowledgeBase', () => {
|
|||
'/internal/elastic_assistant/knowledge_base/',
|
||||
{
|
||||
method: 'DELETE',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
|
@ -80,6 +82,8 @@ describe('useDeleteKnowledgeBase', () => {
|
|||
'/internal/elastic_assistant/knowledge_base/something',
|
||||
{
|
||||
method: 'DELETE',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -57,6 +57,8 @@ describe('useKnowledgeBaseStatus', () => {
|
|||
'/internal/elastic_assistant/knowledge_base/',
|
||||
{
|
||||
method: 'GET',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
|
@ -73,6 +75,8 @@ describe('useKnowledgeBaseStatus', () => {
|
|||
'/internal/elastic_assistant/knowledge_base/something',
|
||||
{
|
||||
method: 'GET',
|
||||
signal: undefined,
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -57,6 +57,7 @@ describe('useSetupKnowledgeBase', () => {
|
|||
'/internal/elastic_assistant/knowledge_base/',
|
||||
{
|
||||
method: 'POST',
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
expect(toasts.addError).not.toHaveBeenCalled();
|
||||
|
@ -79,6 +80,7 @@ describe('useSetupKnowledgeBase', () => {
|
|||
'/internal/elastic_assistant/knowledge_base/something',
|
||||
{
|
||||
method: 'POST',
|
||||
version: '1',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ActionType } from '@kbn/actions-plugin/common';
|
||||
import { ActionConnector } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { AIConnector } from '../connectorland/connector_selector';
|
||||
|
||||
export const mockActionTypes = [
|
||||
{
|
||||
|
@ -31,9 +31,10 @@ export const mockActionTypes = [
|
|||
} as ActionType,
|
||||
];
|
||||
|
||||
export const mockConnectors: ActionConnector[] = [
|
||||
export const mockConnectors: AIConnector[] = [
|
||||
{
|
||||
id: 'connectorId',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
name: 'Captain Connector',
|
||||
isMissingSecrets: false,
|
||||
actionTypeId: '.gen-ai',
|
||||
|
@ -47,6 +48,7 @@ export const mockConnectors: ActionConnector[] = [
|
|||
},
|
||||
{
|
||||
id: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
name: 'Professor Connector',
|
||||
isMissingSecrets: false,
|
||||
actionTypeId: '.gen-ai',
|
||||
|
|
|
@ -9,7 +9,9 @@ import { OpenAiProviderType } from '@kbn/stack-connectors-plugin/common/openai/c
|
|||
import { Conversation } from '../..';
|
||||
|
||||
export const alertConvo: Conversation = {
|
||||
id: 'Alert summary',
|
||||
id: '',
|
||||
title: 'Alert summary',
|
||||
category: 'assistant',
|
||||
isDefault: true,
|
||||
messages: [
|
||||
{
|
||||
|
@ -21,33 +23,26 @@ export const alertConvo: Conversation = {
|
|||
],
|
||||
apiConfig: {
|
||||
connectorId: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
},
|
||||
replacements: {
|
||||
'94277492-11f8-493b-9c52-c1c9ecd330d2': '192.168.0.4',
|
||||
'67bf8338-261a-4de6-b43e-d30b59e884a7': '192.168.0.1',
|
||||
'0b2e352b-35fc-47bd-a8d4-43019ed38a25': 'Stephs-MacBook-Pro.local',
|
||||
},
|
||||
replacements: [
|
||||
{ uuid: '94277492-11f8-493b-9c52-c1c9ecd330d2', value: '192.168.0.4' },
|
||||
{ uuid: '67bf8338-261a-4de6-b43e-d30b59e884a7', value: '192.168.0.1' },
|
||||
{ uuid: '0b2e352b-35fc-47bd-a8d4-43019ed38a25', value: 'Stephs-MacBook-Pro.local' },
|
||||
],
|
||||
};
|
||||
|
||||
export const emptyWelcomeConvo: Conversation = {
|
||||
id: 'Welcome',
|
||||
id: '',
|
||||
title: 'Welcome',
|
||||
category: 'assistant',
|
||||
isDefault: true,
|
||||
theme: {
|
||||
title: 'Elastic AI Assistant',
|
||||
titleIcon: 'logoSecurity',
|
||||
assistant: {
|
||||
name: 'Elastic AI Assistant',
|
||||
icon: 'logoSecurity',
|
||||
},
|
||||
system: {
|
||||
icon: 'logoElastic',
|
||||
},
|
||||
user: {},
|
||||
},
|
||||
messages: [],
|
||||
replacements: [],
|
||||
apiConfig: {
|
||||
connectorId: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
},
|
||||
};
|
||||
|
@ -71,11 +66,15 @@ export const welcomeConvo: Conversation = {
|
|||
};
|
||||
|
||||
export const customConvo: Conversation = {
|
||||
id: 'Custom option',
|
||||
id: '',
|
||||
category: 'assistant',
|
||||
title: 'Custom option',
|
||||
isDefault: false,
|
||||
messages: [],
|
||||
replacements: [],
|
||||
apiConfig: {
|
||||
connectorId: 'c29c28a0-20fe-11ee-9306-a1f4d42ec542',
|
||||
connectorTypeTitle: 'OpenAI',
|
||||
provider: OpenAiProviderType.OpenAi,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -15,20 +15,17 @@ import { ThemeProvider } from 'styled-components';
|
|||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AssistantProvider, AssistantProviderProps } from '../../assistant_context';
|
||||
import { AssistantAvailability, Conversation } from '../../assistant_context/types';
|
||||
import { AssistantAvailability } from '../../assistant_context/types';
|
||||
|
||||
interface Props {
|
||||
assistantAvailability?: AssistantAvailability;
|
||||
children: React.ReactNode;
|
||||
getInitialConversations?: () => Record<string, Conversation>;
|
||||
providerContext?: Partial<AssistantProviderProps>;
|
||||
}
|
||||
|
||||
window.scrollTo = jest.fn();
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
const mockGetInitialConversations = () => ({});
|
||||
|
||||
export const mockAssistantAvailability: AssistantAvailability = {
|
||||
hasAssistantPrivilege: false,
|
||||
hasConnectorsAllPrivilege: true,
|
||||
|
@ -40,7 +37,6 @@ export const mockAssistantAvailability: AssistantAvailability = {
|
|||
export const TestProvidersComponent: React.FC<Props> = ({
|
||||
assistantAvailability = mockAssistantAvailability,
|
||||
children,
|
||||
getInitialConversations = mockGetInitialConversations,
|
||||
providerContext,
|
||||
}) => {
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
|
@ -83,11 +79,10 @@ export const TestProvidersComponent: React.FC<Props> = ({
|
|||
DOC_LINK_VERSION: 'current',
|
||||
}}
|
||||
getComments={mockGetComments}
|
||||
getInitialConversations={getInitialConversations}
|
||||
setConversations={jest.fn()}
|
||||
setDefaultAllow={jest.fn()}
|
||||
setDefaultAllowReplacement={jest.fn()}
|
||||
http={mockHttp}
|
||||
baseConversations={{}}
|
||||
{...providerContext}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { NewChatById } from '.';
|
||||
import { NewChatByTitle } from '.';
|
||||
|
||||
const mockUseAssistantContext = {
|
||||
showAssistantOverlay: jest.fn(),
|
||||
|
@ -18,71 +18,73 @@ jest.mock('../assistant_context', () => ({
|
|||
useAssistantContext: () => mockUseAssistantContext,
|
||||
}));
|
||||
|
||||
describe('NewChatById', () => {
|
||||
describe('NewChatByTitle', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the default New Chat button with a discuss icon', () => {
|
||||
render(<NewChatById />);
|
||||
render(<NewChatByTitle />);
|
||||
|
||||
const newChatButton = screen.getByTestId('newChatById');
|
||||
const newChatButton = screen.getByTestId('newChatByTitle');
|
||||
|
||||
expect(newChatButton.querySelector('[data-euiicon-type="discuss"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the default "New Chat" text when children are NOT provided', () => {
|
||||
render(<NewChatById />);
|
||||
render(<NewChatByTitle />);
|
||||
|
||||
const newChatButton = screen.getByTestId('newChatById');
|
||||
const newChatButton = screen.getByTestId('newChatByTitle');
|
||||
|
||||
expect(newChatButton.textContent).toContain('Chat');
|
||||
});
|
||||
|
||||
it('renders custom children', async () => {
|
||||
render(<NewChatById>{'🪄✨'}</NewChatById>);
|
||||
render(<NewChatByTitle>{'🪄✨'}</NewChatByTitle>);
|
||||
|
||||
const newChatButton = screen.getByTestId('newChatById');
|
||||
const newChatButton = screen.getByTestId('newChatByTitle');
|
||||
|
||||
expect(newChatButton.textContent).toContain('🪄✨');
|
||||
});
|
||||
|
||||
it('renders custom icons', async () => {
|
||||
render(<NewChatById iconType="help" />);
|
||||
render(<NewChatByTitle iconType="help" />);
|
||||
|
||||
const newChatButton = screen.getByTestId('newChatById');
|
||||
const newChatButton = screen.getByTestId('newChatByTitle');
|
||||
|
||||
expect(newChatButton.querySelector('[data-euiicon-type="help"]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does NOT render an icon when iconType is null', () => {
|
||||
render(<NewChatById iconType={null} />);
|
||||
render(<NewChatByTitle iconType={null} />);
|
||||
|
||||
const newChatButton = screen.getByTestId('newChatById');
|
||||
const newChatButton = screen.getByTestId('newChatByTitle');
|
||||
|
||||
expect(newChatButton.querySelector('.euiButtonContent__icon')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders button icon when iconOnly is true', async () => {
|
||||
render(<NewChatById iconOnly />);
|
||||
render(<NewChatByTitle iconOnly />);
|
||||
|
||||
const newChatButton = screen.getByTestId('newChatById');
|
||||
const newChatButton = screen.getByTestId('newChatByTitle');
|
||||
|
||||
expect(newChatButton.querySelector('[data-euiicon-type="discuss"]')).toBeInTheDocument();
|
||||
expect(newChatButton.textContent).not.toContain('Chat');
|
||||
});
|
||||
|
||||
it('calls showAssistantOverlay on click', () => {
|
||||
const conversationId = 'test-conversation-id';
|
||||
const conversationTitle = 'test-conversation-id';
|
||||
const promptContextId = 'test-prompt-context-id';
|
||||
|
||||
render(<NewChatById conversationId={conversationId} promptContextId={promptContextId} />);
|
||||
const newChatButton = screen.getByTestId('newChatById');
|
||||
render(
|
||||
<NewChatByTitle conversationTitle={conversationTitle} promptContextId={promptContextId} />
|
||||
);
|
||||
const newChatButton = screen.getByTestId('newChatByTitle');
|
||||
|
||||
userEvent.click(newChatButton);
|
||||
|
||||
expect(mockUseAssistantContext.showAssistantOverlay).toHaveBeenCalledWith({
|
||||
conversationId,
|
||||
conversationTitle,
|
||||
promptContextId,
|
||||
showOverlay: true,
|
||||
});
|
|
@ -15,7 +15,7 @@ import * as i18n from './translations';
|
|||
export interface Props {
|
||||
children?: React.ReactNode;
|
||||
/** Optionally automatically add this context to a conversation when the assistant is shown */
|
||||
conversationId?: string;
|
||||
conversationTitle?: string;
|
||||
/** Defaults to `discuss`. If null, the button will not have an icon */
|
||||
iconType?: string | null;
|
||||
/** Optionally specify a well known ID, or default to a UUID */
|
||||
|
@ -24,9 +24,9 @@ export interface Props {
|
|||
iconOnly?: boolean;
|
||||
}
|
||||
|
||||
const NewChatByIdComponent: React.FC<Props> = ({
|
||||
const NewChatByTitleComponent: React.FC<Props> = ({
|
||||
children = i18n.NEW_CHAT,
|
||||
conversationId,
|
||||
conversationTitle,
|
||||
iconType,
|
||||
promptContextId,
|
||||
iconOnly = false,
|
||||
|
@ -34,15 +34,13 @@ const NewChatByIdComponent: React.FC<Props> = ({
|
|||
const { showAssistantOverlay } = useAssistantContext();
|
||||
|
||||
// proxy show / hide calls to assistant context, using our internal prompt context id:
|
||||
const showOverlay = useCallback(
|
||||
() =>
|
||||
showAssistantOverlay({
|
||||
conversationId,
|
||||
promptContextId,
|
||||
showOverlay: true,
|
||||
}),
|
||||
[conversationId, promptContextId, showAssistantOverlay]
|
||||
);
|
||||
const showOverlay = useCallback(() => {
|
||||
showAssistantOverlay({
|
||||
conversationTitle,
|
||||
promptContextId,
|
||||
showOverlay: true,
|
||||
});
|
||||
}, [conversationTitle, promptContextId, showAssistantOverlay]);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (iconType === null) {
|
||||
|
@ -57,7 +55,7 @@ const NewChatByIdComponent: React.FC<Props> = ({
|
|||
iconOnly ? (
|
||||
<EuiToolTip content={i18n.NEW_CHAT}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj="newChatById"
|
||||
data-test-subj="newChatByTitle"
|
||||
iconType={icon ?? 'discuss'}
|
||||
onClick={showOverlay}
|
||||
color={'text'}
|
||||
|
@ -66,7 +64,7 @@ const NewChatByIdComponent: React.FC<Props> = ({
|
|||
</EuiToolTip>
|
||||
) : (
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="newChatById"
|
||||
data-test-subj="newChatByTitle"
|
||||
iconType={icon}
|
||||
onClick={showOverlay}
|
||||
aria-label={i18n.NEW_CHAT}
|
||||
|
@ -78,10 +76,10 @@ const NewChatByIdComponent: React.FC<Props> = ({
|
|||
);
|
||||
};
|
||||
|
||||
NewChatByIdComponent.displayName = 'NewChatByIdComponent';
|
||||
NewChatByTitleComponent.displayName = 'NewChatByTitleComponent';
|
||||
|
||||
/**
|
||||
* `NewChatByID` displays a _New chat_ icon button by providing only the `promptContextId`
|
||||
* `NewChatByTitle` displays a _New chat_ icon button by providing only the `promptContextId`
|
||||
* of a context that was (already) registered by the `useAssistantOverlay` hook. You may
|
||||
* optionally style the button icon, or override the default _New chat_ text with custom
|
||||
* content, like {'🪄✨'}
|
||||
|
@ -92,4 +90,4 @@ NewChatByIdComponent.displayName = 'NewChatByIdComponent';
|
|||
* registered where the data is available, and then the _New chat_ button can be displayed
|
||||
* in another part of the tree.
|
||||
*/
|
||||
export const NewChatById = React.memo(NewChatByIdComponent);
|
||||
export const NewChatByTitle = React.memo(NewChatByTitleComponent);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue