| // Copyright 2025 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| import * as Common from '../../core/common/common.js'; | |
| import * as Host from '../../core/host/host.js'; | |
| import * as Root from '../../core/root/root.js'; | |
| export interface LanguageModel { | |
| promptStreaming: (arg0: string, opts?: { | |
| signal?: AbortSignal, | |
| }) => AsyncGenerator<string>; | |
| clone: () => Promise<LanguageModel>; | |
| destroy: () => void; | |
| } | |
| export const enum LanguageModelAvailability { | |
| UNAVAILABLE = 'unavailable', | |
| DOWNLOADABLE = 'downloadable', | |
| DOWNLOADING = 'downloading', | |
| AVAILABLE = 'available', | |
| DISABLED = 'disabled', | |
| } | |
| export class BuiltInAi extends Common.ObjectWrapper.ObjectWrapper<EventTypes> { | |
| #availability: LanguageModelAvailability|null = null; | |
| #hasGpu: boolean; | |
| #consoleInsightsSession?: LanguageModel; | |
| initDoneForTesting: Promise<void>; | |
| #downloadProgress: number|null = null; | |
| #currentlyCreatingSession = false; | |
| static instance(): BuiltInAi { | |
| if (!Root.DevToolsContext.globalInstance().has(BuiltInAi)) { | |
| Root.DevToolsContext.globalInstance().set(BuiltInAi, new BuiltInAi()); | |
| } | |
| return Root.DevToolsContext.globalInstance().get(BuiltInAi); | |
| } | |
| constructor() { | |
| super(); | |
| this.#hasGpu = this.#isGpuAvailable(); | |
| this.initDoneForTesting = | |
| this.getLanguageModelAvailability().then(() => this.#sendAvailabilityMetrics()).then(() => this.initialize()); | |
| } | |
| async getLanguageModelAvailability(): Promise<LanguageModelAvailability> { | |
| if (!Root.Runtime.hostConfig.devToolsConsoleInsightsTeasers?.enabled) { | |
| this.#availability = LanguageModelAvailability.DISABLED; | |
| return this.#availability; | |
| } | |
| try { | |
| // @ts-expect-error | |
| this.#availability = await window.LanguageModel.availability({ | |
| expectedInputs: [{ | |
| type: 'text', | |
| languages: ['en'], | |
| }], | |
| expectedOutputs: [{ | |
| type: 'text', | |
| languages: ['en'], | |
| }], | |
| }) as LanguageModelAvailability; | |
| } catch { | |
| this.#availability = LanguageModelAvailability.UNAVAILABLE; | |
| } | |
| return this.#availability; | |
| } | |
| isDownloading(): boolean { | |
| return this.#availability === LanguageModelAvailability.DOWNLOADING; | |
| } | |
| isEventuallyAvailable(): boolean { | |
| if (!this.#hasGpu && !Boolean(Root.Runtime.hostConfig.devToolsConsoleInsightsTeasers?.allowWithoutGpu)) { | |
| return false; | |
| } | |
| return this.#availability === LanguageModelAvailability.AVAILABLE || | |
| this.#availability === LanguageModelAvailability.DOWNLOADING || | |
| this.#availability === LanguageModelAvailability.DOWNLOADABLE; | |
| } | |
| #setDownloadProgress(newValue: number): void { | |
| this.#downloadProgress = newValue; | |
| this.dispatchEventToListeners(Events.DOWNLOAD_PROGRESS_CHANGED, this.#downloadProgress); | |
| } | |
| getDownloadProgress(): number|null { | |
| return this.#downloadProgress; | |
| } | |
| startDownloadingModel(): void { | |
| if (!Root.Runtime.hostConfig.devToolsConsoleInsightsTeasers?.allowWithoutGpu && !this.#hasGpu) { | |
| return; | |
| } | |
| if (this.#availability !== LanguageModelAvailability.DOWNLOADABLE) { | |
| return; | |
| } | |
| void this.#createSession(); | |
| // Without the timeout, the returned availability would still be `downloadable` | |
| setTimeout(() => { | |
| void this.getLanguageModelAvailability(); | |
| }, 1000); | |
| } | |
| #isGpuAvailable(): boolean { | |
| // In node-based API tests, there is no global `document` available. Since this | |
| // class is instantiated during Universe bootstrapping, we need to guard against | |
| // using document when not in the browser. | |
| if (typeof document === 'undefined') { | |
| return false; | |
| } | |
| const canvas = document.createElement('canvas'); | |
| try { | |
| const webgl = canvas.getContext('webgl'); | |
| if (!webgl) { | |
| return false; | |
| } | |
| const debugInfo = webgl.getExtension('WEBGL_debug_renderer_info'); | |
| if (!debugInfo) { | |
| return false; | |
| } | |
| const renderer = webgl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); | |
| if (renderer.includes('SwiftShader')) { | |
| return false; | |
| } | |
| } catch { | |
| return false; | |
| } | |
| return true; | |
| } | |
| hasSession(): boolean { | |
| return Boolean(this.#consoleInsightsSession); | |
| } | |
| async initialize(): Promise<void> { | |
| if (!Root.Runtime.hostConfig.devToolsConsoleInsightsTeasers?.allowWithoutGpu && !this.#hasGpu) { | |
| return; | |
| } | |
| if (this.#availability !== LanguageModelAvailability.AVAILABLE && | |
| this.#availability !== LanguageModelAvailability.DOWNLOADING) { | |
| return; | |
| } | |
| await this.#createSession(); | |
| } | |
| async #createSession(): Promise<void> { | |
| if (this.#currentlyCreatingSession) { | |
| return; | |
| } | |
| this.#currentlyCreatingSession = true; | |
| const monitor = (m: EventTarget): void => { | |
| m.addEventListener('downloadprogress', ((e: {loaded: number}) => { | |
| this.#setDownloadProgress(e.loaded); | |
| }) as unknown as EventListener); | |
| }; | |
| try { | |
| // @ts-expect-error | |
| this.#consoleInsightsSession = await window.LanguageModel.create({ | |
| monitor, | |
| initialPrompts: [{ | |
| role: 'system', | |
| content: ` | |
| You are an expert web developer. Your goal is to help a human web developer who | |
| is using Chrome DevTools to debug a web site or web app. The Chrome DevTools | |
| console is showing a message which is either an error or a warning. Please help | |
| the user understand the problematic console message. | |
| Your instructions are as follows: | |
| - Explain the reason why the error or warning is showing up. | |
| - The explanation has a maximum length of 200 characters. Anything beyond this | |
| length will be cut off. Make sure that your explanation is at most 200 characters long. | |
| - Your explanation should not end in the middle of a sentence. | |
| - Your explanation should consist of a single paragraph only. Do not include any | |
| headings or code blocks. Only write a single paragraph of text. | |
| - Your response should be concise and to the point. Avoid lengthy explanations | |
| or unnecessary details. | |
| `, | |
| }], | |
| expectedInputs: [{ | |
| type: 'text', | |
| languages: ['en'], | |
| }], | |
| expectedOutputs: [{ | |
| type: 'text', | |
| languages: ['en'], | |
| }], | |
| }); | |
| if (this.#availability !== LanguageModelAvailability.AVAILABLE) { | |
| this.dispatchEventToListeners(Events.DOWNLOADED_AND_SESSION_CREATED); | |
| void this.getLanguageModelAvailability(); | |
| } | |
| } catch (e) { | |
| console.error('Error when creating LanguageModel session', e.message); | |
| } | |
| this.#currentlyCreatingSession = false; | |
| } | |
| static removeInstance(): void { | |
| Root.DevToolsContext.globalInstance().delete(BuiltInAi); | |
| } | |
| async * getConsoleInsight(prompt: string, abortController: AbortController): AsyncGenerator<string> { | |
| if (!this.#consoleInsightsSession) { | |
| return; | |
| } | |
| // Clone the session to start a fresh conversation for each answer. Otherwise | |
| // previous dialog would pollute the context resulting in worse answers. | |
| let session: LanguageModel|null = null; | |
| try { | |
| session = await this.#consoleInsightsSession.clone(); | |
| const stream = session.promptStreaming(prompt, { | |
| signal: abortController.signal, | |
| }); | |
| for await (const chunk of stream) { | |
| yield chunk; | |
| } | |
| } finally { | |
| if (session) { | |
| session.destroy(); | |
| } | |
| } | |
| } | |
| #sendAvailabilityMetrics(): void { | |
| if (this.#hasGpu) { | |
| switch (this.#availability) { | |
| case LanguageModelAvailability.UNAVAILABLE: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.UNAVAILABLE_HAS_GPU); | |
| break; | |
| case LanguageModelAvailability.DOWNLOADABLE: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADABLE_HAS_GPU); | |
| break; | |
| case LanguageModelAvailability.DOWNLOADING: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADING_HAS_GPU); | |
| break; | |
| case LanguageModelAvailability.AVAILABLE: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.AVAILABLE_HAS_GPU); | |
| break; | |
| case LanguageModelAvailability.DISABLED: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DISABLED_HAS_GPU); | |
| break; | |
| } | |
| } else { | |
| switch (this.#availability) { | |
| case LanguageModelAvailability.UNAVAILABLE: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.UNAVAILABLE_NO_GPU); | |
| break; | |
| case LanguageModelAvailability.DOWNLOADABLE: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADABLE_NO_GPU); | |
| break; | |
| case LanguageModelAvailability.DOWNLOADING: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DOWNLOADING_NO_GPU); | |
| break; | |
| case LanguageModelAvailability.AVAILABLE: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.AVAILABLE_NO_GPU); | |
| break; | |
| case LanguageModelAvailability.DISABLED: | |
| Host.userMetrics.builtInAiAvailability(Host.UserMetrics.BuiltInAiAvailability.DISABLED_NO_GPU); | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| export const enum Events { | |
| DOWNLOAD_PROGRESS_CHANGED = 'downloadProgressChanged', | |
| DOWNLOADED_AND_SESSION_CREATED = 'downloadedAndSessionCreated', | |
| } | |
| export interface EventTypes { | |
| [Events.DOWNLOAD_PROGRESS_CHANGED]: number; | |
| [Events.DOWNLOADED_AND_SESSION_CREATED]: void; | |
| } |