Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAI Prompt History #780

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,6 @@
"Ymin",
"llms",
"proxying",
""
"uuidv"
]
}
89 changes: 78 additions & 11 deletions src/components/QueryEditor/OpenAIEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import {
useStyles2,
TextArea,
HorizontalGroup,
Field,
Switch,
Tooltip,
CustomScrollbar,
Icon,
} from '@grafana/ui';
import { AdxDataSource } from 'datasource';
import React, { ChangeEvent, useEffect, useState } from 'react';
Expand All @@ -21,6 +26,7 @@ import { css } from '@emotion/css';
import { useAsync } from 'react-use';

import { getFunctions, getSignatureHelp } from './Suggestions';
import { PromptHistory } from './PromptHistory';

type Props = QueryEditorProps<AdxDataSource, KustoQuery, AdxDataSourceOptions>;

Expand All @@ -47,9 +53,20 @@ export const OpenAIEditor: React.FC<RawQueryEditorProps> = (props) => {
const [generatedQuery, setGeneratedQuery] = useState('//OpenAI generated query');
const [variables] = useState(getTemplateSrv().getVariables());
const [stateSchema, setStateSchema] = useState(cloneDeep(schema));
const [showQueryHistory, setShowQueryHistory] = useState(false);
const [parsedStoredPrompts, setParsedStoredPrompts] = useState([]);
const styles = useStyles2(getStyles);
const baselinePrompt = `You are an AI assistant that is fluent in KQL for querying Azure Data Explorer and you only respond with the correct KQL code snippets and no explanations. Generate a query that fulfills the following text.\nText:"""`;

useEffect(() => {
aangelisc marked this conversation as resolved.
Show resolved Hide resolved
const currentStoredPrompts = localStorage.getItem('storedOpenAIPrompts');
let allParsedStoredPrompts = [];
if (currentStoredPrompts !== null) {
allParsedStoredPrompts = JSON.parse(currentStoredPrompts);
};
alyssabull marked this conversation as resolved.
Show resolved Hide resolved
setParsedStoredPrompts(allParsedStoredPrompts);
}, [])

useAsync(async () => {
const enabled = await llms.openai.enabled();
setEnabled(enabled);
Expand All @@ -71,6 +88,18 @@ export const OpenAIEditor: React.FC<RawQueryEditorProps> = (props) => {
}
}, [schema, stateSchema]);

const addPromptToLocalStorage = () => {
let allPrompts;
if (parsedStoredPrompts.length > 0) {
allPrompts = [...parsedStoredPrompts, prompt];
} else {
allPrompts = [prompt];
};
alyssabull marked this conversation as resolved.
Show resolved Hide resolved
const stringifiedPrompts = JSON.stringify(allPrompts);
localStorage.setItem("storedOpenAIPrompts", stringifiedPrompts);
setParsedStoredPrompts(allPrompts);
};

const generateQuery = () => {
reportInteraction('grafana_ds_adx_openai_query_generated');
setWaiting(true);
Expand All @@ -83,7 +112,6 @@ export const OpenAIEditor: React.FC<RawQueryEditorProps> = (props) => {
.generateQueryForOpenAI(`${baselinePrompt}${prompt}"""`)
.then((resp) => {
setWaiting(false);
setGeneratedQuery(resp);
})
.catch((e) => {
setWaiting(false);
Expand Down Expand Up @@ -111,6 +139,10 @@ export const OpenAIEditor: React.FC<RawQueryEditorProps> = (props) => {
setGeneratedQuery(m);
},
complete: () => {
if (prompt !== "") {
addPromptToLocalStorage();
setPrompt('');
};
setWaiting(false);
},
error: (e) => {
Expand Down Expand Up @@ -175,7 +207,14 @@ export const OpenAIEditor: React.FC<RawQueryEditorProps> = (props) => {

if (!stateSchema) {
return null;
}
};

const useStoredPrompt = (prompt: string) => {
setShowQueryHistory(false);
setPrompt(prompt);
onRawQueryChange(prompt);
onRunQuery();
};

return (
<div>
Expand Down Expand Up @@ -221,13 +260,37 @@ export const OpenAIEditor: React.FC<RawQueryEditorProps> = (props) => {
>
{isWaiting && <Spinner className={styles.spinnerSpace} inline={true} />} Generate query
</Button>
<Field label="Show OpenAI history" style={{ display: 'flex', flexDirection: 'row', margin: '5px' }}>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', justifyContent: 'flex-start', margin: '0 10px 0 5px'}}>
<Tooltip
content={'View past generated prompts that are stored on your local browser.'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
content={'View past generated prompts that are stored on your local browser.'}
content={'View previously generated prompts.'}

placement="bottom-end"
>
<Icon name="info-circle" size="sm" className={styles.settingsIcon} />
</Tooltip>
</div>
<Switch value={showQueryHistory} onChange={() => setShowQueryHistory(!showQueryHistory)}></Switch>
</div>
</Field>
</HorizontalGroup>
<TextArea
data-testid={selectors.components.queryEditor.codeEditor.openAI}
value={prompt}
onChange={onPromptChange}
className={styles.innerMargin}
></TextArea>
{showQueryHistory ? (
<CustomScrollbar autoHeightMax="300px">
<PromptHistory
parsedStoredPrompts={parsedStoredPrompts}
useStoredPrompt={useStoredPrompt}
setParsedStoredPrompts={setParsedStoredPrompts}
setShowQueryHistory={setShowQueryHistory}
/>
</CustomScrollbar>
) : (
<TextArea
data-testid={selectors.components.queryEditor.codeEditor.openAI}
value={prompt}
onChange={onPromptChange}
className={styles.textArea}
></TextArea>
)}
</div>
<div className={styles.dividerSpace}>
<HorizontalGroup justify="flex-start" align="flex-start">
Expand Down Expand Up @@ -274,9 +337,6 @@ const getStyles = (theme: GrafanaTheme2) => {
color: theme.colors.text.link,
textDecoration: 'underline',
}),
innerMargin: css({
marginTop: theme.spacing(2),
}),
editorSpace: css({
paddingTop: theme.spacing(1),
}),
Expand All @@ -289,5 +349,12 @@ const getStyles = (theme: GrafanaTheme2) => {
dividerSpace: css({
marginTop: theme.spacing(4),
}),
textArea: css({
marginTop: theme.spacing(2),
height: '100px'
}),
settingsIcon: css`
color: ${theme.colors.text.secondary};
`,
};
};
89 changes: 89 additions & 0 deletions src/components/QueryEditor/PromptHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { Button, Icon, useStyles2 } from "@grafana/ui";
import { v4 as uuidv4 } from 'uuid';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';

export const PromptHistory = ({ parsedStoredPrompts, useStoredPrompt, setParsedStoredPrompts, setShowQueryHistory }) => {
const NO_STORED_PROMPTS_MESSAGE = "No prompts stored in history. When you write a prompt and click generate query, the prompt will be stored here.";
const styles = useStyles2(getStyles);

const removePrompt = (prompt: string) => {
const updatedPrompts = parsedStoredPrompts.filter((p) => p !== prompt);
const stringifiedPrompts = JSON.stringify(updatedPrompts);
localStorage.setItem("storedOpenAIPrompts", stringifiedPrompts);
setParsedStoredPrompts(updatedPrompts);
};

const useSelectedPrompt = (prompt: string) => {
useStoredPrompt(prompt);
setShowQueryHistory(false);
};

const generatePromptCards = ( ) => {
if (parsedStoredPrompts.length > 0) {
return parsedStoredPrompts.map((prompt) => {
return(
<div key={uuidv4()} style={{ border: `1px solid`, padding: '20px', marginTop: '15px', height: '150px' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end'}}>
<Button
// eslint-disable-next-line react-hooks/rules-of-hooks
onClick={() => removePrompt(prompt)}
icon="trash-alt"
aria-label="Remove"
size="sm"
variant="secondary"
/>
</div>
<div style={{ width: '90%', height: '50%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ margin: '0'}}>{prompt}</div>
<Button
// eslint-disable-next-line react-hooks/rules-of-hooks
onClick={() => useSelectedPrompt(prompt)}
variant="secondary"
size="sm"
>
Use this prompt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Use this prompt
Use prompt

</Button>
</div>
</div>
)
})
alyssabull marked this conversation as resolved.
Show resolved Hide resolved
} else {
return (
<div className={styles.wrapper}>
<div className={styles.icon}>
<Icon name="exclamation-triangle" />
</div>
<div className={styles.message}>
{NO_STORED_PROMPTS_MESSAGE}
</div>
</div>
)
};
};

return(
<div>
{generatePromptCards()}
</div>
)
};

const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
marginTop: theme.spacing(2),
background: theme.colors.background.secondary,
display: 'flex',
}),
icon: css({
background: theme.colors.error.main,
color: theme.colors.error.contrastText,
padding: theme.spacing(1),
}),
message: css({
fontSize: theme.typography.bodySmall.fontSize,
fontFamily: theme.typography.fontFamilyMonospace,
padding: theme.spacing(1),
}),
});
Loading