Skip to content

Commit c39c888

Browse files
committed
Add mid-stream failure error test
1 parent 6ce856c commit c39c888

File tree

2 files changed

+69
-14
lines changed

2 files changed

+69
-14
lines changed

lib/llm-events/error-message.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ module.exports = class LlmErrorMessage {
2727
constructor({ response, cause, summary = {}, embedding = {}, vectorsearch = {}, tool = {}, aiAgent = {}, useNameAsCode = false } = {}) {
2828
this['http.statusCode'] = response?.statusCode ?? response?.status ?? cause?.status
2929
this['error.message'] = cause?.message
30-
this['error.code'] = response?.code ?? cause?.error?.code
31-
if (useNameAsCode) {
32-
this['error.code'] = cause?.name
33-
}
30+
this['error.code'] = this._getErrorCode({ cause, response, summary, useNameAsCode })
3431
this['error.param'] = response?.param ?? cause?.error?.param
3532
this.completion_id = summary?.id
3633
this.embedding_id = embedding?.id
@@ -47,6 +44,17 @@ module.exports = class LlmErrorMessage {
4744
return 'LlmErrorMessage'
4845
}
4946

47+
_getErrorCode({ cause, response, useNameAsCode }) {
48+
if (useNameAsCode) {
49+
return cause?.name
50+
}
51+
if (cause?.['lc_error_code']) {
52+
// langchain error code
53+
return cause['lc_error_code']
54+
}
55+
return response?.code ?? cause?.error?.code ?? cause.code
56+
}
57+
5058
/**
5159
* For `@google/genai` only, `cause` does not have the `error` or `status` fields,
5260
* but it does have `message` with the info we need. So, we need to parse

test/versioned/langgraph/graph-stream.test.js

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
const test = require('node:test')
99
const assert = require('node:assert')
1010

11+
const { tspl } = require('@matteo.collina/tspl')
12+
1113
const { removeModules } = require('../../lib/cache-buster')
1214
const { assertSegments, match } = require('../../lib/custom-assertions')
1315
const createOpenAIMockServer = require('../openai/mock-server')
@@ -70,9 +72,9 @@ test.beforeEach(async (ctx) => {
7072
})
7173

7274
// Create a simple LangGraph agent.
73-
// Ignore the deprecation warning; LangGraph just wants
74-
// us to require from "langchain" directly, but
75-
// the function works the same.
75+
// Ignore the deprecation warning; LangGraph just
76+
// wants us to require from "langchain" directly,
77+
// but the function is still valid.
7678
ctx.nr.langgraphAgent = createReactAgent({
7779
llm: mockLLM,
7880
// must define tools even if empty
@@ -302,6 +304,7 @@ test('should add subcomponent attribute to span', async (t) => {
302304

303305
test('should create LlmError event when given bad input', async (t) => {
304306
const { agent, langgraphAgent } = t.nr
307+
const plan = tspl(t, { plan: 8 })
305308

306309
await helper.runInTransaction(agent, async (tx) => {
307310
try {
@@ -312,23 +315,67 @@ test('should create LlmError event when given bad input', async (t) => {
312315
consumeChunk(chunk)
313316
}
314317
} catch (error) {
315-
assert.ok(error, 'should catch an error')
318+
plan.ok(error, 'should catch an error')
319+
}
320+
321+
// Check for LlmAgent event with error flag
322+
const events = agent.customEventAggregator.events.toArray()
323+
const agentEvent = events.find((e) => e[0].type === 'LlmAgent')?.[1]
324+
plan.ok(agentEvent, 'should have LlmAgent event')
325+
plan.equal(agentEvent.error, true, 'should set LlmAgent event `error` to true')
326+
327+
// Check for LlmError in transaction exceptions.
328+
// 2 will be created, first one for LangChain RunnableSequence.stream
329+
// failure, second one for LangGraph agent failure
330+
const exceptions = tx.exceptions
331+
plan.equal(exceptions.length, 2)
332+
const lgException = exceptions[1]
333+
const str = Object.prototype.toString.call(lgException.customAttributes)
334+
plan.equal(str, '[object LlmErrorMessage]', 'should be a LlmErrorMessage')
335+
plan.equal(lgException.customAttributes.agent_id, agentEvent.id, 'ai agent_id should match')
336+
plan.equal(lgException.customAttributes['error.code'], lgException.error['lc_error_code'], 'error codes should match')
337+
plan.equal(lgException.customAttributes['error.message'], lgException.error['message'], 'error messages should match')
338+
339+
tx.end()
340+
})
341+
})
342+
343+
test('should create LlmError event when stream fails in the middle', async (t) => {
344+
const { agent, langgraphAgent } = t.nr
345+
const plan = tspl(t, { plan: 8 })
346+
347+
await helper.runInTransaction(agent, async (tx) => {
348+
try {
349+
// Starts off with a valid request...
350+
const stream = await langgraphAgent.stream(
351+
{ messages: [{ role: 'user', content: 'You are a scientist.' }] }
352+
)
353+
for await (const chunk of stream) {
354+
consumeChunk(chunk)
355+
// then abruptly abort the stream
356+
stream.cancel('abort')
357+
}
358+
} catch (error) {
359+
plan.ok(error, 'should catch an error')
316360
}
317361

318362
// Check for LlmAgent event with error flag
319363
const events = agent.customEventAggregator.events.toArray()
320364
const agentEvent = events.find((e) => e[0].type === 'LlmAgent')?.[1]
321-
assert.ok(agentEvent, 'should have LlmAgent event')
322-
assert.equal(agentEvent.error, true)
365+
plan.ok(agentEvent, 'should have LlmAgent event')
366+
plan.equal(agentEvent.error, true, 'should set LlmAgent event `error` to true')
323367

324368
// Check for LlmError in transaction exceptions.
325369
// 2 will be created, first one for LangChain RunnableSequence.stream
326370
// failure, second one for LangGraph agent failure
327371
const exceptions = tx.exceptions
328-
assert.equal(exceptions.length, 2)
329-
const str = Object.prototype.toString.call(exceptions[1].customAttributes)
330-
assert.equal(str, '[object LlmErrorMessage]')
331-
assert.equal(exceptions[1].customAttributes.agent_id, agentEvent.id)
372+
plan.equal(exceptions.length, 2)
373+
const lgException = exceptions[1]
374+
const str = Object.prototype.toString.call(lgException.customAttributes)
375+
plan.equal(str, '[object LlmErrorMessage]', 'should be a LlmErrorMessage')
376+
plan.equal(lgException.customAttributes.agent_id, agentEvent.id, 'ai agent_id should match')
377+
plan.equal(lgException.customAttributes['error.code'], lgException.error['code'], 'error codes should match')
378+
plan.equal(lgException.customAttributes['error.message'], lgException.error['message'], 'error messages should match')
332379

333380
tx.end()
334381
})

0 commit comments

Comments
 (0)