Skip to content

Commit

Permalink
Merge branch 'main' into issues-171287
Browse files Browse the repository at this point in the history
  • Loading branch information
angorayc authored Apr 12, 2024
2 parents e34c9a1 + 9195b22 commit 9e7bf1f
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 44 deletions.
6 changes: 2 additions & 4 deletions x-pack/plugins/observability_solution/apm/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,15 @@
"customIntegrations", // Move this to requiredPlugins after completely migrating from the Tutorials Home App
"licenseManagement",
"profilingDataAccess",
"cases",
"observabilityAIAssistant"
"cases"
],
"requiredBundles": [
"fleet",
"kibanaReact",
"kibanaUtils",
"ml",
"observability",
"maps",
"observabilityAIAssistant"
"maps"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import LatencyChart from './latency_chart';
import ThroughputChart from './throughput_chart';
import { AlertDetailsAppSectionProps } from './types';
import { createCallApmApi } from '../../../../services/rest/create_call_apm_api';
import { AlertDetailContextualInsights } from './alert_details_contextual_insights';

export function AlertDetailsAppSection({
rule,
Expand Down Expand Up @@ -167,8 +166,6 @@ export function AlertDetailsAppSection({

return (
<EuiFlexGroup direction="column" gutterSize="s">
<AlertDetailContextualInsights alert={alert} />

<TimeRangeMetadataContextProvider
start={from}
end={to}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,11 @@ export default function AlertDetailsAppSection({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.search.searchSource]);

const overview = !!ruleParams.criteria ? (
if (!ruleParams.criteria) {
return null;
}

return (
<EuiFlexGroup direction="column" data-test-subj="thresholdAlertOverviewSection">
{ruleParams.criteria.map((criterion, index) => (
<EuiFlexItem key={`criterion-${index}`}>
Expand Down Expand Up @@ -329,7 +333,5 @@ export default function AlertDetailsAppSection({
)}
<AlertHistoryChart alert={alert} dataView={dataView} rule={rule} />
</EuiFlexGroup>
) : null;

return overview;
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import dedent from 'dedent';
import { AlertFieldsTable } from '@kbn/alerts-ui-shared';
import { css } from '@emotion/react';
import { omit } from 'lodash';
import { useKibana } from '../../utils/kibana_react';
import { useFetchRule } from '../../hooks/use_fetch_rule';
import { usePluginContext } from '../../hooks/use_plugin_context';
Expand All @@ -43,6 +44,7 @@ import { isAlertDetailsEnabledPerApp } from '../../utils/is_alert_details_enable
import { observabilityFeatureId } from '../../../common';
import { paths } from '../../../common/locators/paths';
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
import { AlertDetailContextualInsights } from './alert_details_contextual_insights';

interface AlertDetailsPathParams {
alertId: string;
Expand Down Expand Up @@ -95,7 +97,7 @@ export function AlertDetails() {
{
name: 'alert_fields',
description: 'The fields and values for the alert',
value: alertDetail.formatted.fields,
value: getRelevantAlertFields(alertDetail),
},
],
});
Expand Down Expand Up @@ -169,6 +171,9 @@ export function AlertDetails() {
<>
<EuiSpacer size="l" />
<AlertSummary alertSummaryFields={summaryFields} />

<AlertDetailContextualInsights alert={alertDetail} />
<EuiSpacer size="l" />
{AlertDetailsAppSection && rule && alertDetail?.formatted && (
<AlertDetailsAppSection
alert={alertDetail.formatted}
Expand Down Expand Up @@ -259,13 +264,27 @@ export function getScreenDescription(alertDetail: AlertData) {
}
The alert details are:
${Object.entries(alertDetail.formatted.fields)
.map(([key, value]) => `${key}: ${value}`)
${Object.entries(getRelevantAlertFields(alertDetail))
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('\n')}
Do not repeat this information to the user, unless it is relevant for them to know.
Please suggestion root causes if possilbe.
Please suggestion root causes if possible.
Suggest next steps for the user to take.
`);
}

function getRelevantAlertFields(alertDetail: AlertData) {
return omit(alertDetail.formatted.fields, [
'kibana.alert.rule.revision',
'kibana.alert.rule.execution.uuid',
'kibana.alert.flapping_history',
'kibana.alert.uuid',
'kibana.alert.rule.uuid',
'event.action',
'event.kind',
'kibana.alert.rule.tags',
'kibana.alert.maintenance_window_ids',
'kibana.alert.consecutive_matches',
]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import dedent from 'dedent';
import { useKibana } from '../../utils/kibana_react';
import { AlertData } from '../../hooks/use_fetch_alert_detail';

export function AlertDetailContextualInsights({ alert }: { alert: AlertData | null }) {
const {
services: { observabilityAIAssistant },
} = useKibana();

const ObservabilityAIAssistantContextualInsight =
observabilityAIAssistant?.ObservabilityAIAssistantContextualInsight;

const messages = useMemo(() => {
if (!observabilityAIAssistant) {
return null;
}

return observabilityAIAssistant.getContextualInsightMessages({
message: `I'm looking at an alert and trying to understand why it was triggered`,
instructions: dedent(
`I'm an SRE. I am looking at an alert that was triggered. I want to understand why it was triggered, what it means, and what I should do next.`
),
});
}, [observabilityAIAssistant]);

if (!ObservabilityAIAssistantContextualInsight || !messages) {
return null;
}

return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem grow={false}>
<ObservabilityAIAssistantContextualInsight
title={i18n.translate(
'xpack.observability.alertDetailContextualInsights.InsightButtonLabel',
{ defaultMessage: 'Help me understand this alert' }
)}
messages={messages}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ describe('Observability AI Assistant client', () => {
});

stream = observableIntoStream(
await client.complete({
client.complete({
connectorId: 'foo',
messages: [system('This is a system message'), user('How many alerts do I have?')],
functionClient: functionClientMock,
Expand Down Expand Up @@ -1358,6 +1358,83 @@ describe('Observability AI Assistant client', () => {
});
});

describe('when context has not been injected since last user message', () => {
let dataHandler: jest.Mock;

beforeEach(async () => {
client = createClient();
actionsClientMock.execute.mockImplementationOnce(async () => {
llmSimulator = createLlmSimulator();
return {
actionId: '',
status: 'ok',
data: llmSimulator.stream,
};
});

functionClientMock.hasFunction.mockReturnValue(true);
functionClientMock.executeFunction.mockImplementationOnce(async () => {
return {
content: [
{
id: 'my_document',
text: 'My document',
},
],
};
});

const stream = observableIntoStream(
await client.complete({
connectorId: 'foo',
messages: [system('This is a system message'), user('How many alerts do I have?')],
functionClient: functionClientMock,
signal: new AbortController().signal,
persist: false,
})
);

dataHandler = jest.fn();

stream.on('data', dataHandler);

await waitForNextWrite(stream);

await llmSimulator.next({
content: 'Hello',
});

await llmSimulator.complete();

await finished(stream);
});

it('executes the context function', async () => {
expect(functionClientMock.executeFunction).toHaveBeenCalledWith(
expect.objectContaining({ name: 'context' })
);
});

it('appends the context request message', async () => {
expect(JSON.parse(dataHandler.mock.calls[0])).toEqual({
type: StreamingChatResponseEventType.MessageAdd,
id: expect.any(String),
message: {
'@timestamp': expect.any(String),
message: {
content: '',
role: MessageRole.Assistant,
function_call: {
name: 'context',
arguments: JSON.stringify({ queries: [], categories: [] }),
trigger: MessageRole.Assistant,
},
},
},
});
});
});

describe('when the function response exceeds the max no of tokens for one', () => {
let stream: Readable;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Logger } from '@kbn/logging';
import type { PublicMethodsOf } from '@kbn/utility-types';
import apm from 'elastic-apm-node';
import { decode, encode } from 'gpt-tokenizer';
import { last, merge, noop, omit, pick, take } from 'lodash';
import { findLastIndex, last, merge, noop, omit, pick, take } from 'lodash';
import {
filter,
isObservable,
Expand Down Expand Up @@ -211,20 +211,24 @@ export class ObservabilityAIAssistantClient {

const next = async (nextMessages: Message[]): Promise<void> => {
const lastMessage = last(nextMessages);

const isUserMessage = lastMessage?.message.role === MessageRole.User;

const isUserMessageWithoutFunctionResponse = isUserMessage && !lastMessage?.message.name;
const indexOfLastUserMessage = findLastIndex(
nextMessages,
({ message }) => message.role === MessageRole.User && !message.name
);

const contextFirst =
isUserMessageWithoutFunctionResponse && functionClient.hasFunction('context');
const hasNoContextRequestAfterLastUserMessage =
indexOfLastUserMessage !== -1 &&
nextMessages
.slice(indexOfLastUserMessage)
.every(({ message }) => message.function_call?.name !== 'context');

const isAssistantMessageWithFunctionRequest =
lastMessage?.message.role === MessageRole.Assistant &&
!!lastMessage?.message.function_call?.name;
const shouldInjectContext =
functionClient.hasFunction('context') && hasNoContextRequestAfterLastUserMessage;

if (contextFirst) {
const addedMessage = {
if (shouldInjectContext) {
const contextFunctionRequest = {
'@timestamp': new Date().toISOString(),
message: {
role: MessageRole.Assistant,
Expand All @@ -243,26 +247,26 @@ export class ObservabilityAIAssistantClient {
subscriber.next({
type: StreamingChatResponseEventType.MessageAdd,
id: v4(),
message: addedMessage,
message: contextFunctionRequest,
});

return await next(nextMessages.concat(addedMessage));
return await next(nextMessages.concat(contextFunctionRequest));
} else if (isUserMessage) {
const functions =
numFunctionsCalled === MAX_FUNCTION_CALLS ? [] : allFunctions.concat(allActions);

const spanName =
lastMessage.message.name && lastMessage.message.name !== 'context'
? 'function_response'
: 'user_message';

const response$ = (
await chatWithTokenCountIncrement(
lastMessage.message.name && lastMessage.message.name !== 'context'
? 'function_response'
: 'user_message',
{
messages: nextMessages,
connectorId,
signal,
functions,
}
)
await chatWithTokenCountIncrement(spanName, {
messages: nextMessages,
connectorId,
signal,
functions,
})
).pipe(emitWithConcatenatedMessage(), shareReplay());

response$.subscribe({
Expand All @@ -286,9 +290,10 @@ export class ObservabilityAIAssistantClient {
);
}

if (isAssistantMessageWithFunctionRequest) {
const functionCallName = lastMessage.message.function_call!.name;
const functionCallName = lastMessage?.message.function_call?.name;
const isAssistantMessage = lastMessage?.message.role === MessageRole.Assistant;

if (isAssistantMessage && functionCallName) {
if (functionClient.hasAction(functionCallName)) {
this.dependencies.logger.debug(`Executing client-side action: ${functionCallName}`);

Expand Down
12 changes: 12 additions & 0 deletions x-pack/plugins/search_notebooks/README.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Search Notebooks plugin

This plugin contains endpoints and components for rendering search python notebooks in the persistent dev console.

### Cached Notebooks

There is a limited set of search notebooks that we cache in `/server/data` directory to be served from this plugin. These should be available in all environments.

To update the cached notebooks, run the following command:

```shell
./scripts/download-notebooks.sh
```

This script reads the `scripts/notebooks.txt` file and downloads each notebook from the url in the file and then saves it in the `server/data` directory with a snake_case filename and `.json` extension. The `.json` extension is just to make loading the files with node.js easier using an `async require()` instead of writing custom loading code with `fs` and implementing in-memory caching with the same loading code.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"source": [
"# Semantic search quick start\n",
"\n",
"<a target=\"_blank\" href=\"https://colab.research.google.com/github/elastic/elasticsearch-labs/blob/main/notebooks/search/00-quick-start.ipynb\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/elastic/elasticsearch-labs/blob/main/notebooks/search/00-quick-start.ipynb)\n",
"\n",
"This interactive notebook will introduce you to some basic operations with Elasticsearch, using the official [Elasticsearch Python client](https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/connecting.html).\n",
"You'll perform semantic search using [Sentence Transformers](https://www.sbert.net) for text embedding. Learn how to integrate traditional text-based search with semantic search, for a hybrid search system."
Expand Down
Loading

0 comments on commit 9e7bf1f

Please sign in to comment.