Skip to content

Commit

Permalink
[AI Assistant] Add to dashboard (#179329)
Browse files Browse the repository at this point in the history
Adds a new functionality in the AI assistant when in dashboards. If the
users ask for a question which will generate a query then then can use
prompts like:

- `Create a visualization from this query and add this to a dashboard`
- `Create a metric from this query and add this to a dashboard`
- ....


![meow](https://github.com/elastic/kibana/assets/17003240/3092f006-13ce-4565-b9d3-c6ad407afb31)


### How it works
- It uses the existing functionality of the assistant to create an ES|QL
query (if the generated query is wrong is not part of this PR)
- The LLM returns the query to the new `add_to_dashboard` function and
with the chart type (if the user has added the preference) and the
configuration needed for the ConfigBuilder it creates a Lens embeddable
and adds it to the dashboard.

### How to test
- Go to advanced settings, find the `Observability AI Assistant scope`
setting and change to Everywhere
- Go to a dahsboard (existing or new)
- Ask a question to the AI such as `I want the 95th percentile of ...
from ... index` or `I want the median of butes from the
kibana_sample_data_logs grouped by the top 5 destinations`
- After the ES|QL query has been generated correctly ask AI to create a
chart from this query and add this to the dashboard


### important note
As this is the first real consumer of the build api for ES|QL I have
fixed and various bugs I discovered in the api.

---------

Co-authored-by: Stratoula Kalafateli <[email protected]>
Co-authored-by: Stratoula Kalafateli <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
4 people authored Jul 9, 2024
1 parent e64102d commit 4013f60
Show file tree
Hide file tree
Showing 25 changed files with 493 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ test('generates gauge chart config', async () => {
"query": Object {
"esql": "from test | count=count()",
},
"timeField": undefined,
},
},
},
Expand Down Expand Up @@ -189,6 +190,7 @@ test('generates gauge chart config with goal and max', async () => {
"query": Object {
"esql": "from test | count=count() | eval max=1000 | eval goal=500",
},
"timeField": undefined,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ test('generates metric chart config', async () => {
"query": Object {
"esql": "from test | count=count() by @timestamp, category",
},
"timeField": undefined,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ test('generates metric chart config', async () => {
"query": Object {
"esql": "from test | count=count()",
},
"timeField": undefined,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ test('generates metric chart config', async () => {
"query": Object {
"esql": "from test | count=count() by @timestamp, category",
},
"timeField": undefined,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ test('generates region map chart config', async () => {
"query": Object {
"esql": "from test | count=count() by category",
},
"timeField": undefined,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ test('generates table config', async () => {
"query": Object {
"esql": "from test | count=count() by category",
},
"timeField": undefined,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ test('generates tag cloud chart config', async () => {
"query": Object {
"esql": "from test | count=count() by category",
},
"timeField": undefined,
},
},
},
Expand All @@ -123,8 +124,8 @@ test('generates tag cloud chart config', async () => {
"minFontSize": 12,
"orientation": "single",
"showLabel": true,
"tagAccessor": "category",
"valueAccessor": "count",
"tagAccessor": "metric_formula_accessor_breakdown",
"valueAccessor": "metric_formula_accessor",
},
},
"title": "test",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
buildDatasourceStates,
buildReferences,
getAdhocDataviews,
isFormulaDataset,
mapToFormula,
} from '../utils';
import { getBreakdownColumn, getFormulaColumn, getValueColumn } from '../columns';
Expand All @@ -31,18 +30,17 @@ function getAccessorName(type: 'breakdown') {

function buildVisualizationState(config: LensTagCloudConfig): TagcloudState {
const layer = config;
const isFormula = isFormulaDataset(config.dataset) || isFormulaDataset(layer.dataset);

return {
layerId: DEFAULT_LAYER_ID,
valueAccessor: !isFormula ? layer.value : ACCESSOR,
valueAccessor: ACCESSOR,
maxFontSize: 72,
minFontSize: 12,
orientation: 'single',
showLabel: true,
...(layer.breakdown
? {
tagAccessor: !isFormula ? (layer.breakdown as string) : getAccessorName('breakdown'),
tagAccessor: getAccessorName('breakdown'),
}
: {}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ test('generates xy chart config', async () => {
"query": Object {
"esql": "from test | count=count() by @timestamp",
},
"timeField": undefined,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class LensConfigBuilder {
async build(
config: LensConfig,
options: LensConfigOptions = {}
): Promise<LensAttributes | LensEmbeddableInput | undefined> {
): Promise<LensAttributes | LensEmbeddableInput> {
const { chartType } = config;
const chartConfig = await this.charts[chartType](config as any, {
formulaAPI: this.formulaAPI,
Expand All @@ -74,6 +74,6 @@ export class LensConfigBuilder {
} as LensEmbeddableInput;
}

return chartState;
return chartState as LensAttributes;
}
}
10 changes: 5 additions & 5 deletions packages/kbn-lens-embeddable-utils/config_builder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import type { FormulaPublicApi, TypedLensByValueInput } from '@kbn/lens-plugin/public';
import type { Filter, Query } from '@kbn/es-query';
import type { AggregateQuery, Filter, Query } from '@kbn/es-query';
import type { Datatable } from '@kbn/expressions-plugin/common';
import { DataViewsCommon } from './config_builder';

Expand Down Expand Up @@ -95,7 +95,7 @@ export interface LensConfigOptions {
/** optional time range override */
timeRange?: TimeRange;
filters?: Filter[];
query?: Query;
query?: Query | AggregateQuery;
}

export interface LensAxisTitleVisibilityConfig {
Expand Down Expand Up @@ -208,9 +208,9 @@ export type LensRegionMapConfig = Identity<
export interface LensMosaicConfigBase {
chartType: 'mosaic';
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown: LensBreakdownConfig;
breakdown: LensBreakdownConfig[];
/** field name to apply breakdown based on field type or full breakdown configuration */
xAxis: LensBreakdownConfig;
xAxis?: LensBreakdownConfig;
}

export type LensMosaicConfig = Identity<LensBaseConfig & LensBaseLayer & LensMosaicConfigBase>;
Expand All @@ -228,7 +228,7 @@ export type LensTableConfig = Identity<LensBaseConfig & LensBaseLayer & LensTabl
export interface LensHeatmapConfigBase {
chartType: 'heatmap';
/** field name to apply breakdown based on field type or full breakdown configuration */
breakdown: LensBreakdownConfig;
breakdown?: LensBreakdownConfig;
xAxis: LensBreakdownConfig;
legend?: Identity<LensLegendConfig>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ describe('buildDatasourceStates', () => {
"query": Object {
"esql": "from test | limit 10",
},
"timeField": undefined,
},
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-lens-embeddable-utils/config_builder/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ function buildDatasourceStatesLayer(
const newLayer = {
index: dataView!.id!,
query: { esql: (dataset as LensESQLDataset).esql } as AggregateQuery,
timeField: dataView!.timeFieldName,
columns,
allColumns: columns,
};
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/dashboard/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"usageCollection",
"taskManager",
"serverless",
"noDataPage"
"noDataPage",
"observabilityAIAssistant"
],
"requiredBundles": [
"kibanaReact",
Expand Down
11 changes: 10 additions & 1 deletion src/plugins/dashboard/public/dashboard_app/dashboard_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { loadDashboardHistoryLocationState } from './locator/load_dashboard_hist
import type { DashboardCreationOptions } from '../dashboard_container/embeddable/dashboard_container_factory';
import { DashboardTopNav } from '../dashboard_top_nav';
import { DashboardTabTitleSetter } from './tab_title_setter/dashboard_tab_title_setter';
import { useObservabilityAIAssistantContext } from './hooks/use_observability_ai_assistant_context';

export interface DashboardAppProps {
history: History;
Expand Down Expand Up @@ -82,13 +83,21 @@ export function DashboardApp({
embeddable: { getStateTransfer },
notifications: { toasts },
settings: { uiSettings },
data: { search },
data: { search, dataViews },
customBranding,
share: { url },
observabilityAIAssistant,
} = pluginServices.getServices();
const showPlainSpinner = useObservable(customBranding.hasCustomBranding$, false);
const { scopedHistory: getScopedHistory } = useDashboardMountContext();

useObservabilityAIAssistantContext({
observabilityAIAssistant: observabilityAIAssistant.start,
dashboardAPI,
search,
dataViews,
});

useExecutionContext(executionContext, {
type: 'application',
page: 'app',
Expand Down
Loading

0 comments on commit 4013f60

Please sign in to comment.