[Obs AI Assistant] knowledge base integration tests (#189000)

Closes https://github.com/elastic/kibana/issues/188999

- integration tests for knowledge base api
- adds new config field `modelId`, for internal use, to override elser
model id
- refactors `knowledgeBaseService.setup()` to fix bug where if the model
failed to install when calling ml.putTrainedModel, we dont get stuck
polling and retrying the install. We were assuming that the first error
that gets throw when the model is exists would only happen once and the
return true or false and poll for whether its done installing. But the
installation could fail itself causing getTrainedModelsStats to
continuously throw and try to install the model. Now user immediately
gets error if model fails to install and polling does not happen.

---------

Co-authored-by: James Gowdy <jgowdy@elastic.co>
This commit is contained in:
Sandra G 2024-08-05 13:02:01 -04:00 committed by GitHub
parent d79bdfd915
commit f18224c686
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 471 additions and 275 deletions

View file

@ -9,6 +9,7 @@ import { schema, type TypeOf } from '@kbn/config-schema';
export const config = schema.object({
enabled: schema.boolean({ defaultValue: true }),
modelId: schema.maybe(schema.string()),
});
export type ObservabilityAIAssistantConfig = TypeOf<typeof config>;

View file

@ -47,10 +47,12 @@ export class ObservabilityAIAssistantPlugin
>
{
logger: Logger;
config: ObservabilityAIAssistantConfig;
service: ObservabilityAIAssistantService | undefined;
constructor(context: PluginInitializerContext<ObservabilityAIAssistantConfig>) {
this.logger = context.logger.get();
this.config = context.config.get<ObservabilityAIAssistantConfig>();
initLangtrace();
}
public setup(
@ -112,10 +114,14 @@ export class ObservabilityAIAssistantPlugin
// Using once to make sure the same model ID is used during service init and Knowledge base setup
const getModelId = once(async () => {
const configModelId = this.config.modelId;
if (configModelId) {
return configModelId;
}
const defaultModelId = '.elser_model_2';
const [_, pluginsStart] = await core.getStartServices();
// Wait for the license to be available so the ML plugin's guards pass once we ask for ELSER stats
const license = await firstValueFrom(pluginsStart.licensing.license$);
if (!license.hasAtLeast('enterprise')) {
return defaultModelId;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { errors } from '@elastic/elasticsearch';
import { serverUnavailable, gatewayTimeout } from '@hapi/boom';
import { serverUnavailable, gatewayTimeout, badRequest } from '@hapi/boom';
import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
@ -39,14 +39,20 @@ export interface RecalledEntry {
labels?: Record<string, string>;
}
function isAlreadyExistsError(error: Error) {
function isModelMissingOrUnavailableError(error: Error) {
return (
error instanceof errors.ResponseError &&
(error.body.error.type === 'resource_not_found_exception' ||
error.body.error.type === 'status_exception')
);
}
function isCreateModelValidationError(error: Error) {
return (
error instanceof errors.ResponseError &&
error.statusCode === 400 &&
error.body?.error?.type === 'action_request_validation_exception'
);
}
function throwKnowledgeBaseNotReady(body: any) {
throw serverUnavailable(`Knowledge base is not ready yet`, body);
}
@ -84,52 +90,73 @@ export class KnowledgeBaseService {
const elserModelId = await this.dependencies.getModelId();
const retryOptions = { factor: 1, minTimeout: 10000, retries: 12 };
const installModel = async () => {
this.dependencies.logger.info('Installing ELSER model');
await this.dependencies.esClient.asInternalUser.ml.putTrainedModel(
{
model_id: elserModelId,
input: {
field_names: ['text_field'],
},
wait_for_completion: true,
},
{ requestTimeout: '20m' }
);
this.dependencies.logger.info('Finished installing ELSER model');
};
const getIsModelInstalled = async () => {
const getResponse = await this.dependencies.esClient.asInternalUser.ml.getTrainedModels({
const getModelInfo = async () => {
return await this.dependencies.esClient.asInternalUser.ml.getTrainedModels({
model_id: elserModelId,
include: 'definition_status',
});
this.dependencies.logger.debug(
() => 'Model definition status:\n' + JSON.stringify(getResponse.trained_model_configs[0])
);
return Boolean(getResponse.trained_model_configs[0]?.fully_defined);
};
await pRetry(async () => {
let isModelInstalled: boolean = false;
const isModelInstalledAndReady = async () => {
try {
isModelInstalled = await getIsModelInstalled();
const getResponse = await getModelInfo();
this.dependencies.logger.debug(
() => 'Model definition status:\n' + JSON.stringify(getResponse.trained_model_configs[0])
);
return Boolean(getResponse.trained_model_configs[0]?.fully_defined);
} catch (error) {
if (isAlreadyExistsError(error)) {
await installModel();
isModelInstalled = await getIsModelInstalled();
if (isModelMissingOrUnavailableError(error)) {
return false;
} else {
throw error;
}
}
};
if (!isModelInstalled) {
throwKnowledgeBaseNotReady({
message: 'Model is not fully defined',
});
const installModelIfDoesNotExist = async () => {
const modelInstalledAndReady = await isModelInstalledAndReady();
if (!modelInstalledAndReady) {
await installModel();
}
}, retryOptions);
};
const installModel = async () => {
this.dependencies.logger.info('Installing ELSER model');
try {
await this.dependencies.esClient.asInternalUser.ml.putTrainedModel(
{
model_id: elserModelId,
input: {
field_names: ['text_field'],
},
wait_for_completion: true,
},
{ requestTimeout: '20m' }
);
} catch (error) {
if (isCreateModelValidationError(error)) {
throw badRequest(error);
} else {
throw error;
}
}
this.dependencies.logger.info('Finished installing ELSER model');
};
const pollForModelInstallCompleted = async () => {
await pRetry(async () => {
this.dependencies.logger.info('Polling installation of ELSER model');
const modelInstalledAndReady = await isModelInstalledAndReady();
if (!modelInstalledAndReady) {
throwKnowledgeBaseNotReady({
message: 'Model is not fully defined',
});
}
}, retryOptions);
};
await installModelIfDoesNotExist();
await pollForModelInstallCompleted();
try {
await this.dependencies.esClient.asInternalUser.ml.startTrainedModelDeployment({
@ -139,7 +166,7 @@ export class KnowledgeBaseService {
} catch (error) {
this.dependencies.logger.debug('Error starting model deployment');
this.dependencies.logger.debug(error);
if (!isAlreadyExistsError(error)) {
if (!isModelMissingOrUnavailableError(error)) {
throw error;
}
}
@ -380,7 +407,7 @@ export class KnowledgeBaseService {
namespace,
modelId,
}).catch((error) => {
if (isAlreadyExistsError(error)) {
if (isModelMissingOrUnavailableError(error)) {
throwKnowledgeBaseNotReady(error.body);
}
throw error;
@ -521,7 +548,7 @@ export class KnowledgeBaseService {
})),
};
} catch (error) {
if (isAlreadyExistsError(error)) {
if (isModelMissingOrUnavailableError(error)) {
throwKnowledgeBaseNotReady(error.body);
}
throw error;
@ -588,7 +615,7 @@ export class KnowledgeBaseService {
return Promise.resolve();
} catch (error) {
if (isAlreadyExistsError(error)) {
if (isModelMissingOrUnavailableError(error)) {
throwKnowledgeBaseNotReady(error.body);
}
throw error;

View file

@ -1473,11 +1473,13 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
}
},
async stopTrainedModelDeploymentES(deploymentId: string) {
async stopTrainedModelDeploymentES(deploymentId: string, force: boolean = false) {
log.debug(`Stopping trained model deployment with id "${deploymentId}"`);
const { body, status } = await esSupertest.post(
`/_ml/trained_models/${deploymentId}/deployment/_stop`
);
const url = `/_ml/trained_models/${deploymentId}/deployment/_stop${
force ? '?force=true' : ''
}`;
const { body, status } = await esSupertest.post(url);
this.assertResponseStatusCode(200, status, body);
log.debug('> Trained model deployment stopped');
@ -1570,8 +1572,13 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
);
},
async importTrainedModel(modelId: string, modelName: SupportedTrainedModelNamesType) {
await this.createTrainedModel(modelId, this.getTrainedModelConfig(modelName));
async importTrainedModel(
modelId: string,
modelName: SupportedTrainedModelNamesType,
config?: PutTrainedModelConfig
) {
const trainedModelConfig = config ?? this.getTrainedModelConfig(modelName);
await this.createTrainedModel(modelId, trainedModelConfig);
await this.createTrainedModelVocabularyES(modelId, this.getTrainedModelVocabulary(modelName));
await this.uploadTrainedModelDefinitionES(
modelId,

View file

@ -8,6 +8,7 @@
import { mapValues } from 'lodash';
import path from 'path';
import { createTestConfig, CreateTestConfig } from '../common/config';
import { SUPPORTED_TRAINED_MODELS } from '../../functional/services/ml/api';
export const observabilityAIAssistantDebugLogger = {
name: 'plugins.observabilityAIAssistant',
@ -30,6 +31,7 @@ export const observabilityAIAssistantFtrConfigs = {
__dirname,
'../../../../test/analytics/plugins/analytics_ftr_helpers'
),
'xpack.observabilityAIAssistant.modelId': SUPPORTED_TRAINED_MODELS.TINY_ELSER.name,
},
},
};

View file

@ -0,0 +1,31 @@
/*
* 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 { MachineLearningProvider } from '../../../api_integration/services/ml';
import { SUPPORTED_TRAINED_MODELS } from '../../../functional/services/ml/api';
export const TINY_ELSER = {
...SUPPORTED_TRAINED_MODELS.TINY_ELSER,
id: SUPPORTED_TRAINED_MODELS.TINY_ELSER.name,
};
export async function createKnowledgeBaseModel(ml: ReturnType<typeof MachineLearningProvider>) {
const config = {
...ml.api.getTrainedModelConfig(TINY_ELSER.name),
input: {
field_names: ['text_field'],
},
};
await ml.api.importTrainedModel(TINY_ELSER.name, TINY_ELSER.id, config);
await ml.api.assureMlStatsIndexExists();
}
export async function deleteKnowledgeBaseModel(ml: ReturnType<typeof MachineLearningProvider>) {
await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true);
await ml.api.deleteTrainedModelES(TINY_ELSER.id);
await ml.api.cleanMlIndices();
await ml.testResources.cleanMLSavedObjects();
}

View file

@ -0,0 +1,257 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { createKnowledgeBaseModel, deleteKnowledgeBaseModel } from './helpers';
interface KnowledgeBaseEntry {
id: string;
text: string;
}
export default function ApiTest({ getService }: FtrProviderContext) {
const ml = getService('ml');
const es = getService('es');
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
const KB_INDEX = '.kibana-observability-ai-assistant-kb-*';
describe('Knowledge base', () => {
before(async () => {
await createKnowledgeBaseModel(ml);
});
after(async () => {
await deleteKnowledgeBaseModel(ml);
});
it('returns 200 on knowledge base setup', async () => {
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/setup',
})
.expect(200);
expect(res.body).to.eql({});
});
describe('when managing a single entry', () => {
const knowledgeBaseEntry = {
id: 'my-doc-id-1',
text: 'My content',
};
it('returns 200 on create', async () => {
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/save',
params: { body: knowledgeBaseEntry },
})
.expect(200);
const res = await observabilityAIAssistantAPIClient.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
params: {
query: {
query: '',
sortBy: 'doc_id',
sortDirection: 'asc',
},
},
});
const entry = res.body.entries[0];
expect(entry.id).to.equal(knowledgeBaseEntry.id);
expect(entry.text).to.equal(knowledgeBaseEntry.text);
});
it('returns 200 on get entries and entry exists', async () => {
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
params: {
query: {
query: '',
sortBy: 'doc_id',
sortDirection: 'asc',
},
},
})
.expect(200);
const entry = res.body.entries[0];
expect(entry.id).to.equal(knowledgeBaseEntry.id);
expect(entry.text).to.equal(knowledgeBaseEntry.text);
});
it('returns 200 on delete', async () => {
const entryId = 'my-doc-id-1';
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}',
params: {
path: { entryId },
},
})
.expect(200);
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
params: {
query: {
query: '',
sortBy: 'doc_id',
sortDirection: 'asc',
},
},
})
.expect(200);
expect(
res.body.entries.filter((entry: KnowledgeBaseEntry) => entry.id.startsWith('my-doc-id'))
.length
).to.eql(0);
});
it('returns 500 on delete not found', async () => {
const entryId = 'my-doc-id-1';
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}',
params: {
path: { entryId },
},
})
.expect(500);
});
});
describe('when managing multiple entries', () => {
before(async () => {
es.deleteByQuery({
index: KB_INDEX,
conflicts: 'proceed',
query: { match_all: {} },
});
});
afterEach(async () => {
es.deleteByQuery({
index: KB_INDEX,
conflicts: 'proceed',
query: { match_all: {} },
});
});
const knowledgeBaseEntries: KnowledgeBaseEntry[] = [
{
id: 'my_doc_a',
text: 'My content a',
},
{
id: 'my_doc_b',
text: 'My content b',
},
{
id: 'my_doc_c',
text: 'My content c',
},
];
it('returns 200 on create', async () => {
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import',
params: { body: { entries: knowledgeBaseEntries } },
})
.expect(200);
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
params: {
query: {
query: '',
sortBy: 'doc_id',
sortDirection: 'asc',
},
},
})
.expect(200);
expect(
res.body.entries.filter((entry: KnowledgeBaseEntry) => entry.id.startsWith('my_doc'))
.length
).to.eql(3);
});
it('allows sorting', async () => {
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import',
params: { body: { entries: knowledgeBaseEntries } },
})
.expect(200);
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
params: {
query: {
query: '',
sortBy: 'doc_id',
sortDirection: 'desc',
},
},
})
.expect(200);
const entries = res.body.entries.filter((entry: KnowledgeBaseEntry) =>
entry.id.startsWith('my_doc')
);
expect(entries[0].id).to.eql('my_doc_c');
expect(entries[1].id).to.eql('my_doc_b');
expect(entries[2].id).to.eql('my_doc_a');
// asc
const resAsc = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
params: {
query: {
query: '',
sortBy: 'doc_id',
sortDirection: 'asc',
},
},
})
.expect(200);
const entriesAsc = resAsc.body.entries.filter((entry: KnowledgeBaseEntry) =>
entry.id.startsWith('my_doc')
);
expect(entriesAsc[0].id).to.eql('my_doc_a');
expect(entriesAsc[1].id).to.eql('my_doc_b');
expect(entriesAsc[2].id).to.eql('my_doc_c');
});
it('allows searching', async () => {
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/entries/import',
params: { body: { entries: knowledgeBaseEntries } },
})
.expect(200);
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
params: {
query: {
query: 'my_doc_a',
sortBy: 'doc_id',
sortDirection: 'asc',
},
},
})
.expect(200);
expect(res.body.entries.length).to.eql(1);
expect(res.body.entries[0].id).to.eql('my_doc_a');
});
});
});
}

View file

@ -1,226 +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 expect from '@kbn/expect';
import getPort from 'get-port';
import http, { Server } from 'http';
import { FtrProviderContext } from '../../common/ftr_provider_context';
/*
This test is disabled because the Knowledge base requires a trained model (ELSER)
which is not available in FTR tests.
When a comparable, less expensive trained model is available, this test should be re-enabled.
*/
export default function ApiTest({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const KNOWLEDGE_BASE_API_URL = `/internal/observability_ai_assistant/kb`;
describe('Knowledge base', () => {
const requestHandler = (
request: http.IncomingMessage,
response: http.ServerResponse<http.IncomingMessage> & { req: http.IncomingMessage }
) => {};
let server: Server;
before(async () => {
const port = await getPort({ port: getPort.makeRange(9000, 9100) });
server = http
.createServer((request, response) => {
requestHandler(request, response);
})
.listen(port);
});
after(() => {
server.close();
});
it('should be possible to set up the knowledge base', async () => {
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/setup`)
.set('kbn-xsrf', 'foo')
.expect(200)
.then((response) => {
expect(response.body).to.eql({ entries: [] });
});
});
describe('when creating a single entry', () => {
it('returns a 200 when using the right payload', async () => {
const knowledgeBaseEntry = {
id: 'my-doc-id-1',
text: 'My content',
};
await supertest
.post(`${KNOWLEDGE_BASE_API_URL}/entries/save`)
.set('kbn-xsrf', 'foo')
.send(knowledgeBaseEntry)
.expect(200);
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/entries?query=&sortBy=doc_id&sortDirection=asc`)
.set('kbn-xsrf', 'foo')
.expect(200)
.then((response) => {
expect(response.body).to.eql({ entries: [knowledgeBaseEntry] });
});
});
it('returns a 500 when using the wrong payload', async () => {
const knowledgeBaseEntry = {
foo: 'my-doc-id-1',
};
await supertest
.post(`${KNOWLEDGE_BASE_API_URL}/entries/save`)
.set('kbn-xsrf', 'foo')
.send(knowledgeBaseEntry)
.expect(500);
});
});
describe('when importing multiple entries', () => {
it('returns a 200 when using the right payload', async () => {
const knowledgeBaseEntries = [
{
id: 'my-doc-id-2',
text: 'My content 2',
},
{
id: 'my-doc-id-3',
text: 'My content 3',
},
];
await supertest
.post(`${KNOWLEDGE_BASE_API_URL}/entries/import`)
.set('kbn-xsrf', 'foo')
.send(knowledgeBaseEntries)
.expect(200);
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/entries?query=&sortBy=doc_id&sortDirection=asc`)
.set('kbn-xsrf', 'foo')
.expect(200)
.then((response) => {
expect(response.body).to.eql({ entries: knowledgeBaseEntries });
});
});
it('returns a 500 when using the wrong payload', async () => {
const knowledgeBaseEntry = {
foo: 'my-doc-id-1',
};
await supertest
.post(`${KNOWLEDGE_BASE_API_URL}/entries/import`)
.set('kbn-xsrf', 'foo')
.send(knowledgeBaseEntry)
.expect(500);
});
});
describe('when deleting an entry', () => {
it('returns a 200 when the item is found and the item is deleted', async () => {
await supertest
.delete(`${KNOWLEDGE_BASE_API_URL}/entries/delete/my-doc-id-2`)
.set('kbn-xsrf', 'foo')
.expect(200);
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/entries?query=&sortBy=doc_id&sortDirection=asc`)
.set('kbn-xsrf', 'foo')
.expect(200)
.then((response) => {
expect(response.body).to.eql({
entries: [
{
id: 'my-doc-id-1',
text: 'My content 1',
confidence: 'high',
},
{
id: 'my-doc-id-3',
text: 'My content 3',
},
],
});
});
});
it('returns a 500 when the item is not found ', async () => {
return await supertest
.delete(`${KNOWLEDGE_BASE_API_URL}/entries/delete/my-doc-id-2`)
.set('kbn-xsrf', 'foo')
.expect(500);
});
});
describe('when retrieving entries', () => {
it('returns a 200 when calling get entries with the right parameters', async () => {
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/entries?query=&sortBy=doc_id&sortDirection=asc`)
.set('kbn-xsrf', 'foo')
.expect(200)
.then((response) => {
expect(response.body).to.eql({ entries: [] });
});
});
it('allows sorting', async () => {
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/entries?query=&sortBy=doc_id&sortDirection=desc`)
.set('kbn-xsrf', 'foo')
.expect(200)
.then((response) => {
expect(response.body).to.eql({
entries: [
{
id: 'my-doc-id-3',
text: 'My content 3',
},
{
id: 'my-doc-id-1',
text: 'My content 1',
},
],
});
});
});
it('allows searching', async () => {
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/entries?query=my-doc-3&sortBy=doc_id&sortDirection=asc`)
.set('kbn-xsrf', 'foo')
.expect(200)
.then((response) => {
expect(response.body).to.eql({
entries: [
{
id: 'my-doc-id-3',
text: 'My content 3',
},
],
});
});
});
it('returns a 500 when calling get entries with the wrong parameters', async () => {
return supertest
.get(`${KNOWLEDGE_BASE_API_URL}/entries`)
.set('kbn-xsrf', 'foo')
.expect(500);
});
});
});
}

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { deleteKnowledgeBaseModel, createKnowledgeBaseModel } from './helpers';
export default function ApiTest({ getService }: FtrProviderContext) {
const ml = getService('ml');
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
describe('/internal/observability_ai_assistant/kb/setup', () => {
it('returns empty object when successful', async () => {
await createKnowledgeBaseModel(ml);
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/setup',
})
.expect(200);
expect(res.body).to.eql({});
await deleteKnowledgeBaseModel(ml);
});
it('returns bad request if model cannot be installed', async () => {
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/setup',
})
.expect(400);
});
});
}

View file

@ -0,0 +1,55 @@
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { deleteKnowledgeBaseModel, createKnowledgeBaseModel, TINY_ELSER } from './helpers';
export default function ApiTest({ getService }: FtrProviderContext) {
const ml = getService('ml');
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
describe('/internal/observability_ai_assistant/kb/status', () => {
before(async () => {
await createKnowledgeBaseModel(ml);
await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'POST /internal/observability_ai_assistant/kb/setup',
})
.expect(200);
});
after(async () => {
await deleteKnowledgeBaseModel(ml);
});
it('returns correct status after knowledge base is setup', async () => {
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/status',
})
.expect(200);
expect(res.body.deployment_state).to.eql('started');
expect(res.body.model_name).to.eql(TINY_ELSER.id);
});
it('returns correct status after elser is stopped', async () => {
await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true);
const res = await observabilityAIAssistantAPIClient
.editorUser({
endpoint: 'GET /internal/observability_ai_assistant/kb/status',
})
.expect(200);
expect(res.body).to.eql({
ready: false,
model_name: TINY_ELSER.id,
});
});
});
}