Path: blob/main/extensions/copilot/src/platform/otel/common/genAiEvents.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { GenAiAttr, GenAiOperationName, StdAttr } from './genAiAttributes';6import { normalizeProviderMessages, toSystemInstructions, truncateForOTel } from './messageFormatters';7import type { IOTelService } from './otelService';8import { type WorkspaceOTelMetadata, workspaceMetadataToOTelAttributes } from './workspaceOTelMetadata';910/**11* Emit OTel GenAI standard events via the IOTelService abstraction.12*/13export function emitInferenceDetailsEvent(14otel: IOTelService,15request: {16model: string;17temperature?: number;18maxTokens?: number;19messages?: unknown;20systemMessage?: unknown;21tools?: unknown;22},23response: {24id?: string;25model?: string;26finishReasons?: string[];27inputTokens?: number;28outputTokens?: number;29} | undefined,30error?: { type: string; message: string },31): void {32const attributes: Record<string, unknown> = {33'event.name': 'gen_ai.client.inference.operation.details',34[GenAiAttr.OPERATION_NAME]: GenAiOperationName.CHAT,35[GenAiAttr.REQUEST_MODEL]: request.model,36};3738if (response) {39if (response.model) { attributes[GenAiAttr.RESPONSE_MODEL] = response.model; }40if (response.id) { attributes[GenAiAttr.RESPONSE_ID] = response.id; }41if (response.finishReasons) { attributes[GenAiAttr.RESPONSE_FINISH_REASONS] = response.finishReasons; }42if (response.inputTokens !== undefined) { attributes[GenAiAttr.USAGE_INPUT_TOKENS] = response.inputTokens; }43if (response.outputTokens !== undefined) { attributes[GenAiAttr.USAGE_OUTPUT_TOKENS] = response.outputTokens; }44}4546if (request.temperature !== undefined) { attributes[GenAiAttr.REQUEST_TEMPERATURE] = request.temperature; }47if (request.maxTokens !== undefined) { attributes[GenAiAttr.REQUEST_MAX_TOKENS] = request.maxTokens; }4849if (error) {50attributes[StdAttr.ERROR_TYPE] = error.type;51}5253// Full content capture with truncation to prevent OTLP batch failures54// Normalize to OTel GenAI semantic convention format55if (otel.config.captureContent) {56if (request.messages !== undefined) {57const msgs = Array.isArray(request.messages) ? request.messages as ReadonlyArray<Record<string, unknown>> : undefined;58attributes[GenAiAttr.INPUT_MESSAGES] = truncateForOTel(JSON.stringify(59msgs ? normalizeProviderMessages(msgs) : request.messages60));61}62if (request.systemMessage !== undefined) {63const systemText = typeof request.systemMessage === 'string'64? request.systemMessage65: JSON.stringify(request.systemMessage);66const systemInstructions = toSystemInstructions(systemText);67if (systemInstructions !== undefined) {68attributes[GenAiAttr.SYSTEM_INSTRUCTIONS] = truncateForOTel(JSON.stringify(systemInstructions));69}70}71if (request.tools !== undefined) {72attributes[GenAiAttr.TOOL_DEFINITIONS] = truncateForOTel(JSON.stringify(request.tools));73}74}7576otel.emitLogRecord(`GenAI inference: ${request.model}`, attributes);77}7879/**80* Emit extension-specific events.81*/82export function emitSessionStartEvent(83otel: IOTelService,84sessionId: string,85model: string,86participant: string,87): void {88otel.emitLogRecord('copilot_chat.session.start', {89'event.name': 'copilot_chat.session.start',90'session.id': sessionId,91[GenAiAttr.REQUEST_MODEL]: model,92[GenAiAttr.AGENT_NAME]: participant,93});94}9596export function emitToolCallEvent(97otel: IOTelService,98toolName: string,99durationMs: number,100success: boolean,101error?: string,102): void {103otel.emitLogRecord(`copilot_chat.tool.call: ${toolName}`, {104'event.name': 'copilot_chat.tool.call',105[GenAiAttr.TOOL_NAME]: toolName,106'duration_ms': durationMs,107'success': success,108...(error ? { [StdAttr.ERROR_TYPE]: error } : {}),109});110}111112export function emitAgentTurnEvent(113otel: IOTelService,114turnIndex: number,115inputTokens: number,116outputTokens: number,117toolCallCount: number,118): void {119otel.emitLogRecord(`copilot_chat.agent.turn: ${turnIndex}`, {120'event.name': 'copilot_chat.agent.turn',121'turn.index': turnIndex,122[GenAiAttr.USAGE_INPUT_TOKENS]: inputTokens,123[GenAiAttr.USAGE_OUTPUT_TOKENS]: outputTokens,124'tool_call_count': toolCallCount,125});126}127128// ── Agent Activity & Outcome Events ──129130export function emitEditFeedbackEvent(131otel: IOTelService,132outcome: string,133languageId: string,134participant: string,135requestId: string,136editSurface: string,137hasRemainingEdits: boolean,138isNotebook: boolean,139workspace?: WorkspaceOTelMetadata,140): void {141otel.emitLogRecord(`copilot_chat.edit.feedback: ${outcome}`, {142'event.name': 'copilot_chat.edit.feedback',143'outcome': outcome,144'language_id': languageId,145'participant': participant,146'request_id': requestId,147'edit_surface': editSurface,148'has_remaining_edits': hasRemainingEdits,149'is_notebook': isNotebook,150...workspaceMetadataToOTelAttributes(workspace),151});152}153154export function emitEditHunkActionEvent(155otel: IOTelService,156outcome: string,157languageId: string,158requestId: string,159lineCount: number,160linesAdded: number,161linesRemoved: number,162workspace?: WorkspaceOTelMetadata,163): void {164otel.emitLogRecord(`copilot_chat.edit.hunk.action: ${outcome}`, {165'event.name': 'copilot_chat.edit.hunk.action',166'outcome': outcome,167'language_id': languageId,168'request_id': requestId,169'line_count': lineCount,170'lines_added': linesAdded,171'lines_removed': linesRemoved,172...workspaceMetadataToOTelAttributes(workspace),173});174}175176export function emitInlineDoneEvent(177otel: IOTelService,178accepted: boolean,179languageId: string,180editCount: number,181editLineCount: number,182replyType: string,183isNotebook: boolean,184workspace?: WorkspaceOTelMetadata,185): void {186otel.emitLogRecord(`copilot_chat.inline.done: ${accepted ? 'accepted' : 'rejected'}`, {187'event.name': 'copilot_chat.inline.done',188'accepted': accepted,189'language_id': languageId,190'edit_count': editCount,191'edit_line_count': editLineCount,192'reply_type': replyType,193'is_notebook': isNotebook,194...workspaceMetadataToOTelAttributes(workspace),195});196}197198export function emitEditSurvivalEvent(199otel: IOTelService,200editSource: string,201survivalRateFourGram: number,202survivalRateNoRevert: number,203timeDelayMs: number,204didBranchChange: boolean,205requestId: string,206workspace?: WorkspaceOTelMetadata,207): void {208otel.emitLogRecord(`copilot_chat.edit.survival: ${editSource}`, {209'event.name': 'copilot_chat.edit.survival',210'edit_source': editSource,211'survival_rate_four_gram': survivalRateFourGram,212'survival_rate_no_revert': survivalRateNoRevert,213'time_delay_ms': timeDelayMs,214'did_branch_change': didBranchChange,215'request_id': requestId,216...workspaceMetadataToOTelAttributes(workspace),217});218}219220export function emitUserFeedbackEvent(221otel: IOTelService,222rating: string,223participant: string,224conversationId: string,225requestId: string,226): void {227otel.emitLogRecord(`copilot_chat.user.feedback: ${rating}`, {228'event.name': 'copilot_chat.user.feedback',229'rating': rating,230'participant': participant,231'conversation_id': conversationId,232'request_id': requestId,233});234}235236export function emitCloudSessionInvokeEvent(237otel: IOTelService,238partnerAgent: string,239model: string,240requestId: string,241): void {242otel.emitLogRecord(`copilot_chat.cloud.session.invoke: ${partnerAgent}`, {243'event.name': 'copilot_chat.cloud.session.invoke',244'partner_agent': partnerAgent,245'model': model,246'request_id': requestId,247});248}249250251