Prometheus: Query advisor added copy for preview (#77903)

* add alert with link to docs

* remove historical secondary text

* add tooltip to opening button to suggest selecting a metric

* fix merge conflict issue

* add llm check for tooltip with docs

* update starting message copy and alert copy

* Update public/app/plugins/datasource/prometheus/querybuilder/components/promQail/PromQail.tsx

Co-authored-by: Rob Whelan <github@jtheory.com>

* Update public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx

Co-authored-by: Rob Whelan <github@jtheory.com>

* add check for vector for button, remove check in helpers

* clean a little, fix tests

* refactor out query assistant button

* fix styles

---------

Co-authored-by: Rob Whelan <github@jtheory.com>
This commit is contained in:
Brendan O'Handley 2023-12-28 11:37:27 -06:00 committed by GitHub
parent 05fb9b5ecb
commit 83eb831a8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 50 deletions

View File

@ -1,10 +1,10 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { DataSourceApi, PanelData } from '@grafana/data';
import { EditorRow } from '@grafana/experimental';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, Drawer } from '@grafana/ui';
import { config } from '@grafana/runtime';
import { Drawer } from '@grafana/ui';
import { PrometheusDatasource } from '../../datasource';
import promqlGrammar from '../../promql';
@ -23,7 +23,8 @@ import { MetricsLabelsSection } from './MetricsLabelsSection';
import { NestedQueryList } from './NestedQueryList';
import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained';
import { PromQail } from './promQail/PromQail';
import AI_Logo_color from './promQail/resources/AI_Logo_color.svg';
import { QueryAssistantButton } from './promQail/QueryAssistantButton';
import { isLLMPluginEnabled } from './promQail/state/helpers';
export interface Props {
query: PromVisualQuery;
@ -42,15 +43,24 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
const [showDrawer, setShowDrawer] = useState<boolean>(false);
const [llmAppEnabled, updateLlmAppEnabled] = useState<boolean>(false);
const lang = { grammar: promqlGrammar, name: 'promql' };
const initHints = datasource.getInitHints();
useEffect(() => {
async function checkLlms() {
const check = await isLLMPluginEnabled();
updateLlmAppEnabled(check);
}
checkLlms();
}, []);
return (
<>
{prometheusPromQAIL && showDrawer && (
<Drawer scrollableContent={true} closeOnMaskClick={false} onClose={() => setShowDrawer(false)}>
<Drawer closeOnMaskClick={false} onClose={() => setShowDrawer(false)}>
<PromQail
query={query}
closeDrawer={() => setShowDrawer(false)}
@ -98,20 +108,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
padding: '0 0 0 6px',
})}
>
<Button
variant={'secondary'}
onClick={() => {
reportInteraction('grafana_prometheus_promqail_ai_button_clicked', {
metric: query.metric,
});
setShowDrawer(true);
}}
title={'Get query suggestions.'}
disabled={!query.metric}
>
<img height={16} src={AI_Logo_color} alt="AI logo black and white" />
{'\u00A0'}Get query suggestions
</Button>
<QueryAssistantButton llmAppEnabled={llmAppEnabled} metric={query.metric} setShowDrawer={setShowDrawer} />
</div>
)}
<QueryBuilderHints<PromVisualQuery>

View File

