Skip to content

Commit b6a7773

Browse files
authored
feat(core): Support truncation for LangChain integration request messages (#18157)
This PR adds [truncation support for LangChain integration request messages](#18018). All messages already get normalized to arrays of messages, so here we need no case distinction for strings. Adds tests to verify behavior for 1. simple string inputs and 2. conversations in the form of arrays of strings. Closes #18018
1 parent 76fef98 commit b6a7773

File tree

3 files changed

+126
-1
lines changed

3 files changed

+126
-1
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ChatAnthropic } from '@langchain/anthropic';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockAnthropicServer() {
6+
const app = express();
7+
app.use(express.json());
8+
9+
app.post('/v1/messages', (req, res) => {
10+
const model = req.body.model;
11+
12+
res.json({
13+
id: 'msg_truncation_test',
14+
type: 'message',
15+
role: 'assistant',
16+
content: [
17+
{
18+
type: 'text',
19+
text: 'Response to truncated messages',
20+
},
21+
],
22+
model: model,
23+
stop_reason: 'end_turn',
24+
stop_sequence: null,
25+
usage: {
26+
input_tokens: 10,
27+
output_tokens: 15,
28+
},
29+
});
30+
});
31+
32+
return new Promise(resolve => {
33+
const server = app.listen(0, () => {
34+
resolve(server);
35+
});
36+
});
37+
}
38+
39+
async function run() {
40+
const server = await startMockAnthropicServer();
41+
const baseUrl = `http://localhost:${server.address().port}`;
42+
43+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
44+
const model = new ChatAnthropic({
45+
model: 'claude-3-5-sonnet-20241022',
46+
apiKey: 'mock-api-key',
47+
clientOptions: {
48+
baseURL: baseUrl,
49+
},
50+
});
51+
52+
const largeContent1 = 'A'.repeat(15000); // ~15KB
53+
const largeContent2 = 'B'.repeat(15000); // ~15KB
54+
const largeContent3 = 'C'.repeat(25000); // ~25KB (will be truncated)
55+
56+
// Create one very large string that gets truncated to only include Cs
57+
await model.invoke(largeContent3 + largeContent2);
58+
59+
// Create an array of messages that gets truncated to only include the last message (result should again contain only Cs)
60+
await model.invoke([
61+
{ role: 'system', content: largeContent1 },
62+
{ role: 'user', content: largeContent2 },
63+
{ role: 'user', content: largeContent3 },
64+
]);
65+
});
66+
67+
await Sentry.flush(2000);
68+
69+
server.close();
70+
}
71+
72+
run();

dev-packages/node-integration-tests/suites/tracing/langchain/test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,55 @@ describe('LangChain integration', () => {
194194
await createRunner().ignore('event').expect({ transaction: EXPECTED_TRANSACTION_TOOL_CALLS }).start().completed();
195195
});
196196
});
197+
198+
const EXPECTED_TRANSACTION_MESSAGE_TRUNCATION = {
199+
transaction: 'main',
200+
spans: expect.arrayContaining([
201+
expect.objectContaining({
202+
data: expect.objectContaining({
203+
'gen_ai.operation.name': 'chat',
204+
'sentry.op': 'gen_ai.chat',
205+
'sentry.origin': 'auto.ai.langchain',
206+
'gen_ai.system': 'anthropic',
207+
'gen_ai.request.model': 'claude-3-5-sonnet-20241022',
208+
// Messages should be present and should include truncated string input (contains only Cs)
209+
'gen_ai.request.messages': expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/),
210+
}),
211+
description: 'chat claude-3-5-sonnet-20241022',
212+
op: 'gen_ai.chat',
213+
origin: 'auto.ai.langchain',
214+
status: 'ok',
215+
}),
216+
expect.objectContaining({
217+
data: expect.objectContaining({
218+
'gen_ai.operation.name': 'chat',
219+
'sentry.op': 'gen_ai.chat',
220+
'sentry.origin': 'auto.ai.langchain',
221+
'gen_ai.system': 'anthropic',
222+
'gen_ai.request.model': 'claude-3-5-sonnet-20241022',
223+
// Messages should be present (truncation happened) and should be a JSON array of a single index (contains only Cs)
224+
'gen_ai.request.messages': expect.stringMatching(/^\[\{"role":"user","content":"C+"\}\]$/),
225+
}),
226+
description: 'chat claude-3-5-sonnet-20241022',
227+
op: 'gen_ai.chat',
228+
origin: 'auto.ai.langchain',
229+
status: 'ok',
230+
}),
231+
]),
232+
};
233+
234+
createEsmAndCjsTests(
235+
__dirname,
236+
'scenario-message-truncation.mjs',
237+
'instrument-with-pii.mjs',
238+
(createRunner, test) => {
239+
test('truncates messages when they exceed byte limit', async () => {
240+
await createRunner()
241+
.ignore('event')
242+
.expect({ transaction: EXPECTED_TRANSACTION_MESSAGE_TRUNCATION })
243+
.start()
244+
.completed();
245+
});
246+
},
247+
);
197248
});

packages/core/src/utils/langchain/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
2424
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
2525
} from '../ai/gen-ai-attributes';
26+
import { truncateGenAiMessages } from '../ai/messageTruncation';
2627
import { LANGCHAIN_ORIGIN, ROLE_MAP } from './constants';
2728
import type { LangChainLLMResult, LangChainMessage, LangChainSerialized } from './types';
2829

@@ -281,7 +282,8 @@ export function extractChatModelRequestAttributes(
281282

282283
if (recordInputs && Array.isArray(langChainMessages) && langChainMessages.length > 0) {
283284
const normalized = normalizeLangChainMessages(langChainMessages.flat());
284-
setIfDefined(attrs, GEN_AI_REQUEST_MESSAGES_ATTRIBUTE, asString(normalized));
285+
const truncated = truncateGenAiMessages(normalized);
286+
setIfDefined(attrs, GEN_AI_REQUEST_MESSAGES_ATTRIBUTE, asString(truncated));
285287
}
286288

287289
return attrs;

0 commit comments

Comments
 (0)