UNPKG

newrelic

Version:
319 lines (296 loc) 10.1 kB
/* * Copyright 2026 New Relic Corporation. All rights reserved. * SPDX-License-Identifier: Apache-2.0 */ 'use strict' const { LlmChatCompletionMessage, LlmChatCompletionSummary, LlmEmbedding, LlmErrorMessage, BedrockResponse } = require('#agentlib/llm-events/aws-bedrock/index.js') const { DESTINATIONS } = require('#agentlib/config/attribute-filter.js') const StreamHandler = require('./stream-handler.js') const ConverseStreamHandler = require('./converse-stream-handler.js') const { extractLlmContext } = require('#agentlib/util/llm-utils.js') const { STREAMING_COMMANDS } = require('./constants') let TRACKING_METRIC /** * Defers the creation of the `TRACKING_METRIC` constant * * @param {string} metric name of metric */ function setTrackingMetric(metric) { TRACKING_METRIC = metric } /** * Checks if ai_monitoring is enabled * @param {object} config agent config * @returns {boolean} if ai monitoring is enabled */ function shouldSkipInstrumentation(config) { return config.ai_monitoring.enabled === false } /** * Checks if streaming is enabled * @param {object} params to fn * @param {string} params.commandName name of command * @param {object} params.config agent config * @returns {boolean} if streaming is enabled */ function isStreamingEnabled({ commandName, config }) { return ( STREAMING_COMMANDS.has(commandName) && config.ai_monitoring?.streaming?.enabled ) } /** * Records a custom event with LLM context * @param {object} params Parameters to function * @param {Agent} params.agent New Relic agent instance * @param {string} params.type Event type * @param {object} params.msg Message object to record */ function recordEvent({ agent, type, msg }) { const llmContext = extractLlmContext(agent) const timestamp = msg?.timestamp ?? Date.now() agent.customEventAggregator.add([ { type, timestamp }, Object.assign({}, msg, llmContext) ]) } /** * Adds LLM metadata to metrics, transaction attributes, and ends the segment * @param {object} params Parameters to function * @param {Agent} params.agent New Relic agent instance * @param {TraceSegment} params.segment Current segment * @param {Transaction} params.transaction Current transaction */ function addLlmMeta({ agent, segment, transaction }) { agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount() transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) segment.end() } /** * Records chat completion messages and summary for LLM monitoring. * Creates message events for both prompts and completions, and records errors if present. * @param {object} params Parameters to function * @param {Agent} params.agent New Relic agent instance * @param {Logger} params.logger Logger instance * @param {TraceSegment} params.segment Current segment * @param {Transaction} params.transaction Current transaction * @param {BedrockCommand} params.bedrockCommand BedrockCommand object * @param {BedrockResponse} params.bedrockResponse BedrockResponse object * @param {Error} [params.err] Error object if it exists * @param {number} [params.timeOfFirstToken] Timestamp of first token for streaming responses */ function recordChatCompletionMessages({ agent, logger, segment, transaction, bedrockCommand, bedrockResponse, err, timeOfFirstToken }) { if (shouldSkipInstrumentation(agent.config) === true) { logger.debug('skipping sending of ai data') return } const summary = new LlmChatCompletionSummary({ agent, bedrockResponse, bedrockCommand, transaction, segment, timeOfFirstToken, error: err !== null }) const promptContextMessages = bedrockCommand.prompt for (let i = 0; i < promptContextMessages.length; i++) { const contextMessage = promptContextMessages[i] const msg = new LlmChatCompletionMessage({ agent, segment, transaction, bedrockCommand, content: contextMessage.content, role: contextMessage.role, bedrockResponse, sequence: i, completionId: summary.id }) recordEvent({ agent, type: 'LlmChatCompletionMessage', msg }) } for (let i = 0; i < bedrockResponse.completions.length; i++) { const content = bedrockResponse.completions[i] const chatCompletionMessage = new LlmChatCompletionMessage({ agent, segment, transaction, bedrockCommand, bedrockResponse, isResponse: true, sequence: promptContextMessages.length + i, content, role: 'assistant', completionId: summary.id }) recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: chatCompletionMessage }) } recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: summary }) if (err) { const llmErrorMessage = new LlmErrorMessage({ response: bedrockResponse, cause: err, summary }) agent.errors.add(transaction, err, llmErrorMessage) } } /** * Records embedding messages for LLM monitoring. * Creates embedding events for each prompt and records errors if present. * @param {object} params Parameters to function * @param {Agent} params.agent New Relic agent instance * @param {Logger} params.logger Logger instance * @param {TraceSegment} params.segment Current segment * @param {Transaction} params.transaction Current transaction * @param {BedrockCommand} params.bedrockCommand BedrockCommand object * @param {BedrockResponse} params.bedrockResponse BedrockResponse object * @param {Error} [params.err] Error object if it exists */ function recordEmbeddingMessage({ agent, logger, segment, transaction, bedrockCommand, bedrockResponse, err }) { if (shouldSkipInstrumentation(agent.config) === true) { logger.debug('skipping sending of ai data') return } const embeddings = bedrockCommand.prompt.map((prompt) => new LlmEmbedding({ agent, segment, transaction, bedrockCommand, requestInput: prompt.content, bedrockResponse, error: err !== null })) for (const embedding of embeddings) { recordEvent({ agent, type: 'LlmEmbedding', msg: embedding }) } if (err) { const llmErrorMessage = new LlmErrorMessage({ response: bedrockResponse, cause: err, embedding: embeddings.length === 1 ? embeddings[0] : undefined }) agent.errors.add(transaction, err, llmErrorMessage) } } /** * Creates a BedrockResponse object from the command and response/error * @param {object} params Parameters to function * @param {BedrockCommand} params.bedrockCommand BedrockCommand object * @param {object} params.response AWS Bedrock response object * @param {Error} [params.err] Error object if it exists * @returns {BedrockResponse} BedrockResponse object */ function createBedrockResponse({ bedrockCommand, response, err }) { if (err) { return new BedrockResponse({ bedrockCommand, response: err, isError: err !== null }) } return new BedrockResponse({ bedrockCommand, response }) } /** * Handles the response (or error) from a bedrock call. * Called directly for non-streamed responses, or as onComplete * callback from StreamHandler/ConverseStreamHandler. * * Note: `this` is bound to the stream handler instance when called * as onComplete, providing `this.stopReason` and `this.timeOfFirstToken`. * @param {object} params Parameters to function * @param {Agent} params.agent New Relic agent instance * @param {Logger} params.logger Logger instance * @param {object} [params.err] Error object if it exists * @param {object} params.response AWS Bedrock reponse object * @param {TraceSegment} params.segment Current segment * @param {Transaction} params.transaction Current transaction * @param {BedrockCommand} params.bedrockCommand BedrockCommand object * @param {string} params.modelType AWS Bedrock model type */ function handleResponse({ agent, logger, err, response, segment, transaction, bedrockCommand, modelType }) { if (response?.output && this?.stopReason) { response.output.stopReason = this.stopReason } const bedrockResponse = createBedrockResponse({ bedrockCommand, response, err }) addLlmMeta({ agent, segment, transaction }) if (modelType === 'completion') { recordChatCompletionMessages({ agent, logger, segment, transaction, bedrockCommand, bedrockResponse, timeOfFirstToken: this?.timeOfFirstToken, err }) } else if (modelType === 'embedding') { recordEmbeddingMessage({ agent, logger, segment, transaction, bedrockCommand, bedrockResponse, err }) } } /** * Handles streaming responses by wrapping the stream with appropriate handler. * Uses ConverseStreamHandler for Converse API streams, otherwise uses StreamHandler. * @param {object} passThroughParams Parameters passed through to stream handlers * @param {BedrockCommand} passThroughParams.bedrockCommand BedrockCommand object * @param {object} passThroughParams.response AWS Bedrock response object * @param {Agent} passThroughParams.agent New Relic agent instance * @param {Logger} passThroughParams.logger Logger instance * @param {TraceSegment} passThroughParams.segment Current segment * @param {Transaction} passThroughParams.transaction Current transaction * @param {string} passThroughParams.modelType AWS Bedrock model type */ function handleStream(passThroughParams) { const { bedrockCommand, response } = passThroughParams if (bedrockCommand.isConverse) { const handler = new ConverseStreamHandler({ stream: response.output.stream, onComplete: handleResponse, passThroughParams }) response.output.stream = handler.generator(handleResponse) } else { const handler = new StreamHandler({ stream: response.output.body, onComplete: handleResponse, passThroughParams }) response.output.body = handler.generator(handleResponse) } } module.exports = { addLlmMeta, handleStream, handleResponse, isStreamingEnabled, setTrackingMetric, shouldSkipInstrumentation }