Skip to content

Commit

Permalink
feat: Add instrumentation for RunnableSequence (#1996)
Browse files Browse the repository at this point in the history
Co-authored-by: Bob Evans <robert.evans25@gmail.com>
Co-authored-by: Bob Evans <revans@newrelic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: James Sumners <jsumners@newrelic.com>
  • Loading branch information
5 people committed Feb 22, 2024
1 parent ead8c25 commit 71ffa37
Show file tree
Hide file tree
Showing 10 changed files with 664 additions and 11 deletions.
6 changes: 6 additions & 0 deletions lib/instrumentation/langchain/nr-hooks.js
Expand Up @@ -6,6 +6,7 @@
'use strict'
const toolsInstrumentation = require('./tools')
const cbManagerInstrumentation = require('./callback-manager')
const runnableInstrumentation = require('./runnable')

module.exports = [
{
Expand All @@ -17,5 +18,10 @@ module.exports = [
type: 'generic',
moduleName: '@langchain/core/dist/callbacks/manager',
onRequire: cbManagerInstrumentation
},
{
type: 'generic',
moduleName: '@langchain/core/dist/runnables/base',
onRequire: runnableInstrumentation
}
]
98 changes: 98 additions & 0 deletions lib/instrumentation/langchain/runnable.js
@@ -0,0 +1,98 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const common = require('./common')
const {
AI: { LANGCHAIN }
} = require('../../metrics/names')
const {
LangChainCompletionMessage,
LangChainCompletionSummary
} = require('../../llm-events/langchain/')
const { DESTINATIONS } = require('../../config/attribute-filter')
const { langchainRunId } = require('../../symbols')
const { RecorderSpec } = require('../../shim/specs')

module.exports = function initialize(shim, langchain) {
const { agent, pkgVersion } = shim

if (common.shouldSkipInstrumentation(agent.config)) {
shim.logger.debug(
'langchain instrumentation is disabled. To enable set `config.ai_monitoring.enabled` to true'
)
return
}

shim.record(
langchain.RunnableSequence.prototype,
'invoke',
function wrapCall(shim, invoke, fnName, args) {
const [request, params] = args
const metadata = params?.metadata ?? {}
const tags = params?.tags ?? []

return new RecorderSpec({
name: `${LANGCHAIN.CHAIN}/${fnName}`,
promise: true,
// eslint-disable-next-line max-params
after(_shim, _fn, _name, _err, output, segment) {
segment.end()
const completionSummary = new LangChainCompletionSummary({
agent,
messages: [{ output }],
metadata,
tags,
segment,
runId: segment[langchainRunId]
})

common.recordEvent({
agent,
type: 'LlmChatCompletionSummary',
pkgVersion,
msg: completionSummary
})

const data = [request, output]

// output can be BaseMessage with a content property https://js.langchain.com/docs/modules/model_io/concepts#messages
// or an output parser https://js.langchain.com/docs/modules/model_io/concepts#output-parsers
data.forEach((msg, sequence) => {
if (msg?.content) {
msg = msg.content
}

let msgString
try {
msgString = typeof msg === 'string' ? msg : JSON.stringify(msg)
} catch (err) {
shim.logger.error(err, 'Failed to stringify message')
msgString = ''
}

const completionMsg = new LangChainCompletionMessage({
sequence,
agent,
content: msgString,
completionId: completionSummary.id,
segment,
runId: segment[langchainRunId]
})

common.recordEvent({
agent,
type: 'LlmChatCompletionMessage',
pkgVersion,
msg: completionMsg
})
})
segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
}
})
}
)
}
4 changes: 0 additions & 4 deletions lib/llm-events/langchain/chat-completion-message.js
Expand Up @@ -12,7 +12,6 @@ const { makeId } = require('../../util/hashes')
* @typedef {object} LangChainCompletionMessageParams
* @augments LangChainEventParams
* @property {string} content The text of the response received from LangChain.
* @property {string} role The role of the message, e.g. "human."
* @property {number} [sequence=0] The order of the message in the response.
* @property {string} [completionId] An identifier for the message.
*/
Expand All @@ -21,14 +20,12 @@ const { makeId } = require('../../util/hashes')
*/
const defaultParams = {
content: '',
role: undefined,
sequence: 0,
completionId: makeId(36)
}

class LangChainCompletionMessage extends LangChainEvent {
content
role
sequence
completion_id

Expand All @@ -43,7 +40,6 @@ class LangChainCompletionMessage extends LangChainEvent {
}

this.content = params.content
this.role = params.role
this.sequence = params.sequence
this.completion_id = params.completionId
}
Expand Down
6 changes: 4 additions & 2 deletions lib/metrics/names.js
Expand Up @@ -169,7 +169,8 @@ const AI = {
STREAMING_DISABLED: `${SUPPORTABILITY.NODEJS}/ML/Streaming/Disabled`,
EMBEDDING: 'Llm/embedding',
COMPLETION: 'Llm/completion',
TOOL: 'Llm/tool'
TOOL: 'Llm/tool',
CHAIN: 'Llm/chain'
}

AI.OPENAI = {
Expand All @@ -182,7 +183,8 @@ AI.LANGCHAIN = {
TRACKING_PREFIX: `${AI.TRACKING_PREFIX}/Langchain`,
EMBEDDING: `${AI.EMBEDDING}/Langchain`,
COMPLETION: `${AI.COMPLETION}/Langchain`,
TOOL: `${AI.TOOL}/Langchain`
TOOL: `${AI.TOOL}/Langchain`,
CHAIN: `${AI.CHAIN}/Langchain`
}

const RESTIFY = {
Expand Down
68 changes: 68 additions & 0 deletions test/unit/instrumentation/langchain/runnables.test.js
@@ -0,0 +1,68 @@
/*
* Copyright 2023 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const { test } = require('tap')
const helper = require('../../../lib/agent_helper')
const GenericShim = require('../../../../lib/shim/shim')
const sinon = require('sinon')

test('langchain/core/runnables unit tests', (t) => {
t.beforeEach(function (t) {
const sandbox = sinon.createSandbox()
const agent = helper.loadMockedAgent()
agent.config.ai_monitoring = { enabled: true }
agent.config.feature_flag = { langchain_instrumentation: true }
const shim = new GenericShim(agent, 'langchain')
shim.pkgVersion = '0.1.26'
sandbox.stub(shim.logger, 'debug')
sandbox.stub(shim.logger, 'warn')

t.context.agent = agent
t.context.shim = shim
t.context.sandbox = sandbox
t.context.initialize = require('../../../../lib/instrumentation/langchain/runnable')
})

t.afterEach(function (t) {
helper.unloadAgent(t.context.agent)
t.context.sandbox.restore()
})

function getMockModule() {
function RunnableSequence() {}
RunnableSequence.prototype.invoke = async function call() {}
return { RunnableSequence }
}

;[
{ aiMonitoring: false, langChain: true },
{ aiMonitoring: true, langChain: false },
{ aiMonitoring: false, langChain: false }
].forEach(({ aiMonitoring, langChain }) => {
t.test(
`should not register instrumentation if ai_monitoring is ${aiMonitoring} and langchain_instrumentation is ${langChain}`,
(t) => {
const { shim, agent, initialize } = t.context
const MockRunnable = getMockModule()
agent.config.ai_monitoring.enabled = aiMonitoring
agent.config.feature_flag.langchain_instrumentation = langChain

initialize(shim, MockRunnable)
t.equal(shim.logger.debug.callCount, 1, 'should log 1 debug messages')
t.equal(
shim.logger.debug.args[0][0],
'langchain instrumentation is disabled. To enable set `config.ai_monitoring.enabled` to true'
)
const isWrapped = shim.isWrapped(MockRunnable.RunnableSequence.prototype.invoke)
t.equal(isWrapped, false, 'should not wrap runnable invoke')
t.end()
}
)
})

t.end()
})
Expand Up @@ -49,7 +49,6 @@ tap.beforeEach((t) => {
tap.test('creates entity', async (t) => {
const msg = new LangChainCompletionMessage({
...t.context,
role: 'human',
sequence: 1,
content: 'hello world'
})
Expand All @@ -65,7 +64,6 @@ tap.test('creates entity', async (t) => {
ingest_source: 'Node',
vendor: 'langchain',
virtual_llm: true,
role: 'human',
sequence: 1,
content: 'hello world',
completion_id: /[a-z0-9-]{36}/
Expand Down
91 changes: 91 additions & 0 deletions test/versioned/langchain/common.js
@@ -0,0 +1,91 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'

const tap = require('tap')

function filterLangchainEvents(events) {
return events.filter((event) => {
const [, chainEvent] = event
return chainEvent.vendor === 'langchain'
})
}

function filterLangchainMessages(events, msgType) {
return events.filter((event) => {
const [{ type }] = event
return type === msgType
})
}

function assertLangChainChatCompletionSummary({ tx, chatSummary, withCallback }) {
const expectedSummary = {
'id': /[a-f0-9]{36}/,
'appName': 'New Relic for Node.js tests',
'span_id': tx.trace.root.children[0].id,
'trace_id': tx.traceId,
'transaction_id': tx.id,
'request_id': undefined,
'ingest_source': 'Node',
'vendor': 'langchain',
'metadata.key': 'value',
'metadata.hello': 'world',
'tags': 'tag1,tag2',
'virtual_llm': true,
['response.number_of_messages']: 1,
'duration': tx.trace.root.children[0].getDurationInMillis()
}

if (withCallback) {
expectedSummary.request_id = /[a-f0-9\-]{36}/
expectedSummary.id = /[a-f0-9\-]{36}/
}

this.equal(chatSummary[0].type, 'LlmChatCompletionSummary')
this.match(chatSummary[1], expectedSummary, 'should match chat summary message')
}

function assertLangChainChatCompletionMessages({ tx, chatMsgs, chatSummary, withCallback }) {
const baseMsg = {
id: /[a-f0-9]{36}/,
appName: 'New Relic for Node.js tests',
span_id: tx.trace.root.children[0].id,
trace_id: tx.traceId,
transaction_id: tx.id,
ingest_source: 'Node',
vendor: 'langchain',
completion_id: chatSummary.id,
virtual_llm: true,
request_id: undefined
}

if (withCallback) {
baseMsg.request_id = /[a-f0-9\-]{36}/
baseMsg.id = /[a-f0-9\-]{36}/
}

chatMsgs.forEach((msg) => {
const expectedChatMsg = { ...baseMsg }
if (msg[1].sequence === 0) {
expectedChatMsg.sequence = 0
expectedChatMsg.content = '{"topic":"scientist"}'
} else if (msg[1].sequence === 1) {
expectedChatMsg.sequence = 1
expectedChatMsg.content = '212 degrees Fahrenheit is equal to 100 degrees Celsius.'
}

this.equal(msg[0].type, 'LlmChatCompletionMessage')
this.match(msg[1], expectedChatMsg, 'should match chat completion message')
})
}

tap.Test.prototype.addAssert('langchainMessages', 1, assertLangChainChatCompletionMessages)
tap.Test.prototype.addAssert('langchainSummary', 1, assertLangChainChatCompletionSummary)

module.exports = {
filterLangchainEvents,
filterLangchainMessages
}
6 changes: 4 additions & 2 deletions test/versioned/langchain/package.json
Expand Up @@ -11,10 +11,12 @@
"node": ">=18"
},
"dependencies": {
"@langchain/core": ">=0.1.17"
"@langchain/core": ">=0.1.17",
"@langchain/openai": "latest"
},
"files": [
"tools.tap.js"
"tools.tap.js",
"runnables.tap.js"
]
}
]
Expand Down

0 comments on commit 71ffa37

Please sign in to comment.