UNPKG

chrome-devtools-frontend

Version:
502 lines (460 loc) • 18 kB
// 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; } }