Skip to content

Commit 3b22dc1

Browse files
committed
Tool and CallbackManager subscriber
1 parent 5e5bcbc commit 3b22dc1

File tree

9 files changed

+228
-24
lines changed

9 files changed

+228
-24
lines changed

lib/instrumentation/langchain/nr-hooks.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
const toolsInstrumentation = require('./tools')
88
const cbManagerInstrumentation = require('./callback-manager')
99
const runnableInstrumentation = require('./runnable')
10-
const vectorstoreInstrumentation = require('./vectorstore')
1110
const InstrumentationDescriptor = require('../../instrumentation-descriptor')
1211

1312
module.exports = [
@@ -33,10 +32,5 @@ module.exports = [
3332
type: InstrumentationDescriptor.TYPE_GENERIC,
3433
moduleName: '@langchain/core/runnables',
3534
onRequire: runnableInstrumentation
36-
},
37-
{
38-
type: InstrumentationDescriptor.TYPE_GENERIC,
39-
moduleName: '@langchain/core/vectorstores',
40-
onRequire: vectorstoreInstrumentation
4135
}
4236
]

lib/subscribers/base.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,8 +374,13 @@ class Subscriber {
374374
* Subscribes to the events defined in the `events` array.
375375
*/
376376
subscribe() {
377+
if (!this.events || !Array.isArray(this.events)) {
378+
this.logger.debug('Subscriber does not have valid events to subscribe to.')
379+
}
377380
this.subscriptions = this.events.reduce((events, curr) => {
378-
events[curr] = this[curr].bind(this)
381+
try { events[curr] = this[curr].bind(this) } catch {
382+
this.logger.debug(`Failed to bind subscriber event '${curr}'.`)
383+
}
379384
return events
380385
}, {})
381386

lib/subscribers/langchain/base.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,18 @@ const Subscriber = require('../base')
88
const {
99
AI: { LANGCHAIN }
1010
} = require('../../metrics/names')
11-
// const { LangChainVectorSearch, LangChainVectorSearchResult } = require('../../llm-events/langchain')
12-
// const { DESTINATIONS } = require('../../config/attribute-filter')
13-
// const LlmErrorMessage = require('../../llm-events/error-message')
1411
const { extractLlmContext } = require('../../util/llm-utils')
1512

1613
class LangchainSubscriber extends Subscriber {
17-
constructor({ agent, logger, channelName, packageName }) {
18-
super({ agent, logger, channelName, packageName })
14+
constructor({ agent, logger, channelName }) {
15+
super({ agent, logger, channelName, packageName: '@langchain/core' })
1916
}
2017

2118
get enabled() {
19+
if (!this.agent?.config?.ai_monitoring?.enabled) {
20+
this.logger.debug('langchain instrumentation is disabled. To enable set `config.ai_monitoring.enabled` to true')
21+
}
2222
return super.enabled && this.agent?.config?.ai_monitoring?.enabled
23-
// TODO: should we log this?
24-
// 'langchain instrumentation is disabled. To enable set `config.ai_monitoring.enabled` to true'
2523
}
2624

2725
/**
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const LangchainCallbackManagerSubscriber = require('./tool-callback-manager')
7+
8+
class LangchainChainCallbackManagerSubscriber extends LangchainCallbackManagerSubscriber {
9+
constructor({ agent, logger }) {
10+
super({ agent, logger, channelName: 'nr_handleChainStart' })
11+
}
12+
}
13+
14+
module.exports = LangchainChainCallbackManagerSubscriber

lib/subscribers/langchain/config.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,78 @@ const similaritySearch = {
2727
]
2828
}
2929

30+
const toolCall = {
31+
path: './langchain/tool.js',
32+
instrumentations: [
33+
// CommonJs
34+
{
35+
channelName: 'nr_call',
36+
module: { name: '@langchain/core', versionRange: '>=1.0.0', filePath: 'dist/tools/index.cjs' },
37+
functionQuery: {
38+
methodName: 'call',
39+
kind: 'Async'
40+
}
41+
},
42+
// ESM
43+
{
44+
channelName: 'nr_call',
45+
module: { name: '@langchain/core', versionRange: '>=1.0.0', filePath: 'dist/tools/index.js' },
46+
functionQuery: {
47+
methodName: 'call',
48+
kind: 'Async'
49+
}
50+
},
51+
]
52+
}
53+
54+
const toolCallbackManager = {
55+
path: './langchain/tool-callback-manager.js',
56+
instrumentations: [
57+
// CommonJs
58+
{
59+
channelName: 'nr_handleToolStart',
60+
module: { name: '@langchain/core', versionRange: '>=1.0.0', filePath: 'dist/callbacks/manager.cjs' },
61+
functionQuery: {
62+
methodName: 'handleToolStart',
63+
kind: 'Async'
64+
}
65+
},
66+
// ESM
67+
{
68+
channelName: 'nr_handleToolStart',
69+
module: { name: '@langchain/core', versionRange: '>=1.0.0', filePath: 'dist/callbacks/manager.js' },
70+
functionQuery: {
71+
methodName: 'handleToolStart',
72+
kind: 'Async'
73+
}
74+
},
75+
]
76+
}
77+
78+
const chainCallbackManager = {
79+
path: './langchain/chain-callback-manager.js',
80+
instrumentations: [
81+
// CommonJs
82+
{
83+
channelName: 'nr_handleChainStart',
84+
module: { name: '@langchain/core', versionRange: '>=1.0.0', filePath: 'dist/callbacks/manager.cjs' },
85+
functionQuery: {
86+
methodName: 'handleChainStart',
87+
kind: 'Async'
88+
}
89+
},
90+
// ESM
91+
{
92+
channelName: 'nr_handleChainStart',
93+
module: { name: '@langchain/core', versionRange: '>=1.0.0', filePath: 'dist/callbacks/manager.js' },
94+
functionQuery: {
95+
methodName: 'handleChainStart',
96+
kind: 'Async'
97+
}
98+
},
99+
]
100+
}
101+
30102
module.exports = {
31-
'@langchain/core': [similaritySearch]
103+
'@langchain/core': [similaritySearch, toolCall, toolCallbackManager, chainCallbackManager]
32104
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const LangchainSubscriber = require('./base')
7+
const { langchainRunId } = require('../../symbols')
8+
9+
class LangchainCallbackManagerSubscriber extends LangchainSubscriber {
10+
constructor({ agent, logger }) {
11+
super({ agent, logger, channelName: 'nr_handleToolStart' })
12+
this.events = ['asyncEnd']
13+
}
14+
15+
asyncEnd(data) {
16+
const { result } = data
17+
const ctx = this.agent.tracer.getContext()
18+
const { segment } = ctx
19+
if (segment) {
20+
segment[langchainRunId] = result?.runId
21+
}
22+
}
23+
}
24+
25+
module.exports = LangchainCallbackManagerSubscriber

lib/subscribers/langchain/tool.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2025 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const LangchainSubscriber = require('./base')
8+
const {
9+
AI: { LANGCHAIN }
10+
} = require('../../metrics/names')
11+
const { langchainRunId } = require('../../symbols')
12+
const { LangChainTool } = require('../../llm-events/langchain')
13+
const LlmErrorMessage = require('../../llm-events/error-message')
14+
const { DESTINATIONS } = require('../../config/attribute-filter')
15+
16+
class LangchainToolSubscriber extends LangchainSubscriber {
17+
constructor({ agent, logger }) {
18+
super({ agent, logger, channelName: 'nr_call' })
19+
this.events = ['asyncEnd']
20+
}
21+
22+
handler(data, ctx) {
23+
if (!this.enabled) {
24+
// We need this check inside the wrapper because it is possible for monitoring
25+
// to be disabled at the account level. In such a case, the value is set
26+
// after the instrumentation has been initialized.
27+
return ctx
28+
}
29+
const tool = data?.self
30+
31+
const segment = this.agent.tracer.createSegment({
32+
name: `${LANGCHAIN.TOOL}/${tool?.name}`,
33+
parent: ctx.segment,
34+
transaction: ctx.transaction
35+
})
36+
return ctx.enterSegment({ segment })
37+
}
38+
39+
asyncEnd(data) {
40+
const { moduleVersion: pkgVersion, result, error: err } = data
41+
const { name, metadata: instanceMeta, description, tags: instanceTags } = data?.self
42+
const request = data?.arguments?.[0]
43+
const params = data?.arguments?.[1] || {}
44+
const { metadata: paramsMeta, tags: paramsTags } = params
45+
const metadata = this.mergeMetadata(instanceMeta, paramsMeta)
46+
const tags = this.mergeTags(instanceTags, paramsTags)
47+
48+
const { agent } = this
49+
const ctx = agent.tracer.getContext()
50+
const { segment, transaction } = ctx
51+
if (transaction?.isActive() !== true) {
52+
return
53+
}
54+
segment.end()
55+
56+
if (!this.enabled) {
57+
// We need this check inside the wrapper because it is possible for monitoring
58+
// to be disabled at the account level. In such a case, the value is set
59+
// after the instrumentation has been initialized.
60+
this.logger.debug('skipping sending of ai data')
61+
return
62+
}
63+
64+
const toolEvent = new LangChainTool({
65+
agent,
66+
description,
67+
name,
68+
runId: segment[langchainRunId],
69+
metadata,
70+
transaction,
71+
tags,
72+
input: request?.input,
73+
output: result,
74+
segment,
75+
error: err != null
76+
})
77+
this.recordEvent({ type: 'LlmTool', pkgVersion, msg: toolEvent })
78+
79+
if (err) {
80+
agent.errors.add(
81+
transaction,
82+
err,
83+
new LlmErrorMessage({
84+
response: {},
85+
cause: err,
86+
tool: toolEvent
87+
})
88+
)
89+
}
90+
91+
transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
92+
}
93+
}
94+
95+
module.exports = LangchainToolSubscriber

lib/subscribers/langchain/vectorstore.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const { DESTINATIONS } = require('../../config/attribute-filter')
1414

1515
class LangchainVectorstoreSubscriber extends LangchainSubscriber {
1616
constructor({ agent, logger }) {
17-
super({ agent, logger, packageName: '@langchain/core', channelName: 'nr_similaritySearch' })
17+
super({ agent, logger, channelName: 'nr_similaritySearch' })
1818
this.events = ['asyncEnd']
1919
}
2020

@@ -103,14 +103,6 @@ class LangchainVectorstoreSubscriber extends LangchainSubscriber {
103103
}
104104

105105
asyncEnd(data) {
106-
if (!this.enabled) {
107-
// We need this check inside the wrapper because it is possible for monitoring
108-
// to be disabled at the account level. In such a case, the value is set
109-
// after the instrumentation has been initialized.
110-
this.logger.debug('skipping sending of ai data')
111-
return
112-
}
113-
114106
const request = data?.arguments[0]
115107
const k = data?.arguments[1]
116108
// If we get an error, it is possible that `result = null`.
@@ -123,6 +115,14 @@ class LangchainVectorstoreSubscriber extends LangchainSubscriber {
123115
}
124116
ctx.segment.end()
125117

118+
if (!this.enabled) {
119+
// We need this check inside the wrapper because it is possible for monitoring
120+
// to be disabled at the account level. In such a case, the value is set
121+
// after the instrumentation has been initialized.
122+
this.logger.debug('skipping sending of ai data')
123+
return
124+
}
125+
126126
this.recordVectorSearch({
127127
request,
128128
k,

lib/tracking-packages.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
const trackingPkgs = [
1717
'@azure/openai',
18+
'@langchain/core/tools',
1819
'@langchain/core/vectorstores',
1920
'@langchain/community/llms/bedrock',
2021
'fancy-log',

0 commit comments

Comments
 (0)