Skip to content

Commit e375792

Browse files
authored
internal: Add assistant feature toggle (#1100)
1 parent 717fe3d commit e375792

12 files changed

Lines changed: 212 additions & 40 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { LanguageModelV2, LanguageModelV2CallOptions } from '@ai-sdk/provider'
2+
3+
/**
4+
* Placeholder implementation of the Grafana Assistant language model.
5+
* The actual A2A integration will be implemented in a follow-up PR.
6+
*/
7+
export class GrafanaAssistantLanguageModel implements LanguageModelV2 {
8+
readonly specificationVersion = 'v2' as const
9+
readonly provider = 'grafana-assistant'
10+
readonly modelId = 'grafana_assistant_k6_studio'
11+
readonly supportedUrls = {}
12+
13+
doGenerate(
14+
_options: LanguageModelV2CallOptions
15+
): ReturnType<LanguageModelV2['doGenerate']> {
16+
throw new Error('not implemented')
17+
}
18+
19+
doStream(
20+
_options: LanguageModelV2CallOptions
21+
): ReturnType<LanguageModelV2['doStream']> {
22+
throw new Error('not implemented')
23+
}
24+
}

src/handlers/ai/index.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { OpenAIResponsesProviderOptions } from '@ai-sdk/openai'
22
import { convertToModelMessages, streamText } from 'ai'
33
import { ipcMain, IpcMainEvent } from 'electron'
44

5-
import { setupAiModel } from './model'
5+
import { getGrafanaAssistantModel, getOpenAiModel } from './model'
66
import { streamMessages } from './streamMessages'
77
import { tools } from './tools'
88
import { AiHandler, StreamChatRequest, AbortStreamChatRequest } from './types'
@@ -19,31 +19,47 @@ async function handleStreamChat(
1919
event: IpcMainEvent,
2020
request: StreamChatRequest
2121
) {
22-
const aiModel = await setupAiModel()
22+
const provider = request.provider ?? 'openai'
2323
const messages = convertToModelMessages(request.messages)
2424

2525
const abortController = new AbortController()
2626
activeAbortControllers.set(request.id, abortController)
2727

2828
try {
29-
const response = streamText({
30-
model: aiModel,
31-
toolChoice: 'required',
32-
messages,
33-
tools,
34-
abortSignal: abortController.signal,
35-
providerOptions: {
36-
openai: {
37-
parallelToolCalls: false,
38-
reasoningEffort: 'low',
39-
textVerbosity: 'low',
40-
// Disable storing of conversations, required for orgs with zero data retention
41-
store: false,
42-
} satisfies OpenAIResponsesProviderOptions,
43-
},
44-
})
29+
if (provider === 'grafana-assistant') {
30+
const aiModel = getGrafanaAssistantModel()
4531

46-
await streamMessages(event.sender, response, request.id)
32+
const response = streamText({
33+
model: aiModel,
34+
toolChoice: 'required',
35+
messages,
36+
tools,
37+
abortSignal: abortController.signal,
38+
})
39+
40+
await streamMessages(event.sender, response, request.id, false)
41+
} else {
42+
const aiModel = await getOpenAiModel()
43+
44+
const response = streamText({
45+
model: aiModel,
46+
toolChoice: 'required',
47+
messages,
48+
tools,
49+
abortSignal: abortController.signal,
50+
providerOptions: {
51+
openai: {
52+
parallelToolCalls: false,
53+
reasoningEffort: 'low',
54+
textVerbosity: 'low',
55+
// Disable storing of conversations, required for orgs with zero data retention
56+
store: false,
57+
} satisfies OpenAIResponsesProviderOptions,
58+
},
59+
})
60+
61+
await streamMessages(event.sender, response, request.id, true)
62+
}
4763
} finally {
4864
// Clean up the AbortController after streaming completes or fails
4965
activeAbortControllers.delete(request.id)

src/handlers/ai/model.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@ import { LanguageModel } from 'ai'
33

44
import { getDecryptedAiKey } from '@/main/settings'
55

6-
let model: LanguageModel | null = null
6+
import { GrafanaAssistantLanguageModel } from './grafanaAssistantProvider'
77

8-
export async function setupAiModel() {
9-
if (model) {
10-
return model
8+
let openAiModel: LanguageModel | null = null
9+
const grafanaAssistantModel = new GrafanaAssistantLanguageModel()
10+
11+
export async function getOpenAiModel() {
12+
if (openAiModel) {
13+
return openAiModel
1114
}
1215

1316
const apiKey = await getDecryptedAiKey()
1417
const provider = createOpenAI({ apiKey })
15-
model = provider('gpt-5')
18+
openAiModel = provider('gpt-5')
19+
20+
return openAiModel
21+
}
1622

17-
return model
23+
export function resetOpenAiModel() {
24+
openAiModel = null
1825
}
1926

20-
export function resetAiModel() {
21-
model = null
27+
export function getGrafanaAssistantModel() {
28+
return grafanaAssistantModel
2229
}

src/handlers/ai/streamMessages.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { AiHandler } from './types'
55
export async function streamMessages<Tools extends ToolSet, PARTIAL_OUTPUT>(
66
webContents: Electron.WebContents,
77
response: StreamTextResult<Tools, PARTIAL_OUTPUT>,
8-
requestId: string
8+
requestId: string,
9+
includeUsage: boolean
910
) {
1011
const stream = response.toUIMessageStream({})
1112

@@ -16,7 +17,7 @@ export async function streamMessages<Tools extends ToolSet, PARTIAL_OUTPUT>(
1617
})
1718
}
1819

19-
const usageData = await response.usage
20+
const usageData = includeUsage ? await response.usage : undefined
2021

2122
webContents.send(AiHandler.StreamChatEnd, {
2223
id: requestId,

src/handlers/ai/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { LanguageModelUsage, UIMessage, UIMessageChunk } from 'ai'
22

3+
import { AiProvider } from '@/types/features'
4+
35
export enum AiHandler {
46
StreamChat = 'ai:streamChat',
57
StreamChatChunk = 'ai:streamChatChunk',
@@ -14,6 +16,7 @@ export interface StreamChatRequest {
1416
messages: UIMessage[]
1517
headers?: Record<string, string>
1618
body?: object
19+
provider?: AiProvider
1720
}
1821

1922
export interface StreamChatChunk {

src/main/settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs'
44
import { writeFile, open } from 'fs/promises'
55
import path from 'path'
66

7-
import { resetAiModel } from '@/handlers/ai/model'
7+
import { resetOpenAiModel } from '@/handlers/ai/model'
88
import { configureSystemProxy } from '@/services/http'
99

1010
import { AppSettingsSchema } from '../schemas/settings'
@@ -185,7 +185,7 @@ export async function applySettings(
185185

186186
if (modifiedSettings.ai) {
187187
k6StudioState.appSettings.ai = modifiedSettings.ai
188-
resetAiModel()
188+
resetOpenAiModel()
189189
}
190190
}
191191

src/store/features/useFeaturesStore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const defaultFeatures: Record<Feature, boolean> = {
1212
'dummy-feature': false,
1313
'typeahead-json': false,
1414
'browser-test-editor': import.meta.env.DEV,
15+
'grafana-assistant': false,
1516
}
1617

1718
export const useFeaturesStore = create<FeaturesStore>()(

src/types/features.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
export type Feature = 'dummy-feature' | 'typeahead-json' | 'browser-test-editor'
1+
export type Feature =
2+
| 'dummy-feature'
3+
| 'typeahead-json'
4+
| 'browser-test-editor'
5+
| 'grafana-assistant'
6+
7+
export type AiProvider = 'openai' | 'grafana-assistant'

src/views/Generator/AutoCorrelation/ErrorMessage.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ExternalLink, KeyIcon, RefreshCw } from 'lucide-react'
33

44
import grotCrashed from '@/assets/grot-crashed.svg'
55
import { useSettingsChanged } from '@/hooks/useSettings'
6+
import { useFeaturesStore } from '@/store/features'
67
import { useStudioUIStore } from '@/store/ui'
78

89
interface AutoCorrelationErrorProps {
@@ -11,6 +12,18 @@ interface AutoCorrelationErrorProps {
1112
}
1213

1314
export function ErrorMessage({ error, onRetry }: AutoCorrelationErrorProps) {
15+
const isGrafanaAssistant = useFeaturesStore(
16+
(state) => state.features['grafana-assistant']
17+
)
18+
19+
if (isGrafanaAssistant) {
20+
return <GrafanaAssistantError error={error} onRetry={onRetry} />
21+
}
22+
23+
return <OpenAiError error={error} onRetry={onRetry} />
24+
}
25+
26+
function OpenAiError({ error, onRetry }: AutoCorrelationErrorProps) {
1427
const errorMessage = error.message.toLowerCase()
1528
const openSettingsDialog = useStudioUIStore(
1629
(state) => state.openSettingsDialog
@@ -83,6 +96,34 @@ export function ErrorMessage({ error, onRetry }: AutoCorrelationErrorProps) {
8396
)
8497
}
8598

99+
function GrafanaAssistantError({ onRetry }: AutoCorrelationErrorProps) {
100+
const retryButton = (
101+
<Button onClick={onRetry}>
102+
<RefreshCw />
103+
Retry
104+
</Button>
105+
)
106+
107+
const reportIssueButton = (
108+
<Button onClick={() => window.studio.ui.reportIssue()} variant="outline">
109+
<ExternalLink />
110+
Report issue
111+
</Button>
112+
)
113+
114+
// TODO: Add Assistant specific error handling
115+
116+
return (
117+
<MessageContent
118+
title="Something went wrong"
119+
message="An unexpected error occurred during autocorrelation. Click retry to try again or report an issue if problem persists."
120+
>
121+
{retryButton}
122+
{reportIssueButton}
123+
</MessageContent>
124+
)
125+
}
126+
86127
function MessageContent({
87128
title,
88129
message,

src/views/Generator/AutoCorrelation/IntroductionMessage.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,31 @@ import { CheckCircleIcon, KeyIcon, WandSparkles } from 'lucide-react'
44
import grotIllustration from '@/assets/grot-magic.svg'
55
import { useProxyStatus } from '@/hooks/useProxyStatus'
66
import { useSettings } from '@/hooks/useSettings'
7+
import { useFeaturesStore } from '@/store/features'
78
import { useStudioUIStore } from '@/store/ui'
89

910
interface IntroductionMessageProps {
1011
onStart: () => void
1112
}
1213

1314
export function IntroductionMessage({ onStart }: IntroductionMessageProps) {
15+
const isGrafanaAssistant = useFeaturesStore(
16+
(state) => state.features['grafana-assistant']
17+
)
18+
19+
if (isGrafanaAssistant) {
20+
return <GrafanaAssistantIntro onStart={onStart} />
21+
}
22+
23+
return <OpenAiIntro onStart={onStart} />
24+
}
25+
26+
function OpenAiIntro({ onStart }: IntroductionMessageProps) {
1427
const openSettingsDialog = useStudioUIStore(
1528
(state) => state.openSettingsDialog
1629
)
1730
const { data: settings } = useSettings()
1831
const isAiConfigured = !!settings?.ai.apiKey
19-
2032
const proxyStatus = useProxyStatus()
2133

2234
return (
@@ -93,6 +105,52 @@ export function IntroductionMessage({ onStart }: IntroductionMessageProps) {
93105
)
94106
}
95107

108+
function GrafanaAssistantIntro({ onStart }: IntroductionMessageProps) {
109+
return (
110+
<Flex
111+
direction="column"
112+
align="center"
113+
gap="6"
114+
justify="center"
115+
height="100%"
116+
>
117+
<img
118+
src={grotIllustration}
119+
role="img"
120+
aria-label="Grafana mascot illustration"
121+
css={{ maxWidth: 250 }}
122+
/>
123+
124+
<Flex
125+
direction="column"
126+
align="center"
127+
gap="4"
128+
maxWidth="600px"
129+
css={{ textAlign: 'center' }}
130+
>
131+
<Badge color="orange" variant="soft">
132+
Coming Soon
133+
</Badge>
134+
<Text size="3" weight="bold">
135+
Automatically correlate dynamic values
136+
</Text>
137+
<Text size="2" color="gray" mb="2">
138+
Powered by Grafana Assistant
139+
</Text>
140+
141+
<Button onClick={onStart} size="3" disabled>
142+
<WandSparkles />
143+
Analyze recording
144+
</Button>
145+
146+
<Text size="1" color="gray" mt="1">
147+
This feature is in public preview and subject to change.
148+
</Text>
149+
</Flex>
150+
</Flex>
151+
)
152+
}
153+
96154
function ListItem({ children }: { children: React.ReactNode }) {
97155
return (
98156
<Flex align="center" gap="2">

0 commit comments

Comments
 (0)