Skip to content

Commit 2244cf0

Browse files
sabrennertlhunter
andauthored
feat(langchain, llmobs): expand langchain support for tools and vectorstores (#5760)
* minimal apm spans * add llmobs plugins * tool io refactor * add tool tests * apm vectorstore tests * fix peer dependencies issue * switch to agent.assertSomeTraces * llmobs vectorstores tests * remove .only * remove promise.all * lint * do not need to format i/o for tools --------- Co-authored-by: Thomas Hunter II <tlhunter@datadog.com>
1 parent 765635b commit 2244cf0

File tree

10 files changed

+566
-17
lines changed

10 files changed

+566
-17
lines changed

packages/datadog-instrumentations/src/langchain.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ for (const extension of extensions) {
5353
return exports
5454
})
5555

56+
addHook({ name: '@langchain/core', file: `dist/tools/index.${extension}`, versions: ['>=0.1'] }, exports => {
57+
if (extension === 'cjs') {
58+
wrap(exports.StructuredTool.prototype, 'invoke', 'orchestrion:@langchain/core:Tool_invoke')
59+
}
60+
return exports
61+
})
62+
63+
addHook({ name: '@langchain/core', file: `dist/vectorstores.${extension}`, versions: ['>=0.1'] }, exports => {
64+
if (extension === 'cjs') {
65+
wrap(
66+
exports.VectorStore.prototype, 'similaritySearch', 'orchestrion:@langchain/core:VectorStore_similaritySearch'
67+
)
68+
wrap(
69+
exports.VectorStore.prototype, 'similaritySearchWithScore',
70+
'orchestrion:@langchain/core:VectorStore_similaritySearchWithScore'
71+
)
72+
}
73+
74+
return exports
75+
})
76+
5677
addHook({ name: '@langchain/core', file: `dist/embeddings.${extension}`, versions: ['>=0.1'] }, exports => {
5778
if (extension === 'cjs') {
5879
shimmer.wrap(exports, 'Embeddings', Embeddings => {

packages/datadog-instrumentations/src/orchestrion-config/index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,34 @@ instrumentations:
5353
class: Embeddings
5454
operator: traceSync
5555
channel_name: "Embeddings_constructor"
56+
- module_name: "@langchain/core"
57+
version_range: ">=0.1.0"
58+
file_path: dist/tools/index.js
59+
function_query:
60+
name: invoke
61+
type: method
62+
kind: async
63+
class: StructuredTool
64+
operator: tracePromise
65+
channel_name: "Tool_invoke"
66+
- module_name: "@langchain/core"
67+
version_range: ">=0.1.0"
68+
file_path: dist/vectorstores.js
69+
function_query:
70+
name: similaritySearch
71+
type: method
72+
kind: async
73+
class: VectorStore
74+
operator: tracePromise
75+
channel_name: "VectorStore_similaritySearch"
76+
- module_name: "@langchain/core"
77+
version_range: ">=0.1.0"
78+
file_path: dist/vectorstores.js
79+
function_query:
80+
name: similaritySearchWithScore
81+
type: method
82+
kind: async
83+
class: VectorStore
84+
operator: tracePromise
85+
channel_name: "VectorStore_similaritySearchWithScore"
5686
`

packages/datadog-plugin-langchain/src/tracing.js

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ class BaseLangChainTracingPlugin extends TracingPlugin {
4040

4141
// Runnable interfaces have an `lc_namespace` property
4242
const ns = ctx.self.lc_namespace || ctx.namespace
43-
const resource = ctx.resource = [...ns, ctx.self.constructor.name].join('.')
4443

45-
const handler = this.handlers[type]
44+
const resourceParts = [...ns, ctx.self.constructor.name]
45+
if (type === 'tool') {
46+
resourceParts.push(ctx.instance.name)
47+
}
48+
const resource = ctx.resource = resourceParts.join('.')
49+
50+
const handler = this.handlers[type] || this.handlers.default
4651

4752
const instance = ctx.instance
4853
const apiKey = handler.extractApiKey(instance)
@@ -78,7 +83,7 @@ class BaseLangChainTracingPlugin extends TracingPlugin {
7883

7984
const { type } = ctx
8085

81-
const handler = this.handlers[type]
86+
const handler = this.handlers[type] || this.handlers.default
8287
const tags = handler.getSpanEndTags(ctx, span) || {}
8388

8489
span.addTags(tags)
@@ -139,11 +144,38 @@ class EmbeddingsEmbedDocumentsPlugin extends BaseLangChainTracingPlugin {
139144
}
140145
}
141146

147+
class ToolInvokePlugin extends BaseLangChainTracingPlugin {
148+
static get id () { return 'langchain_tool_invoke' }
149+
static get lcType () { return 'tool' }
150+
static get prefix () {
151+
return 'tracing:orchestrion:@langchain/core:Tool_invoke'
152+
}
153+
}
154+
155+
class VectorStoreSimilaritySearchPlugin extends BaseLangChainTracingPlugin {
156+
static get id () { return 'langchain_vectorstore_similarity_search' }
157+
static get lcType () { return 'similarity_search' }
158+
static get prefix () {
159+
return 'tracing:orchestrion:@langchain/core:VectorStore_similaritySearch'
160+
}
161+
}
162+
163+
class VectorStoreSimilaritySearchWithScorePlugin extends BaseLangChainTracingPlugin {
164+
static get id () { return 'langchain_vectorstore_similarity_search_with_score' }
165+
static get lcType () { return 'similarity_search' }
166+
static get prefix () {
167+
return 'tracing:orchestrion:@langchain/core:VectorStore_similaritySearchWithScore'
168+
}
169+
}
170+
142171
module.exports = [
143172
RunnableSequenceInvokePlugin,
144173
RunnableSequenceBatchPlugin,
145174
BaseChatModelGeneratePlugin,
146175
BaseLLMGeneratePlugin,
147176
EmbeddingsEmbedQueryPlugin,
148-
EmbeddingsEmbedDocumentsPlugin
177+
EmbeddingsEmbedDocumentsPlugin,
178+
ToolInvokePlugin,
179+
VectorStoreSimilaritySearchPlugin,
180+
VectorStoreSimilaritySearchWithScorePlugin
149181
]

packages/datadog-plugin-langchain/test/index.spec.js

Lines changed: 162 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@ const openAiBaseEmbeddingInfo = { base: 'https://api.openai.com', path: '/v1/emb
1919

2020
const isDdTrace = iastFilter.isDdTrace
2121

22+
function stubSingleEmbedding (langchainOpenaiOpenAiVersion) {
23+
if (semver.satisfies(langchainOpenaiOpenAiVersion, '>=4.91.0')) {
24+
stubCall({
25+
...openAiBaseEmbeddingInfo,
26+
response: require('./fixtures/single-embedding.json')
27+
})
28+
} else {
29+
stubCall({
30+
...openAiBaseEmbeddingInfo,
31+
response: {
32+
object: 'list',
33+
data: [{
34+
object: 'embedding',
35+
index: 0,
36+
embedding: Array(1536).fill(0)
37+
}]
38+
}
39+
})
40+
}
41+
}
42+
2243
describe('Plugin', () => {
2344
let langchainOpenai
2445
let langchainAnthropic
@@ -28,7 +49,8 @@ describe('Plugin', () => {
2849
let langchainOutputParsers
2950
let langchainPrompts
3051
let langchainRunnables
31-
52+
let langchainTools
53+
let MemoryVectorStore
3254
/**
3355
* In OpenAI 4.91.0, the default response format for embeddings was changed from `float` to `base64`.
3456
* We do not have control in @langchain/openai embeddings to change this for an individual call,
@@ -65,7 +87,8 @@ describe('Plugin', () => {
6587
})
6688

6789
beforeEach(() => {
68-
langchainOpenai = require(`../../../versions/@langchain/openai@${version}`).get()
90+
langchainOpenai = require(`../../../versions/langchain@${version}`)
91+
.get('@langchain/openai')
6992
langchainAnthropic = require(`../../../versions/@langchain/anthropic@${version}`).get()
7093
if (version !== '0.1.0') {
7194
// version mismatching otherwise
@@ -80,10 +103,17 @@ describe('Plugin', () => {
80103
langchainPrompts = require(`../../../versions/@langchain/core@${version}`).get('@langchain/core/prompts')
81104
langchainRunnables = require(`../../../versions/@langchain/core@${version}`).get('@langchain/core/runnables')
82105

106+
langchainTools = require(`../../../versions/@langchain/core@${version}`)
107+
.get('@langchain/core/tools')
108+
109+
MemoryVectorStore = require(`../../../versions/@langchain/core@${version}`)
110+
.get('langchain/vectorstores/memory')
111+
.MemoryVectorStore
112+
83113
langchainOpenaiOpenAiVersion =
84-
require(`../../../versions/@langchain/openai@${version}`)
85-
.get('openai/version')
86-
.VERSION
114+
require(`../../../versions/langchain@${version}`)
115+
.get('openai/version')
116+
.VERSION
87117
})
88118

89119
afterEach(() => {
@@ -1013,6 +1043,133 @@ describe('Plugin', () => {
10131043
})
10141044
})
10151045
})
1046+
1047+
describe('tools', () => {
1048+
it('traces a tool call', async function () {
1049+
if (!langchainTools?.tool) this.skip()
1050+
1051+
const myTool = langchainTools.tool(
1052+
() => 'Hello, world!',
1053+
{
1054+
name: 'myTool',
1055+
description: 'A tool that returns a greeting'
1056+
}
1057+
)
1058+
1059+
const checkTraces = agent.assertSomeTraces(traces => {
1060+
const span = traces[0][0]
1061+
1062+
expect(span).to.have.property('name', 'langchain.request')
1063+
expect(span.resource).to.match(/^langchain\.tools\.[^.]+\.myTool$/)
1064+
})
1065+
const result = await myTool.invoke()
1066+
expect(result).to.equal('Hello, world!')
1067+
1068+
await checkTraces
1069+
})
1070+
1071+
it('traces a tool call with an error', async function () {
1072+
if (!langchainTools?.tool) this.skip()
1073+
1074+
const myTool = langchainTools.tool(
1075+
() => { throw new Error('This is a test error') },
1076+
{
1077+
name: 'myTool',
1078+
description: 'A tool that throws an error'
1079+
}
1080+
)
1081+
1082+
const checkTraces = agent.assertSomeTraces(traces => {
1083+
const span = traces[0][0]
1084+
1085+
expect(span).to.have.property('name', 'langchain.request')
1086+
expect(span.resource).to.match(/^langchain\.tools\.[^.]+\.myTool$/)
1087+
1088+
expect(span.meta).to.have.property('error.message')
1089+
expect(span.meta).to.have.property('error.type')
1090+
expect(span.meta).to.have.property('error.stack')
1091+
})
1092+
1093+
try {
1094+
await myTool.invoke()
1095+
expect.fail('Expected an error to be thrown')
1096+
} catch {}
1097+
1098+
await checkTraces
1099+
})
1100+
})
1101+
1102+
describe('vectorstores', () => {
1103+
let vectorstore
1104+
1105+
beforeEach(async () => {
1106+
// need to mock out adding a document to the vectorstore
1107+
stubSingleEmbedding(langchainOpenaiOpenAiVersion)
1108+
1109+
const embeddings = new langchainOpenai.OpenAIEmbeddings()
1110+
vectorstore = new MemoryVectorStore(embeddings)
1111+
1112+
const document = {
1113+
pageContent: 'The powerhouse of the cell is the mitochondria',
1114+
metadata: { source: 'https://example.com' },
1115+
id: '1'
1116+
}
1117+
1118+
return vectorstore.addDocuments([document])
1119+
})
1120+
1121+
it('traces a vectorstore similaritySearch call', async () => {
1122+
stubSingleEmbedding(langchainOpenaiOpenAiVersion)
1123+
1124+
const checkTraces = agent.assertSomeTraces(traces => {
1125+
const spans = traces[0]
1126+
1127+
expect(spans).to.have.length(2)
1128+
1129+
const vectorstoreSpan = spans[0]
1130+
const embeddingSpan = spans[1]
1131+
1132+
expect(vectorstoreSpan).to.have.property('name', 'langchain.request')
1133+
expect(vectorstoreSpan).to.have.property('resource', 'langchain.vectorstores.memory.MemoryVectorStore')
1134+
1135+
expect(embeddingSpan).to.have.property('name', 'langchain.request')
1136+
expect(embeddingSpan).to.have.property('resource', 'langchain.embeddings.openai.OpenAIEmbeddings')
1137+
}, { spanResourceMatch: /langchain\.vectorstores\.memory\.MemoryVectorStore/ })
1138+
// we need the spanResourceMatch, otherwise we'll match from the beforeEach
1139+
1140+
const result = await vectorstore.similaritySearch('The powerhouse of the cell is the mitochondria', 2)
1141+
expect(result).to.exist
1142+
1143+
await checkTraces
1144+
})
1145+
1146+
it('traces a vectorstore similaritySearchWithScore call', async () => {
1147+
stubSingleEmbedding(langchainOpenaiOpenAiVersion)
1148+
1149+
const checkTraces = agent.assertSomeTraces(traces => {
1150+
const spans = traces[0]
1151+
1152+
expect(spans).to.have.length(2)
1153+
1154+
const vectorstoreSpan = spans[0]
1155+
const embeddingSpan = spans[1]
1156+
1157+
expect(vectorstoreSpan).to.have.property('name', 'langchain.request')
1158+
expect(vectorstoreSpan).to.have.property('resource', 'langchain.vectorstores.memory.MemoryVectorStore')
1159+
1160+
expect(embeddingSpan).to.have.property('name', 'langchain.request')
1161+
expect(embeddingSpan).to.have.property('resource', 'langchain.embeddings.openai.OpenAIEmbeddings')
1162+
}, { spanResourceMatch: /langchain\.vectorstores\.memory\.MemoryVectorStore/ })
1163+
// we need the spanResourceMatch, otherwise we'll match from the beforeEach
1164+
1165+
const result = await vectorstore.similaritySearchWithScore(
1166+
'The powerhouse of the cell is the mitochondria', 2
1167+
)
1168+
expect(result).to.exist
1169+
1170+
await checkTraces
1171+
})
1172+
})
10161173
})
10171174
})
10181175
})

packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ const ROLE_MAPPINGS = {
88

99
class LangChainLLMObsHandler {
1010
constructor (tagger) {
11+
/** @type {import('../../../tagger')} */
1112
this._tagger = tagger
1213
}
1314

15+
getName ({ span }) {
16+
return span?.context()._tags?.['resource.name']
17+
}
18+
1419
setMetaTags () {}
1520

1621
formatIO (messages) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict'
2+
3+
const LangChainLLMObsHandler = require('.')
4+
5+
class LangChainLLMObsToolHandler extends LangChainLLMObsHandler {
6+
getName ({ instance }) {
7+
return instance.name
8+
}
9+
10+
setMetaTags ({ span, inputs, results }) {
11+
this._tagger.tagTextIO(span, inputs, results)
12+
}
13+
}
14+
15+
module.exports = LangChainLLMObsToolHandler
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict'
2+
3+
const LangChainLLMObsHandler = require('.')
4+
const { spanHasError } = require('../../../util')
5+
6+
class LangChainLLMObsVectorStoreHandler extends LangChainLLMObsHandler {
7+
setMetaTags ({ span, inputs, results }) {
8+
const input = this.formatIO(inputs)
9+
if (spanHasError(span)) {
10+
this._tagger.tagRetrievalIO(span, input)
11+
return
12+
}
13+
14+
const documents = []
15+
for (const documentResult of results) {
16+
let document, score
17+
if (Array.isArray(documentResult)) {
18+
document = documentResult[0]
19+
score = documentResult[1]
20+
} else {
21+
document = documentResult
22+
}
23+
24+
documents.push({
25+
text: document.pageContent,
26+
id: document.id,
27+
name: document.metadata?.source,
28+
score
29+
})
30+
}
31+
32+
this._tagger.tagRetrievalIO(span, input, documents)
33+
}
34+
}
35+
36+
module.exports = LangChainLLMObsVectorStoreHandler

0 commit comments

Comments
 (0)