chrome-devtools-frontend
Version:
Chrome DevTools UI
502 lines (460 loc) • 18 kB
text/typescript
// Copyright 2026 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 AIDA from './AidaClientTypes.js';
import * as GCA from './GcaTypes.js';
type AidaRequest = AIDA.DoConversationRequest|AIDA.CompletionRequest|AIDA.GenerateCodeRequest;
function createBaseGcaRequest(
request: AidaRequest, contents: GCA.Content[], experience: string): GCA.GenerateContentRequest {
const gcaRequest: GCA.GenerateContentRequest = {contents, aicode: {experience}};
mapCommonAidaRequestFields(request, gcaRequest);
buildLabels(request, gcaRequest);
if ('preamble' in request && request.preamble) {
gcaRequest.systemInstruction = {
role: 'user',
parts: [{text: request.preamble}],
};
}
return gcaRequest;
}
export function aidaDoConversationRequestToGcaRequest(request: AIDA.DoConversationRequest): GCA.GenerateContentRequest {
try {
const contents: GCA.Content[] = [];
if (request.facts) {
contents.push(convertAidaFactsToGcaContent(request.facts));
}
if (request.historical_contexts) {
contents.push(...(request.historical_contexts).map(convertAidaContentToGcaContent));
}
contents.push(convertAidaContentToGcaContent(request.current_message));
const gcaRequest = createBaseGcaRequest(request, contents, 'chat_console_insights');
if (request.function_declarations) {
gcaRequest.tools = [{
functionDeclarations:
(request.function_declarations).map(fd => ({
name: fd.name,
description: fd.description,
parameters: convertAidaParamToGcaSchema(fd.parameters),
})),
}];
}
AIDA.debugLog('Translation succeded:', JSON.stringify(request), JSON.stringify(gcaRequest));
return gcaRequest;
} catch (e) {
AIDA.debugLog('Translation error:', JSON.stringify(request), e);
throw e;
}
}
function mapCommonAidaRequestFields(aidaRequest: AidaRequest, gcaRequest: GCA.GenerateContentRequest): void {
if (aidaRequest.options?.model_id) {
gcaRequest.model = aidaRequest.options.model_id;
}
if (aidaRequest.options?.temperature !== undefined) {
gcaRequest.generationConfig = {
...gcaRequest.generationConfig,
temperature: aidaRequest.options.temperature,
};
}
}
export function gcaResponseToAidaDoConversationResponse(response: GCA.GenerateContentResponse):
AIDA.DoConversationResponse {
const functionCalls: AIDA.AidaFunctionCallResponse[] = [];
if (response.candidates?.[0].content?.parts) {
for (const part of response.candidates[0].content.parts) {
if (part.functionCall) {
functionCalls.push({
name: part.functionCall.name,
args: part.functionCall.args || {},
});
}
}
}
return {
explanation: extractTextFromGcaParts(response.candidates[0].content?.parts),
metadata: {
rpcGlobalId: response.responseId,
},
functionCalls: functionCalls.length > 0 ?
(functionCalls as [AIDA.AidaFunctionCallResponse, ...AIDA.AidaFunctionCallResponse[]]) :
undefined,
completed: true,
};
}
function extractTextFromGcaParts(parts: GCA.Part[]|undefined): string {
if (!parts) {
return '';
}
return parts.map(p => p.text || '').join('');
}
export function aidaEventToGcaTelemetryRequest(clientEvent: AIDA.AidaRegisterClientEvent): GCA.SendTelemetryRequest {
try {
const feedbackMetrics: GCA.FeedbackMetric[] = [];
const responseId = String(clientEvent.corresponding_aida_rpc_global_id);
const eventTime = new Date().toISOString();
if (clientEvent.do_conversation_client_event) {
const feedback = clientEvent.do_conversation_client_event.user_feedback;
if (feedback.sentiment) {
let interaction: GCA.InteractionType = GCA.InteractionType.INTERACTION_TYPE_UNSPECIFIED;
if (feedback.sentiment === AIDA.Rating.POSITIVE) {
interaction = GCA.InteractionType.THUMBS_UP;
} else if (feedback.sentiment === AIDA.Rating.NEGATIVE) {
interaction = GCA.InteractionType.THUMBS_DOWN;
}
feedbackMetrics.push({
eventTime,
responseId,
suggestionInteraction: {interaction},
});
}
}
feedbackMetrics.push(...convertCodeTelemetry(
clientEvent.complete_code_client_event, GCA.Method.COMPLETE_CODE, responseId, eventTime));
feedbackMetrics.push(...convertCodeTelemetry(
clientEvent.generate_code_client_event, GCA.Method.GENERATE_CODE, responseId, eventTime));
const gcaTelemetryRequest: GCA.SendTelemetryRequest = {
feedbackMetrics,
};
AIDA.debugLog('Translation succeeded:', JSON.stringify(clientEvent), JSON.stringify(gcaTelemetryRequest));
return gcaTelemetryRequest;
} catch (e) {
AIDA.debugLog('Translation error:', JSON.stringify(clientEvent), e);
throw e;
}
}
/* eslint-disable @typescript-eslint/naming-convention */
function convertCodeTelemetry(
event: {user_acceptance?: AIDA.UserAcceptance, user_impression?: AIDA.UserImpression}|undefined, method: GCA.Method,
responseId: string, eventTime: string): GCA.FeedbackMetric[] {
if (!event) {
return [];
}
if ('user_impression' in event && event.user_impression) {
const impression = event.user_impression;
return [{
eventTime,
responseId,
suggestionOffered: {
method,
status: GCA.SuggestionStatus.NO_ERROR,
responseLatency: `${impression.latency.duration.seconds + impression.latency.duration.nanos / 1e9}s`,
},
}];
}
if ('user_acceptance' in event && event.user_acceptance) {
const acceptance = event.user_acceptance;
return [{
eventTime,
responseId,
suggestionInteraction: {
interaction: GCA.InteractionType.ACCEPT,
candidateIndex: acceptance.sample.sample_id,
},
}];
}
return [];
}
/* eslint-enable @typescript-eslint/naming-convention */
export function aidaCompletionRequestToGcaRequest(request: AIDA.CompletionRequest): GCA.GenerateContentRequest {
try {
let additionalFiles: GCA.SourceFile[] =
(request.additional_files ?? []).map(f => ({
fileUri: f.path,
inclusionReason: [AidaReasonToGcaInclusionReason[f.included_reason]],
segments: [{content: f.content, isSelected: false}],
}));
const inEditorFile: GCA.SourceFile = inFileEditRequestToSourceFile(request);
if (inEditorFile) {
additionalFiles = [inEditorFile, ...additionalFiles];
}
const gcaRequest = createBaseGcaRequest(request, [], 'complete_code');
gcaRequest.aicode.files = additionalFiles;
if (request.options?.stop_sequences) {
gcaRequest.generationConfig = {
...gcaRequest.generationConfig,
stopSequences: request.options.stop_sequences,
};
}
AIDA.debugLog('Translation succeeded:', JSON.stringify(request), JSON.stringify(gcaRequest));
return gcaRequest;
} catch (e) {
AIDA.debugLog('Translation error:', JSON.stringify(request), e);
throw e;
}
}
function inFileEditRequestToSourceFile(request: AIDA.CompletionRequest): GCA.SourceFile {
const sourceFile: GCA.SourceFile = {
inclusionReason: [GCA.InclusionReason.ACTIVE],
fileUri: 'devtools-code-completion',
segments: [
{
content: request.prefix,
isSelected: false,
},
{
content: '',
isSelected: true, // Cursor position
}
],
};
if (request.suffix) {
sourceFile.segments?.push({
content: request.suffix,
isSelected: false,
});
}
return sourceFile;
}
/* eslint-disable @typescript-eslint/naming-convention */
function buildLabels(request: AidaRequest, gcaRequest: GCA.GenerateContentRequest): void {
const labels: Record<string, string> = {};
if (request.client) {
labels['client'] = request.client;
}
if ('functionality_type' in request && request.functionality_type !== undefined) {
labels['functionality_type'] = AIDA.FunctionalityType[request.functionality_type];
}
if ('client_feature' in request && request.client_feature !== undefined) {
labels['client_feature'] = AIDA.ClientFeature[request.client_feature];
}
if ('last_user_action' in request && request.last_user_action !== undefined) {
labels['last_user_action'] = AIDA.EditType[request.last_user_action];
}
if ('use_case' in request && request.use_case !== undefined) {
labels['use_case'] = AIDA.UseCase[request.use_case];
}
if (request.metadata.string_session_id) {
labels['session_id'] = request.metadata.string_session_id;
}
const options = request.options as {
inference_language?: string,
expect_code_output?: boolean,
} | undefined;
if (options?.inference_language) {
labels['inference_language'] = options.inference_language;
}
if (options?.expect_code_output !== undefined) {
labels['expect_code_output'] = String(options.expect_code_output);
}
if (request.metadata.disable_user_content_logging !== undefined) {
labels['disable_user_content_logging'] = String(request.metadata.disable_user_content_logging);
}
if (request.metadata.client_version) {
labels['client_version'] = request.metadata.client_version;
}
if (Object.keys(labels).length > 0) {
gcaRequest.labels = labels;
}
}
/* eslint-enable @typescript-eslint/naming-convention */
const AidaReasonToGcaInclusionReason: Record<AIDA.Reason, GCA.InclusionReason> = {
[AIDA.Reason.UNKNOWN]: GCA.InclusionReason.INCLUSION_REASON_UNSPECIFIED,
[AIDA.Reason.CURRENTLY_OPEN]: GCA.InclusionReason.OPEN,
// Intentional mapping due to type mismatch
// TODO(liviurau): find a way to validate this mapping
[AIDA.Reason.RECENTLY_OPENED]: GCA.InclusionReason.RECENTLY_CLOSED,
[AIDA.Reason.RECENTLY_EDITED]: GCA.InclusionReason.RECENTLY_EDITED,
[AIDA.Reason.COLOCATED]: GCA.InclusionReason.COLOCATED,
[AIDA.Reason.RELATED_FILE]: GCA.InclusionReason.RELATED,
};
export function gcaResponseToAidaCompletionResponse(response: GCA.GenerateContentResponse): AIDA.CompletionResponse {
try {
const {samples, metadata} = gcaResponseToAidaSamplesAndMetadata(response);
const aidaResponse: AIDA.CompletionResponse = {
generatedSamples: samples,
metadata,
};
AIDA.debugLog('Translation succeeded:', JSON.stringify(response), JSON.stringify(aidaResponse));
return aidaResponse;
} catch (e) {
AIDA.debugLog('Translation error', JSON.stringify(response), e);
throw e;
}
}
function gcaResponseToAidaSamplesAndMetadata(response: GCA.GenerateContentResponse): {
samples: AIDA.GenerationSample[],
metadata: AIDA.ResponseMetadata,
} {
return {
samples: (response.candidates ?? []).map(gcaCandidateToAidaGenerationSample),
metadata: {
rpcGlobalId: response.responseId,
},
};
}
export function aidaGenerateCodeRequestToGcaRequest(request: AIDA.GenerateCodeRequest): GCA.GenerateContentRequest {
try {
const gcaRequest =
createBaseGcaRequest(request, [convertAidaContentToGcaContent(request.current_message)], 'generate_code');
if (request.context_files) {
gcaRequest.aicode.files = (request.context_files).map(f => ({
fileUri: f.path,
programmingLanguage: f.programming_language,
}));
}
AIDA.debugLog('Translation succeeded:', JSON.stringify(request), JSON.stringify(gcaRequest));
return gcaRequest;
} catch (e) {
AIDA.debugLog('Translation error', JSON.stringify(request), e);
throw e;
}
}
export function gcaResponseToAidaGenerateCodeResponse(response: GCA.GenerateContentResponse):
AIDA.GenerateCodeResponse {
try {
const aidaResponse: AIDA.GenerateCodeResponse = gcaResponseToAidaSamplesAndMetadata(response);
AIDA.debugLog('Translation succeeded:', JSON.stringify(response), JSON.stringify(aidaResponse));
return aidaResponse;
} catch (e) {
AIDA.debugLog('translation error', JSON.stringify(response), e);
throw e;
}
}
function gcaCandidateToAidaGenerationSample(candidate: GCA.Candidate): AIDA.GenerationSample {
const generationSample: AIDA.GenerationSample = {
generationString: extractTextFromGcaParts(candidate.content?.parts),
score: 0,
sampleId: candidate.index,
};
if (candidate.citationMetadata) {
generationSample.attributionMetadata = {
attributionAction: AIDA.RecitationAction.CITE,
citations: (candidate.citationMetadata.citations ?? []).map(c => ({
startIndex: c.startIndex,
endIndex: c.endIndex,
uri: c.uri,
})),
};
}
return generationSample;
}
function convertAidaFactsToGcaContent(facts: AIDA.RequestFact[]): GCA.Content {
return {
role: 'user',
parts: facts.map(fact => {
return {text: `[source: ${fact.metadata.source}] ${fact.text}`};
}),
};
}
function convertAidaContentToGcaContent(content: AIDA.Content): GCA.Content {
// TODO(liviurau): decide how to map AIDA.Role.SYSTEM
// currently it will default to 'user'
let role: GCA.Role = 'user';
if (content.role === AIDA.Role.MODEL) {
role = 'model';
}
return {
role,
parts: (content.parts ?? []).map(convertAidaPartToGcaPart),
};
}
function convertAidaPartToGcaPart(part: AIDA.Part): GCA.Part {
if ('text' in part) {
return {text: part.text};
}
if ('functionCall' in part) {
return {
functionCall: {
name: part.functionCall.name,
args: part.functionCall.args,
},
};
}
if ('functionResponse' in part) {
const fResponse: Record<string, unknown> = {};
if ('result' in part.functionResponse.response) {
fResponse.output = part.functionResponse.response['result'];
} else if ('output' in part.functionResponse.response) {
fResponse.output = part.functionResponse.response['output'];
} else if (!('error' in part.functionResponse.response)) {
fResponse.output = part.functionResponse.response;
}
if ('error' in part.functionResponse.response) {
fResponse.error = part.functionResponse.response['error'];
}
return {
functionResponse: {
name: part.functionResponse.name,
response: fResponse,
},
};
}
if ('inlineData' in part) {
return {
inlineData: {
mimeType: part.inlineData.mimeType,
data: part.inlineData.data,
},
};
}
return {};
}
type FunctionParam<T extends string|number|symbol = string> =
AIDA.FunctionObjectParam<T>|AIDA.FunctionArrayParam|AIDA.FunctionPrimitiveParams;
function convertAidaParamToGcaSchema<T extends string|number|symbol = string>(param: FunctionParam<T>): GCA.Schema {
const schema: GCA.Schema = {
type: param.type as unknown as GCA.Type,
description: param.description,
};
if (param.nullable) {
schema.nullable = param.nullable;
}
if (param.type === AIDA.ParametersTypes.ARRAY && param.items) {
schema.items = convertAidaParamToGcaSchema(param.items);
} else if (param.type === AIDA.ParametersTypes.OBJECT && param.properties) {
schema.properties = {};
for (const [key, value] of Object.entries(param.properties)) {
schema.properties[key] = convertAidaParamToGcaSchema(value as FunctionParam);
}
schema.required = (param.required ?? []).map(r => r.toString());
}
return schema;
}
export function gcaChunkResponseToAidaChunkResponse(response: GCA.GenerateContentResponse): AIDA.AidaChunkResponse[] {
try {
const candidate = response.candidates?.[0];
const parts = candidate?.content?.parts || [];
const metadata: AIDA.ResponseMetadata = {
rpcGlobalId: response.responseId,
inferenceOptionMetadata: {modelId: response.modelVersion}
};
if (candidate?.citationMetadata?.citations) {
metadata.attributionMetadata = {
attributionAction: AIDA.RecitationAction.CITE,
citations: candidate.citationMetadata.citations.map(c => ({
startIndex: c.startIndex,
endIndex: c.endIndex,
uri: c.uri,
})),
};
}
const chunks: AIDA.AidaChunkResponse[] = (parts).map(part => {
const aidaChunkResponse: AIDA.AidaChunkResponse = {metadata};
if (part.text) {
aidaChunkResponse.textChunk = {
text: extractTextFromGcaParts(parts),
};
}
if (part.functionCall) {
aidaChunkResponse.functionCallChunk = {
functionCall: {
name: part.functionCall.name,
args: part.functionCall.args || {},
},
};
}
if (part.executableCode) {
aidaChunkResponse.codeChunk = {
code: part.executableCode.code,
inferenceLanguage: part.executableCode.language ? AIDA.AidaInferenceLanguage.PYTHON :
AIDA.AidaInferenceLanguage.UNKNOWN,
};
}
return aidaChunkResponse;
});
AIDA.debugLog('Translation succeeded:', JSON.stringify(response), JSON.stringify(chunks));
return chunks;
} catch (e) {
AIDA.debugLog('Translation error', JSON.stringify(response), e);
throw e;
}
}