@librechat/agents 3.1.91 → 3.1.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/graphs/Graph.cjs +5 -3
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +2 -7
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +62 -11
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/run.cjs +33 -19
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/utils/callbacks.cjs +27 -0
- package/dist/cjs/utils/callbacks.cjs.map +1 -0
- package/dist/esm/graphs/Graph.mjs +6 -4
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +2 -7
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +63 -14
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/run.mjs +34 -20
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/utils/callbacks.mjs +24 -0
- package/dist/esm/utils/callbacks.mjs.map +1 -0
- package/dist/types/langfuse.d.ts +13 -4
- package/dist/types/types/run.d.ts +2 -2
- package/dist/types/utils/callbacks.d.ts +5 -0
- package/package.json +4 -4
- package/src/graphs/Graph.ts +10 -6
- package/src/instrumentation.ts +2 -7
- package/src/langfuse.ts +98 -15
- package/src/run.ts +53 -29
- package/src/specs/langfuse-callbacks.test.ts +75 -0
- package/src/specs/langfuse-config.test.ts +58 -1
- package/src/types/run.ts +2 -7
- package/src/utils/callbacks.ts +39 -0
package/src/langfuse.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CallbackHandler } from '@langfuse/langchain';
|
|
2
|
-
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
2
|
+
import { isDefaultExportSpan, LangfuseSpanProcessor } from '@langfuse/otel';
|
|
3
3
|
import {
|
|
4
|
+
LangfuseOtelSpanAttributes,
|
|
4
5
|
createObservationAttributes,
|
|
5
|
-
createTraceAttributes,
|
|
6
6
|
} from '@langfuse/tracing';
|
|
7
7
|
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
8
8
|
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
@@ -10,16 +10,20 @@ import { SpanStatusCode } from '@opentelemetry/api';
|
|
|
10
10
|
import type { Serialized } from '@langchain/core/load/serializable';
|
|
11
11
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
12
12
|
import type { LLMResult } from '@langchain/core/outputs';
|
|
13
|
-
import type { Span } from '@opentelemetry/api';
|
|
13
|
+
import type { Attributes, Span } from '@opentelemetry/api';
|
|
14
14
|
import type * as t from '@/types';
|
|
15
15
|
import { isPresent } from '@/utils/misc';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
const TRACE_METADATA_MAX_LENGTH = 200;
|
|
18
|
+
const LANGFUSE_TRACER_NAME = 'langfuse-sdk';
|
|
19
|
+
|
|
20
|
+
export type LangfuseTraceMetadata = Record<string, string>;
|
|
18
21
|
|
|
19
22
|
type LangfuseHandlerParams = {
|
|
20
23
|
userId?: string;
|
|
21
24
|
sessionId?: string;
|
|
22
|
-
traceMetadata?:
|
|
25
|
+
traceMetadata?: LangfuseTraceMetadata;
|
|
26
|
+
tags?: string[];
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
|
|
@@ -32,6 +36,49 @@ type ResolvedLangfuseConfig = t.LangfuseConfig & {
|
|
|
32
36
|
secretKey: string;
|
|
33
37
|
};
|
|
34
38
|
|
|
39
|
+
function getEnvLangfuseBaseUrl(): string | undefined {
|
|
40
|
+
return process.env.LANGFUSE_BASE_URL ?? process.env.LANGFUSE_BASEURL;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createTraceMetadata(
|
|
44
|
+
metadata: Record<string, unknown>
|
|
45
|
+
): LangfuseTraceMetadata {
|
|
46
|
+
const traceMetadata: LangfuseTraceMetadata = {};
|
|
47
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
48
|
+
if (value == null) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const stringValue = typeof value === 'string' ? value : String(value);
|
|
52
|
+
if (
|
|
53
|
+
stringValue.trim() === '' ||
|
|
54
|
+
stringValue.length > TRACE_METADATA_MAX_LENGTH
|
|
55
|
+
) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
traceMetadata[key] = stringValue;
|
|
59
|
+
}
|
|
60
|
+
return traceMetadata;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createLangfuseTraceMetadata({
|
|
64
|
+
messageId,
|
|
65
|
+
parentMessageId,
|
|
66
|
+
agentId,
|
|
67
|
+
agentName,
|
|
68
|
+
}: {
|
|
69
|
+
messageId?: unknown;
|
|
70
|
+
parentMessageId?: unknown;
|
|
71
|
+
agentId?: unknown;
|
|
72
|
+
agentName?: unknown;
|
|
73
|
+
}): LangfuseTraceMetadata {
|
|
74
|
+
return createTraceMetadata({
|
|
75
|
+
messageId,
|
|
76
|
+
parentMessageId,
|
|
77
|
+
agentId,
|
|
78
|
+
agentName,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
35
82
|
function getModelName(serialized: Serialized): string {
|
|
36
83
|
const serializedRecord = serialized as unknown as Record<string, unknown>;
|
|
37
84
|
const kwargs = serializedRecord.kwargs as Record<string, unknown> | undefined;
|
|
@@ -99,11 +146,39 @@ function getUsageDetails(
|
|
|
99
146
|
: undefined;
|
|
100
147
|
}
|
|
101
148
|
|
|
102
|
-
function
|
|
149
|
+
export function getLangfuseTraceName(
|
|
150
|
+
traceMetadata?: LangfuseTraceMetadata,
|
|
151
|
+
fallback: string = 'LibreChat Agent'
|
|
152
|
+
): string {
|
|
103
153
|
const agentName = traceMetadata?.agentName;
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
154
|
+
return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getTraceAttributes({
|
|
158
|
+
userId,
|
|
159
|
+
sessionId,
|
|
160
|
+
traceMetadata,
|
|
161
|
+
tags,
|
|
162
|
+
}: LangfuseHandlerParams): Attributes {
|
|
163
|
+
const attributes: Attributes = {
|
|
164
|
+
[LangfuseOtelSpanAttributes.TRACE_NAME]:
|
|
165
|
+
getLangfuseTraceName(traceMetadata),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (isPresent(userId)) {
|
|
169
|
+
attributes[LangfuseOtelSpanAttributes.TRACE_USER_ID] = userId;
|
|
170
|
+
}
|
|
171
|
+
if (isPresent(sessionId)) {
|
|
172
|
+
attributes[LangfuseOtelSpanAttributes.TRACE_SESSION_ID] = sessionId;
|
|
173
|
+
}
|
|
174
|
+
if (tags != null && tags.length > 0) {
|
|
175
|
+
attributes[LangfuseOtelSpanAttributes.TRACE_TAGS] = tags;
|
|
176
|
+
}
|
|
177
|
+
for (const [key, value] of Object.entries(traceMetadata ?? {})) {
|
|
178
|
+
attributes[`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`] = value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return attributes;
|
|
107
182
|
}
|
|
108
183
|
|
|
109
184
|
export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
|
|
@@ -113,7 +188,8 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
|
|
|
113
188
|
private readonly processor: LangfuseSpanProcessor;
|
|
114
189
|
private readonly userId?: string;
|
|
115
190
|
private readonly sessionId?: string;
|
|
116
|
-
private readonly traceMetadata?:
|
|
191
|
+
private readonly traceMetadata?: LangfuseTraceMetadata;
|
|
192
|
+
private readonly tags?: string[];
|
|
117
193
|
private readonly spans = new Map<string, Span>();
|
|
118
194
|
|
|
119
195
|
constructor({
|
|
@@ -121,11 +197,13 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
|
|
|
121
197
|
userId,
|
|
122
198
|
sessionId,
|
|
123
199
|
traceMetadata,
|
|
200
|
+
tags,
|
|
124
201
|
}: LangfuseHandlerParams & { langfuse: ResolvedLangfuseConfig }) {
|
|
125
202
|
super();
|
|
126
203
|
this.userId = userId;
|
|
127
204
|
this.sessionId = sessionId;
|
|
128
205
|
this.traceMetadata = traceMetadata;
|
|
206
|
+
this.tags = tags;
|
|
129
207
|
this.processor = new LangfuseSpanProcessor({
|
|
130
208
|
publicKey: langfuse.publicKey,
|
|
131
209
|
secretKey: langfuse.secretKey,
|
|
@@ -135,6 +213,9 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
|
|
|
135
213
|
process.env.NODE_ENV ??
|
|
136
214
|
'development',
|
|
137
215
|
exportMode: 'immediate',
|
|
216
|
+
shouldExportSpan: ({ otelSpan }): boolean =>
|
|
217
|
+
isDefaultExportSpan(otelSpan) ||
|
|
218
|
+
otelSpan.instrumentationScope.name === LANGFUSE_TRACER_NAME,
|
|
138
219
|
});
|
|
139
220
|
this.provider = new BasicTracerProvider({
|
|
140
221
|
spanProcessors: [this.processor],
|
|
@@ -160,16 +241,16 @@ export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
|
|
|
160
241
|
return;
|
|
161
242
|
}
|
|
162
243
|
|
|
163
|
-
const tracer = this.provider.getTracer(
|
|
244
|
+
const tracer = this.provider.getTracer(LANGFUSE_TRACER_NAME);
|
|
164
245
|
const spanName =
|
|
165
246
|
typeof name === 'string' && name.trim() !== '' ? name : getModelName(llm);
|
|
166
247
|
const span = tracer.startSpan(spanName, {
|
|
167
248
|
attributes: {
|
|
168
|
-
...
|
|
169
|
-
name: getTraceName(this.traceMetadata),
|
|
249
|
+
...getTraceAttributes({
|
|
170
250
|
userId: this.userId,
|
|
171
251
|
sessionId: this.sessionId,
|
|
172
|
-
|
|
252
|
+
traceMetadata: this.traceMetadata,
|
|
253
|
+
tags: this.tags,
|
|
173
254
|
}),
|
|
174
255
|
...createObservationAttributes('generation', {
|
|
175
256
|
input,
|
|
@@ -312,6 +393,7 @@ export function createLangfuseHandler({
|
|
|
312
393
|
userId,
|
|
313
394
|
sessionId,
|
|
314
395
|
traceMetadata,
|
|
396
|
+
tags,
|
|
315
397
|
}: AgentLangfuseHandlerParams): LangfuseAgentCallbackHandler | undefined {
|
|
316
398
|
if (!hasRequiredLangfuseConfig(langfuse)) {
|
|
317
399
|
return undefined;
|
|
@@ -322,6 +404,7 @@ export function createLangfuseHandler({
|
|
|
322
404
|
userId,
|
|
323
405
|
sessionId,
|
|
324
406
|
traceMetadata,
|
|
407
|
+
tags,
|
|
325
408
|
});
|
|
326
409
|
}
|
|
327
410
|
|
|
@@ -340,7 +423,7 @@ export function hasLangfuseEnvConfig(): boolean {
|
|
|
340
423
|
return (
|
|
341
424
|
isPresent(process.env.LANGFUSE_SECRET_KEY) &&
|
|
342
425
|
isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
|
|
343
|
-
isPresent(
|
|
426
|
+
isPresent(getEnvLangfuseBaseUrl())
|
|
344
427
|
);
|
|
345
428
|
}
|
|
346
429
|
|
package/src/run.ts
CHANGED
|
@@ -30,10 +30,17 @@ import { initializeModel } from '@/llm/init';
|
|
|
30
30
|
import { HandlerRegistry } from '@/events';
|
|
31
31
|
import { executeHooks } from '@/hooks';
|
|
32
32
|
import { isOpenAILike } from '@/utils/llm';
|
|
33
|
+
import {
|
|
34
|
+
appendCallbacks,
|
|
35
|
+
findCallback,
|
|
36
|
+
type CallbackEntry,
|
|
37
|
+
} from '@/utils/callbacks';
|
|
33
38
|
import {
|
|
34
39
|
createLegacyLangfuseHandler,
|
|
40
|
+
createLangfuseTraceMetadata,
|
|
35
41
|
createLangfuseHandler,
|
|
36
42
|
disposeLangfuseHandler,
|
|
43
|
+
getLangfuseTraceName,
|
|
37
44
|
hasExplicitLangfuseConfig,
|
|
38
45
|
hasLangfuseEnvConfig,
|
|
39
46
|
isLangfuseCallbackHandler,
|
|
@@ -598,42 +605,48 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
598
605
|
/** Custom event callback to intercept and handle custom events */
|
|
599
606
|
const customEventCallback = this.createCustomEventCallback();
|
|
600
607
|
|
|
601
|
-
const baseCallbacks = (config.callbacks as t.ProvidedCallbacks) ?? [];
|
|
602
608
|
const streamCallbacks = streamOptions?.callbacks
|
|
603
609
|
? this.getCallbacks(streamOptions.callbacks)
|
|
604
|
-
:
|
|
610
|
+
: undefined;
|
|
605
611
|
|
|
606
612
|
const customHandler = BaseCallbackHandler.fromMethods({
|
|
607
613
|
[Callback.CUSTOM_EVENT]: customEventCallback,
|
|
608
614
|
});
|
|
609
615
|
customHandler.awaitHandlers = true;
|
|
610
616
|
|
|
611
|
-
config.callbacks =
|
|
612
|
-
.
|
|
613
|
-
|
|
617
|
+
config.callbacks = appendCallbacks(
|
|
618
|
+
config.callbacks,
|
|
619
|
+
streamCallbacks ? [streamCallbacks, customHandler] : [customHandler]
|
|
620
|
+
);
|
|
614
621
|
|
|
615
622
|
if (
|
|
616
623
|
hasLangfuseEnvConfig() &&
|
|
617
624
|
!hasExplicitLangfuseConfig(this.Graph.agentContexts.values())
|
|
618
625
|
) {
|
|
619
|
-
const userId =
|
|
620
|
-
|
|
626
|
+
const userId =
|
|
627
|
+
typeof config.configurable?.user_id === 'string'
|
|
628
|
+
? config.configurable.user_id
|
|
629
|
+
: undefined;
|
|
630
|
+
const sessionId =
|
|
631
|
+
typeof config.configurable?.thread_id === 'string'
|
|
632
|
+
? config.configurable.thread_id
|
|
633
|
+
: undefined;
|
|
621
634
|
const primaryContext = this.Graph.agentContexts.get(
|
|
622
635
|
this.Graph.defaultAgentId
|
|
623
636
|
);
|
|
624
|
-
const traceMetadata = {
|
|
637
|
+
const traceMetadata = createLangfuseTraceMetadata({
|
|
625
638
|
messageId: this.id,
|
|
626
639
|
parentMessageId: config.configurable?.requestBody?.parentMessageId,
|
|
627
640
|
agentName: primaryContext?.name,
|
|
628
|
-
};
|
|
641
|
+
});
|
|
629
642
|
const handler = createLegacyLangfuseHandler({
|
|
630
643
|
userId,
|
|
631
644
|
sessionId,
|
|
632
645
|
traceMetadata,
|
|
646
|
+
tags: ['librechat', 'agent'],
|
|
633
647
|
});
|
|
634
|
-
config.
|
|
635
|
-
|
|
636
|
-
).concat([handler]);
|
|
648
|
+
config.runName = config.runName ?? getLangfuseTraceName(traceMetadata);
|
|
649
|
+
config.callbacks = appendCallbacks(config.callbacks, [handler]);
|
|
637
650
|
}
|
|
638
651
|
|
|
639
652
|
if (!this.id) {
|
|
@@ -1139,17 +1152,26 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1139
1152
|
titleMethod = TitleMethod.COMPLETION,
|
|
1140
1153
|
titlePromptTemplate,
|
|
1141
1154
|
}: t.RunTitleOptions): Promise<{ language?: string; title?: string }> {
|
|
1142
|
-
let titleLangfuseHandler:
|
|
1155
|
+
let titleLangfuseHandler: CallbackEntry | undefined;
|
|
1156
|
+
const titleContext =
|
|
1157
|
+
this.Graph == null
|
|
1158
|
+
? undefined
|
|
1159
|
+
: this.Graph.agentContexts.get(this.Graph.defaultAgentId);
|
|
1160
|
+
const traceMetadata = createLangfuseTraceMetadata({
|
|
1161
|
+
messageId: 'title-' + this.id,
|
|
1162
|
+
agentName: titleContext?.name,
|
|
1163
|
+
});
|
|
1164
|
+
const titleRunName = getLangfuseTraceName(traceMetadata, 'LibreChat Title');
|
|
1165
|
+
|
|
1143
1166
|
if (chainOptions != null) {
|
|
1144
|
-
const userId =
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
};
|
|
1167
|
+
const userId =
|
|
1168
|
+
typeof chainOptions.configurable?.user_id === 'string'
|
|
1169
|
+
? chainOptions.configurable.user_id
|
|
1170
|
+
: undefined;
|
|
1171
|
+
const sessionId =
|
|
1172
|
+
typeof chainOptions.configurable?.thread_id === 'string'
|
|
1173
|
+
? chainOptions.configurable.thread_id
|
|
1174
|
+
: undefined;
|
|
1153
1175
|
const hasExplicitLangfuse =
|
|
1154
1176
|
this.Graph != null &&
|
|
1155
1177
|
hasExplicitLangfuseConfig(this.Graph.agentContexts.values());
|
|
@@ -1159,20 +1181,20 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1159
1181
|
userId,
|
|
1160
1182
|
sessionId,
|
|
1161
1183
|
traceMetadata,
|
|
1184
|
+
tags: ['librechat', 'title'],
|
|
1162
1185
|
});
|
|
1163
1186
|
} else if (hasLangfuseEnvConfig() && !hasExplicitLangfuse) {
|
|
1164
1187
|
titleLangfuseHandler = createLegacyLangfuseHandler({
|
|
1165
1188
|
userId,
|
|
1166
1189
|
sessionId,
|
|
1167
1190
|
traceMetadata,
|
|
1191
|
+
tags: ['librechat', 'title'],
|
|
1168
1192
|
});
|
|
1169
1193
|
}
|
|
1170
1194
|
|
|
1171
1195
|
if (titleLangfuseHandler != null) {
|
|
1172
|
-
chainOptions.callbacks = (
|
|
1173
|
-
|
|
1174
|
-
).concat([
|
|
1175
|
-
titleLangfuseHandler as NonNullable<t.ProvidedCallbacks>[number],
|
|
1196
|
+
chainOptions.callbacks = appendCallbacks(chainOptions.callbacks, [
|
|
1197
|
+
titleLangfuseHandler,
|
|
1176
1198
|
]);
|
|
1177
1199
|
}
|
|
1178
1200
|
}
|
|
@@ -1236,6 +1258,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1236
1258
|
const invokeConfig = Object.assign({}, chainOptions, {
|
|
1237
1259
|
run_id: this.id,
|
|
1238
1260
|
runId: this.id,
|
|
1261
|
+
runName: chainOptions?.runName ?? titleRunName,
|
|
1239
1262
|
});
|
|
1240
1263
|
|
|
1241
1264
|
try {
|
|
@@ -1247,9 +1270,10 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1247
1270
|
} catch (_e) {
|
|
1248
1271
|
// Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
|
|
1249
1272
|
// but preserve Langfuse tracing if it exists.
|
|
1250
|
-
const langfuseHandler = (
|
|
1251
|
-
invokeConfig.callbacks
|
|
1252
|
-
|
|
1273
|
+
const langfuseHandler = findCallback(
|
|
1274
|
+
invokeConfig.callbacks,
|
|
1275
|
+
isLangfuseCallbackHandler
|
|
1276
|
+
);
|
|
1253
1277
|
const { callbacks: _cb, ...rest } = invokeConfig;
|
|
1254
1278
|
const safeConfig = Object.assign({}, rest, {
|
|
1255
1279
|
callbacks: langfuseHandler ? [langfuseHandler] : [],
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CallbackManager } from '@langchain/core/callbacks/manager';
|
|
2
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
3
|
+
import { Providers } from '@/common';
|
|
4
|
+
import { Run } from '@/run';
|
|
5
|
+
import type * as t from '@/types';
|
|
6
|
+
|
|
7
|
+
const mockSpan = {
|
|
8
|
+
end: jest.fn(),
|
|
9
|
+
setAttributes: jest.fn(),
|
|
10
|
+
setStatus: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
const mockStartSpan = jest.fn(() => mockSpan);
|
|
13
|
+
const mockForceFlush = jest.fn();
|
|
14
|
+
const mockShutdown = jest.fn();
|
|
15
|
+
|
|
16
|
+
jest.mock('@langfuse/otel', () => ({
|
|
17
|
+
LangfuseSpanProcessor: jest.fn().mockImplementation(() => ({})),
|
|
18
|
+
isDefaultExportSpan: jest.fn(() => false),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
jest.mock('@opentelemetry/sdk-trace-base', () => ({
|
|
22
|
+
BasicTracerProvider: jest.fn().mockImplementation(() => ({
|
|
23
|
+
forceFlush: mockForceFlush,
|
|
24
|
+
getTracer: jest.fn(() => ({
|
|
25
|
+
startSpan: mockStartSpan,
|
|
26
|
+
})),
|
|
27
|
+
shutdown: mockShutdown,
|
|
28
|
+
})),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe('Langfuse callback composition', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('runs explicit per-agent tracing when callbacks is a CallbackManager', async () => {
|
|
37
|
+
const manager = CallbackManager.fromHandlers({
|
|
38
|
+
handleCustomEvent: async (): Promise<void> => undefined,
|
|
39
|
+
});
|
|
40
|
+
const run = await Run.create<t.IState>({
|
|
41
|
+
runId: 'test-langfuse-callback-manager',
|
|
42
|
+
graphConfig: {
|
|
43
|
+
type: 'standard',
|
|
44
|
+
agents: [
|
|
45
|
+
{
|
|
46
|
+
agentId: 'agent_abc123',
|
|
47
|
+
name: 'DWAINE',
|
|
48
|
+
provider: Providers.OPENAI,
|
|
49
|
+
clientOptions: { model: 'gpt-4' },
|
|
50
|
+
tools: [],
|
|
51
|
+
langfuse: {
|
|
52
|
+
enabled: true,
|
|
53
|
+
publicKey: 'pk-test',
|
|
54
|
+
secretKey: 'sk-test',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
skipCleanup: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
run.Graph?.overrideTestModel(['hello']);
|
|
63
|
+
|
|
64
|
+
const config = {
|
|
65
|
+
callbacks: manager,
|
|
66
|
+
configurable: { thread_id: 'thread-1', user_id: 'user-1' },
|
|
67
|
+
streamMode: 'values' as const,
|
|
68
|
+
version: 'v2' as const,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
await run.processStream({ messages: [new HumanMessage('hello')] }, config);
|
|
72
|
+
|
|
73
|
+
expect(mockStartSpan).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
2
2
|
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
|
|
3
|
+
import { HumanMessage } from '@langchain/core/messages';
|
|
4
|
+
import type { Serialized } from '@langchain/core/load/serializable';
|
|
3
5
|
import { createLangfuseHandler } from '@/langfuse';
|
|
4
6
|
|
|
7
|
+
const mockSpan = {
|
|
8
|
+
end: jest.fn(),
|
|
9
|
+
setAttributes: jest.fn(),
|
|
10
|
+
setStatus: jest.fn(),
|
|
11
|
+
};
|
|
12
|
+
const mockStartSpan = jest.fn(() => mockSpan);
|
|
13
|
+
const mockGetTracer = jest.fn(() => ({
|
|
14
|
+
startSpan: mockStartSpan,
|
|
15
|
+
}));
|
|
16
|
+
|
|
5
17
|
jest.mock('@langfuse/otel', () => ({
|
|
6
18
|
LangfuseSpanProcessor: jest.fn().mockImplementation(() => ({})),
|
|
19
|
+
isDefaultExportSpan: jest.fn(() => false),
|
|
7
20
|
}));
|
|
8
21
|
|
|
9
22
|
jest.mock('@opentelemetry/sdk-trace-base', () => ({
|
|
10
23
|
BasicTracerProvider: jest.fn().mockImplementation(() => ({
|
|
11
24
|
forceFlush: jest.fn(),
|
|
12
|
-
getTracer:
|
|
25
|
+
getTracer: mockGetTracer,
|
|
13
26
|
shutdown: jest.fn(),
|
|
14
27
|
})),
|
|
15
28
|
}));
|
|
@@ -42,6 +55,50 @@ describe('createLangfuseHandler', () => {
|
|
|
42
55
|
expect(BasicTracerProvider).toHaveBeenCalledTimes(1);
|
|
43
56
|
});
|
|
44
57
|
|
|
58
|
+
it('starts per-agent spans with v5 trace attributes', async () => {
|
|
59
|
+
const handler = createLangfuseHandler({
|
|
60
|
+
langfuse: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
publicKey: 'pk-test',
|
|
63
|
+
secretKey: 'sk-test',
|
|
64
|
+
},
|
|
65
|
+
userId: 'user-1',
|
|
66
|
+
sessionId: 'thread-1',
|
|
67
|
+
traceMetadata: {
|
|
68
|
+
messageId: 'message-1',
|
|
69
|
+
agentId: 'agent-1',
|
|
70
|
+
agentName: 'DWAINE',
|
|
71
|
+
},
|
|
72
|
+
tags: ['librechat', 'agent'],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await handler?.handleChatModelStart(
|
|
76
|
+
{
|
|
77
|
+
id: ['langchain', 'chat_models', 'ChatOpenAI'],
|
|
78
|
+
kwargs: { model: 'gpt-4o' },
|
|
79
|
+
} as unknown as Serialized,
|
|
80
|
+
[[new HumanMessage('hello')]],
|
|
81
|
+
'run-1'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(mockGetTracer).toHaveBeenCalledWith('langfuse-sdk');
|
|
85
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
86
|
+
'gpt-4o',
|
|
87
|
+
expect.objectContaining({
|
|
88
|
+
attributes: expect.objectContaining({
|
|
89
|
+
'langfuse.trace.name': 'LibreChat Agent: DWAINE',
|
|
90
|
+
'langfuse.trace.metadata.agentId': 'agent-1',
|
|
91
|
+
'langfuse.trace.metadata.messageId': 'message-1',
|
|
92
|
+
'langfuse.observation.model.name': 'gpt-4o',
|
|
93
|
+
'langfuse.observation.type': 'generation',
|
|
94
|
+
'user.id': 'user-1',
|
|
95
|
+
'session.id': 'thread-1',
|
|
96
|
+
'langfuse.trace.tags': ['librechat', 'agent'],
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
45
102
|
it('does not create a handler when a required key is missing', () => {
|
|
46
103
|
const handler = createLangfuseHandler({
|
|
47
104
|
langfuse: {
|
package/src/types/run.ts
CHANGED
|
@@ -3,10 +3,7 @@ import type * as z from 'zod';
|
|
|
3
3
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
4
|
import type { StructuredTool } from '@langchain/core/tools';
|
|
5
5
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
6
|
-
import type {
|
|
7
|
-
BaseCallbackHandler,
|
|
8
|
-
CallbackHandlerMethods,
|
|
9
|
-
} from '@langchain/core/callbacks/base';
|
|
6
|
+
import type { Callbacks } from '@langchain/core/callbacks/manager';
|
|
10
7
|
import type * as s from '@/types/stream';
|
|
11
8
|
import type * as e from '@/common/enum';
|
|
12
9
|
import type * as g from '@/types/graph';
|
|
@@ -213,9 +210,7 @@ export type RunConfig = {
|
|
|
213
210
|
humanInTheLoop?: HumanInTheLoopConfig;
|
|
214
211
|
};
|
|
215
212
|
|
|
216
|
-
export type ProvidedCallbacks =
|
|
217
|
-
| (BaseCallbackHandler | CallbackHandlerMethods)[]
|
|
218
|
-
| undefined;
|
|
213
|
+
export type ProvidedCallbacks = Callbacks | undefined;
|
|
219
214
|
|
|
220
215
|
export type TokenCounter = (message: BaseMessage) => number;
|
|
221
216
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ensureHandler } from '@langchain/core/callbacks/manager';
|
|
2
|
+
import type {
|
|
3
|
+
BaseCallbackHandler,
|
|
4
|
+
CallbackHandlerMethods,
|
|
5
|
+
} from '@langchain/core/callbacks/base';
|
|
6
|
+
import type { Callbacks } from '@langchain/core/callbacks/manager';
|
|
7
|
+
|
|
8
|
+
export type CallbackEntry = BaseCallbackHandler | CallbackHandlerMethods;
|
|
9
|
+
|
|
10
|
+
export function appendCallbacks(
|
|
11
|
+
callbacks: Callbacks | undefined,
|
|
12
|
+
additions: readonly CallbackEntry[]
|
|
13
|
+
): Callbacks {
|
|
14
|
+
if (additions.length === 0) {
|
|
15
|
+
return callbacks ?? [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (callbacks == null) {
|
|
19
|
+
return [...additions];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(callbacks)) {
|
|
23
|
+
return callbacks.concat(additions);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return callbacks.copy(additions.map(ensureHandler));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function findCallback(
|
|
30
|
+
callbacks: Callbacks | undefined,
|
|
31
|
+
predicate: (callback: CallbackEntry) => boolean
|
|
32
|
+
): CallbackEntry | undefined {
|
|
33
|
+
if (callbacks == null) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handlers = Array.isArray(callbacks) ? callbacks : callbacks.handlers;
|
|
38
|
+
return handlers.find(predicate);
|
|
39
|
+
}
|