blob: bf74cce752169786e4e2a6ae122e45842cd050f6 [file]
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Platform from '../platform/platform.js';
import * as Root from '../root/root.js';
import {InspectorFrontendHostInstance} from './InspectorFrontendHost.js';
import {bindOutputStream} from './ResourceLoader.js';
export interface AidaRequest {
input: string;
client: string;
options?: {
temperature?: Number,
// eslint-disable-next-line @typescript-eslint/naming-convention
model_id?: string,
};
metadata?: {
// eslint-disable-next-line @typescript-eslint/naming-convention
disable_user_content_logging: boolean,
};
}
export interface AidaResponse {
explanation: string;
metadata: {
rpcGlobalId?: number,
};
}
export class AidaClient {
static buildApiRequest(input: string): AidaRequest {
const request: AidaRequest = {
input,
client: 'CHROME_DEVTOOLS',
};
const temperature = parseFloat(Root.Runtime.Runtime.queryParam('aidaTemperature') || '');
if (!isNaN(temperature)) {
request.options ??= {};
request.options.temperature = temperature;
}
const modelId = Root.Runtime.Runtime.queryParam('aidaModelId');
if (modelId) {
request.options ??= {};
request.options.model_id = modelId;
}
if (Root.Runtime.Runtime.queryParam('ci_disallowLogging') === 'true') {
request.metadata = {
disable_user_content_logging: true,
};
}
return request;
}
async * fetch(input: string): AsyncGenerator<AidaResponse, void, void> {
if (!InspectorFrontendHostInstance.doAidaConversation) {
throw new Error('doAidaConversation is not available');
}
const stream = (() => {
let {promise, resolve, reject} = Platform.PromiseUtilities.promiseWithResolvers<string|null>();
return {
write: async(data: string): Promise<void> => {
resolve(data);
({promise, resolve, reject} = Platform.PromiseUtilities.promiseWithResolvers<string|null>());
},
close: async(): Promise<void> => {
resolve(null);
},
read: (): Promise<string|null> => {
return promise;
},
fail: (e: Error) => reject(e),
};
})();
const streamId = bindOutputStream(stream);
InspectorFrontendHostInstance.doAidaConversation(
JSON.stringify(AidaClient.buildApiRequest(input)), streamId, result => {
if (result.statusCode === 403) {
stream.fail(new Error('Server responded: permission denied'));
} else if (result.error) {
stream.fail(new Error(`Cannot send request: ${result.error} ${result.detail || ''}`));
} else if (result.statusCode !== 200) {
stream.fail(new Error(`Request failed: ${JSON.stringify(result)}`));
} else {
void stream.close();
}
});
let chunk;
const text = [];
let inCodeChunk = false;
const metadata = {rpcGlobalId: 0};
while ((chunk = await stream.read())) {
let textUpdated = false;
// The AIDA response is a JSON array of objects, split at the object
// boundary. Therefore each chunk may start with `[` or `,` and possibly
// followed by `]`. Each chunk may include one or more objects, so we
// make sure that each chunk becomes a well-formed JSON array when we
// parse it by adding `[` and `]` and removing `,` where appropriate.
if (!chunk.length) {
continue;
}
if (chunk.startsWith(',')) {
chunk = chunk.slice(1);
}
if (!chunk.startsWith('[')) {
chunk = '[' + chunk;
}
if (!chunk.endsWith(']')) {
chunk = chunk + ']';
}
let results;
try {
results = JSON.parse(chunk);
} catch (error) {
throw new Error('Cannot parse chunk: ' + chunk, {cause: error});
}
const CODE_CHUNK_SEPARATOR = '\n`````\n';
for (const result of results) {
if ('metadata' in result) {
metadata.rpcGlobalId = result.metadata.rpcGlobalId;
}
if ('textChunk' in result) {
if (inCodeChunk) {
text.push(CODE_CHUNK_SEPARATOR);
inCodeChunk = false;
}
text.push(result.textChunk.text);
textUpdated = true;
} else if ('codeChunk' in result) {
if (!inCodeChunk) {
text.push(CODE_CHUNK_SEPARATOR);
inCodeChunk = true;
}
text.push(result.codeChunk.code);
textUpdated = true;
} else if ('error' in result) {
throw new Error(`Server responded: ${JSON.stringify(result)}`);
} else {
throw new Error('Unknown chunk result');
}
}
if (textUpdated) {
yield {
explanation: text.join('') + (inCodeChunk ? CODE_CHUNK_SEPARATOR : ''),
metadata,
};
}
}
}
}