@ -3,7 +3,7 @@ import React, { useEffect, useReducer, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Button, Checkbox, Input, Spinner, useTheme2 } from '@grafana/ui';
import { Alert, Button, Checkbox, Input, Spinner, useTheme2 } from '@grafana/ui';
import store from 'app/core/store';
import { PrometheusDatasource } from '../../../datasource';
@ -83,24 +83,30 @@ export const PromQail = (props: PromQailProps) => {
</div>
{state.showStartingMessage ? (
<>
<div className={styles.textPadding}>
This assistant can suggest queries based on your use case and the metric you want to query
</div>
<div className={styles.textPadding}>
The assistant will connect to OpenAI using your API key. The following information will be sent to OpenAI:
</div>
<div className={styles.dataList}>
<ul>
<li>Metrics</li>
<li>Labels</li>
<li>Metrics metadata</li>
</ul>
</div>
<div className={styles.textPadding}>Check with OpenAI to understand how your data is being used.</div>
<div>
AI-suggested queries may not always be the right one for your use case. Always take a moment to understand
the queries before using them.
<ol>
<li className={styles.textPadding}>
Query Advisor suggests queries based on a metric and requests you type in.
</li>
<li className={styles.textPadding}>
Query Advisor sends Prometheus metrics, labels and metadata to the LLM provider you&#39;ve configured.
Be sure to align its usage with your company&#39;s internal policies.
</li>
<li className={styles.textPadding}>
An AI-suggested query may not fully answer your question. Always take a moment to understand a query
before you use it.
</li>
</ol>
</div>
<Alert
title={''}
severity={'info'}
key={'promqail-llm-app'}
className={cx(styles.textPadding, styles.noMargin)}
>
Query Advisor is currently in Private Preview. Feedback is appreciated and can be provided on explanations
and suggestions.
</Alert>
{/* don't show this message again, store in localstorage */}
<div className={styles.textPadding}>
@ -409,7 +415,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
marginRight: '10px',
}),
dataList: css({
padding: '0px 28px 28px 28px',
padding: '0px 28px 0px 28px',
}),
textPadding: css({
paddingBottom: '12px',
@ -538,6 +544,21 @@ export const getStyles = (theme: GrafanaTheme2) => {
submitFeedback: css({
padding: '16px 0',
}),
noMargin: css({
margin: 0,
}),
enableButtonTooltip: css({
padding: 8,
}),
enableButtonTooltipText: css({
color: `${theme.colors.text.secondary}`,
ul: {
marginLeft: 16,
},
}),
link: css({
color: `${theme.colors.text.link} !important`,
}),
};
};

View File

@ -0,0 +1,51 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { QueryAssistantButton } from './QueryAssistantButton';
const setShowDrawer = jest.fn(() => {});
describe('QueryAssistantButton', () => {
it('renders the button', async () => {
const props = createProps(true, 'metric', setShowDrawer);
render(<QueryAssistantButton {...props} />);
expect(screen.getByText('Get query suggestions')).toBeInTheDocument();
});
it('shows the LLM app disabled message when LLM app is not set up with vector DB', async () => {
const props = createProps(false, 'metric', setShowDrawer);
render(<QueryAssistantButton {...props} />);
const button = screen.getByText('Get query suggestions');
userEvent.hover(button);
await waitFor(() => {
expect(screen.getByText('Install and enable the LLM plugin')).toBeInTheDocument();
});
});
it('shows the message to select a metric when LLM is enabled and no metric is selected', async () => {
const props = createProps(true, '', setShowDrawer);
render(<QueryAssistantButton {...props} />);
const button = screen.getByText('Get query suggestions');
userEvent.hover(button);
await waitFor(() => {
expect(screen.getByText('First, select a metric.')).toBeInTheDocument();
});
});
it('calls setShowDrawer when button is clicked', async () => {
const props = createProps(true, 'metric', setShowDrawer);
render(<QueryAssistantButton {...props} />);
const button = screen.getByText('Get query suggestions');
fireEvent.click(button);
expect(setShowDrawer).toHaveBeenCalled();
});
});
function createProps(llmAppEnabled: boolean, metric: string, setShowDrawer: () => void) {
return {
llmAppEnabled,
metric,
setShowDrawer,
};
}

View File

@ -0,0 +1,85 @@
import React from 'react';
import { reportInteraction } from '@grafana/runtime';
import { Button, Tooltip, useTheme2 } from '@grafana/ui';
import { getStyles } from './PromQail';
import AI_Logo_color from './resources/AI_Logo_color.svg';
export type Props = {
llmAppEnabled: boolean;
metric: string;
setShowDrawer: (show: boolean) => void;
};
export function QueryAssistantButton(props: Props) {
const { llmAppEnabled, metric, setShowDrawer } = props;
const llmAppDisabled = !llmAppEnabled;
const noMetricSelected = !metric;
const theme = useTheme2();
const styles = getStyles(theme);
const button = () => {
return (
<Button
variant={'secondary'}
onClick={() => {
reportInteraction('grafana_prometheus_promqail_ai_button_clicked', {
metric: metric,
});
setShowDrawer(true);
}}
disabled={!metric || !llmAppEnabled}
>
<img height={16} src={AI_Logo_color} alt="AI logo black and white" />
{'\u00A0'}Get query suggestions
</Button>
);
};
const selectMetricMessage = (
<Tooltip content={'First, select a metric.'} placement={'bottom-end'}>
{button()}
</Tooltip>
);
const llmAppMessage = (
<Tooltip
interactive={true}
placement={'auto-end'}
content={
<div className={styles.enableButtonTooltip}>
<h6>Query Advisor is disabled</h6>
<div className={styles.enableButtonTooltipText}>To enable Query Advisor you must:</div>
<div className={styles.enableButtonTooltipText}>
<ul>
<li>
<a
href={'https://grafana.com/docs/grafana-cloud/alerting-and-irm/machine-learning/llm-plugin/'}
target="_blank"
rel="noreferrer noopener"
className={styles.link}
>
Install and enable the LLM plugin
</a>
</li>
<li>Select a metric</li>
</ul>
</div>
</div>
}
>
{button()}
</Tooltip>
);
if (llmAppDisabled) {
return llmAppMessage;
} else if (noMetricSelected) {
return selectMetricMessage;
} else {
return button();
}
}

View File

@ -31,7 +31,6 @@ export function QuerySuggestionContainer(props: Props) {
if (suggestionType === SuggestionType.Historical) {
text = `Here are ${querySuggestions.length} query suggestions:`;
secondaryText = 'These queries are based off of historical data (top used queries) for your metric.';
refineText = 'I want to write a prompt';
} else if (suggestionType === SuggestionType.AI) {
text = text = 'Here is your query suggestion:';
@ -42,8 +41,15 @@ export function QuerySuggestionContainer(props: Props) {
return (
<>
<div className={styles.textPadding}>{text}</div>
<div className={cx(styles.secondaryText, styles.bottomMargin)}>{secondaryText}</div>
{suggestionType === SuggestionType.Historical ? (
<div className={styles.bottomMargin}>{text}</div>
) : (
<>
<div className={styles.textPadding}>{text}</div>
<div className={cx(styles.secondaryText, styles.bottomMargin)}>{secondaryText}</div>
</>
)}
<div className={styles.infoContainerWrapper}>
<div className={styles.infoContainer}>
{querySuggestions.map((qs: QuerySuggestion, idx: number) => {

View File

@ -107,11 +107,6 @@ export async function promQailExplain(
suggIdx: number,
datasource: PrometheusDatasource
) {
// const enabled = await llms.openai.enabled();
// if (!enabled) {
// return false;
// }
const suggestedQuery = interaction.suggestions[suggIdx].query;
const promptMessages = getExplainMessage(suggestedQuery, query.metric, datasource);
@ -273,6 +268,7 @@ function guessMetricFamily(metric: string): string {
/**
* Check if the LLM plugin is enabled.
* Used in the PromQueryBuilder to enable/disable the button based on openai and vector db checks
* @returns true if the LLM plugin is enabled.
*/
export async function isLLMPluginEnabled(): Promise<boolean> {
@ -302,10 +298,6 @@ export async function promQailSuggest(
datasource: PrometheusDatasource,
interaction?: Interaction
) {
// when you're not running promqail
// @ts-ignore llms types issue
const check = await isLLMPluginEnabled();
const interactionToUpdate = interaction ? interaction : createInteraction(SuggestionType.Historical);
// Decide metric type
@ -329,7 +321,7 @@ export async function promQailSuggest(
metricType = guessMetricType(query.metric, datasource.languageProvider.metrics);
}
if (!check || interactionToUpdate.suggestionType === SuggestionType.Historical) {
if (interactionToUpdate.suggestionType === SuggestionType.Historical) {
return new Promise<void>((resolve) => {
return setTimeout(() => {
const suggestions = getTemplateSuggestions(