@librechat/agents 3.1.95 → 3.1.97

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.
Files changed (67) hide show
  1. package/dist/cjs/graphs/Graph.cjs +54 -21
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +120 -9
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuse.cjs +30 -226
  6. package/dist/cjs/langfuse.cjs.map +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +465 -0
  8. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -0
  9. package/dist/cjs/main.cjs +1 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/run.cjs +142 -69
  12. package/dist/cjs/run.cjs.map +1 -1
  13. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +29 -2
  14. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  15. package/dist/cjs/tools/ToolNode.cjs +20 -8
  16. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  17. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +10 -6
  18. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  19. package/dist/esm/graphs/Graph.mjs +56 -23
  20. package/dist/esm/graphs/Graph.mjs.map +1 -1
  21. package/dist/esm/instrumentation.mjs +118 -9
  22. package/dist/esm/instrumentation.mjs.map +1 -1
  23. package/dist/esm/langfuse.mjs +28 -224
  24. package/dist/esm/langfuse.mjs.map +1 -1
  25. package/dist/esm/langfuseToolOutputTracing.mjs +457 -0
  26. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -0
  27. package/dist/esm/main.mjs +1 -1
  28. package/dist/esm/run.mjs +144 -71
  29. package/dist/esm/run.mjs.map +1 -1
  30. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +29 -3
  31. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  32. package/dist/esm/tools/ToolNode.mjs +20 -8
  33. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  34. package/dist/esm/tools/subagent/SubagentExecutor.mjs +10 -6
  35. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  36. package/dist/types/graphs/Graph.d.ts +5 -1
  37. package/dist/types/instrumentation.d.ts +5 -1
  38. package/dist/types/langfuse.d.ts +6 -28
  39. package/dist/types/langfuseToolOutputTracing.d.ts +20 -0
  40. package/dist/types/run.d.ts +5 -1
  41. package/dist/types/tools/BashProgrammaticToolCalling.d.ts +1 -0
  42. package/dist/types/tools/ToolNode.d.ts +4 -1
  43. package/dist/types/tools/subagent/SubagentExecutor.d.ts +2 -0
  44. package/dist/types/types/graph.d.ts +30 -0
  45. package/dist/types/types/run.d.ts +6 -0
  46. package/dist/types/types/tools.d.ts +7 -0
  47. package/package.json +2 -1
  48. package/src/graphs/Graph.ts +90 -34
  49. package/src/instrumentation.ts +172 -11
  50. package/src/langfuse.ts +59 -324
  51. package/src/langfuseToolOutputTracing.ts +683 -0
  52. package/src/run.ts +190 -87
  53. package/src/specs/langfuse-callbacks.test.ts +178 -1
  54. package/src/specs/langfuse-config.test.ts +112 -76
  55. package/src/specs/langfuse-instrumentation.test.ts +283 -0
  56. package/src/specs/langfuse-metadata.test.ts +54 -1
  57. package/src/specs/langfuse-tool-output-tracing.test.ts +588 -0
  58. package/src/tools/BashProgrammaticToolCalling.ts +39 -5
  59. package/src/tools/ToolNode.ts +28 -7
  60. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +54 -0
  61. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +72 -4
  62. package/src/tools/__tests__/SubagentExecutor.test.ts +32 -0
  63. package/src/tools/__tests__/ToolNode.langfuse.test.ts +41 -0
  64. package/src/tools/subagent/SubagentExecutor.ts +11 -6
  65. package/src/types/graph.ts +32 -0
  66. package/src/types/run.ts +6 -0
  67. package/src/types/tools.ts +7 -0
package/src/langfuse.ts CHANGED
@@ -1,21 +1,9 @@
1
+ import { getLangfuseTracerProvider } from '@langfuse/tracing';
1
2
  import { CallbackHandler } from '@langfuse/langchain';
2
- import { isDefaultExportSpan, LangfuseSpanProcessor } from '@langfuse/otel';
3
- import {
4
- LangfuseOtelSpanAttributes,
5
- createObservationAttributes,
6
- } from '@langfuse/tracing';
7
- import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
8
- import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
9
- import { SpanStatusCode } from '@opentelemetry/api';
10
- import type { Serialized } from '@langchain/core/load/serializable';
11
- import type { BaseMessage } from '@langchain/core/messages';
12
- import type { LLMResult } from '@langchain/core/outputs';
13
- import type { Attributes, Span } from '@opentelemetry/api';
14
3
  import type * as t from '@/types';
15
4
  import { isPresent } from '@/utils/misc';
16
5
 
17
6
  const TRACE_METADATA_MAX_LENGTH = 200;
18
- const LANGFUSE_TRACER_NAME = 'langfuse-sdk';
19
7
 
20
8
  export type LangfuseTraceMetadata = Record<string, string>;
21
9
 
@@ -30,14 +18,43 @@ type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
30
18
  langfuse?: t.LangfuseConfig;
31
19
  };
32
20
 
33
- type ResolvedLangfuseConfig = t.LangfuseConfig & {
34
- enabled: true;
21
+ type FlushableTracerProvider = {
22
+ forceFlush?: () => Promise<void> | void;
23
+ };
24
+
25
+ function hasLangfuseTracingConfig(langfuse?: t.LangfuseConfig): boolean {
26
+ return (
27
+ langfuse?.toolNodeTracing != null || langfuse?.toolOutputTracing != null
28
+ );
29
+ }
30
+
31
+ export function hasLangfuseConfigCredentials(
32
+ langfuse?: t.LangfuseConfig
33
+ ): langfuse is t.LangfuseConfig & {
35
34
  publicKey: string;
36
35
  secretKey: string;
37
- };
36
+ } {
37
+ return (
38
+ langfuse != null &&
39
+ isPresent(langfuse.publicKey) &&
40
+ isPresent(langfuse.secretKey)
41
+ );
42
+ }
43
+
44
+ function hasLangfuseConfigBaseUrl(langfuse?: t.LangfuseConfig): boolean {
45
+ return isPresent(langfuse?.baseUrl);
46
+ }
38
47
 
39
- function getEnvLangfuseBaseUrl(): string | undefined {
40
- return process.env.LANGFUSE_BASE_URL ?? process.env.LANGFUSE_BASEURL;
48
+ export function isExplicitLangfuseConfig(
49
+ langfuse?: t.LangfuseConfig
50
+ ): boolean {
51
+ return (
52
+ langfuse?.enabled != null ||
53
+ isPresent(langfuse?.publicKey) ||
54
+ isPresent(langfuse?.secretKey) ||
55
+ isPresent(langfuse?.baseUrl) ||
56
+ hasLangfuseTracingConfig(langfuse)
57
+ );
41
58
  }
42
59
 
43
60
  function createTraceMetadata(
@@ -79,73 +96,6 @@ export function createLangfuseTraceMetadata({
79
96
  });
80
97
  }
81
98
 
82
- function getModelName(serialized: Serialized): string {
83
- const serializedRecord = serialized as unknown as Record<string, unknown>;
84
- const kwargs = serializedRecord.kwargs as Record<string, unknown> | undefined;
85
- const modelName =
86
- kwargs?.model ??
87
- kwargs?.model_name ??
88
- kwargs?.modelName ??
89
- kwargs?.model_id ??
90
- kwargs?.modelId ??
91
- serializedRecord.name;
92
-
93
- if (typeof modelName === 'string' && modelName.trim() !== '') {
94
- return modelName;
95
- }
96
-
97
- if (Array.isArray(serializedRecord.id) && serializedRecord.id.length > 0) {
98
- return String(serializedRecord.id[serializedRecord.id.length - 1]);
99
- }
100
-
101
- return 'ChatModel';
102
- }
103
-
104
- function getModelParameters(
105
- extraParams?: Record<string, unknown>
106
- ): Record<string, string | number> {
107
- const invocationParams = extraParams?.invocation_params;
108
- const params =
109
- invocationParams != null && typeof invocationParams === 'object'
110
- ? (invocationParams as Record<string, unknown>)
111
- : (extraParams ?? {});
112
-
113
- return Object.fromEntries(
114
- Object.entries(params).filter(([, value]) => {
115
- return typeof value === 'string' || typeof value === 'number';
116
- })
117
- ) as Record<string, string | number>;
118
- }
119
-
120
- function getOutput(output: LLMResult): unknown {
121
- return output.generations.map((generation) =>
122
- generation.map((item) => {
123
- if ('message' in item && item.message != null) {
124
- return (item.message as { content?: unknown }).content;
125
- }
126
- return item.text;
127
- })
128
- );
129
- }
130
-
131
- function getUsageDetails(
132
- output: LLMResult
133
- ): Record<string, number> | undefined {
134
- const llmOutput = output.llmOutput as Record<string, unknown> | undefined;
135
- const usage = llmOutput?.tokenUsage ?? llmOutput?.usage;
136
- if (usage == null || typeof usage !== 'object') {
137
- return undefined;
138
- }
139
-
140
- const usageEntries = Object.entries(usage as Record<string, unknown>).filter(
141
- ([, value]) => typeof value === 'number'
142
- );
143
-
144
- return usageEntries.length > 0
145
- ? (Object.fromEntries(usageEntries) as Record<string, number>)
146
- : undefined;
147
- }
148
-
149
99
  export function getLangfuseTraceName(
150
100
  traceMetadata?: LangfuseTraceMetadata,
151
101
  fallback: string = 'LibreChat Agent'
@@ -154,231 +104,27 @@ export function getLangfuseTraceName(
154
104
  return isPresent(agentName) ? `${fallback}: ${agentName}` : fallback;
155
105
  }
156
106
 
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
+ export function hasLangfuseEnvConfig(): boolean {
108
+ return hasLangfuseEnvCredentials();
182
109
  }
183
110
 
184
- export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
185
- name = 'librechat_langfuse_agent_handler';
186
-
187
- private readonly provider: BasicTracerProvider;
188
- private readonly processor: LangfuseSpanProcessor;
189
- private readonly userId?: string;
190
- private readonly sessionId?: string;
191
- private readonly traceMetadata?: LangfuseTraceMetadata;
192
- private readonly tags?: string[];
193
- private readonly spans = new Map<string, Span>();
194
-
195
- constructor({
196
- langfuse,
197
- userId,
198
- sessionId,
199
- traceMetadata,
200
- tags,
201
- }: LangfuseHandlerParams & { langfuse: ResolvedLangfuseConfig }) {
202
- super();
203
- this.userId = userId;
204
- this.sessionId = sessionId;
205
- this.traceMetadata = traceMetadata;
206
- this.tags = tags;
207
- this.processor = new LangfuseSpanProcessor({
208
- publicKey: langfuse.publicKey,
209
- secretKey: langfuse.secretKey,
210
- ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
211
- environment:
212
- process.env.LANGFUSE_TRACING_ENVIRONMENT ??
213
- process.env.NODE_ENV ??
214
- 'development',
215
- exportMode: 'immediate',
216
- shouldExportSpan: ({ otelSpan }): boolean =>
217
- isDefaultExportSpan(otelSpan) ||
218
- otelSpan.instrumentationScope.name === LANGFUSE_TRACER_NAME,
219
- });
220
- this.provider = new BasicTracerProvider({
221
- spanProcessors: [this.processor],
222
- });
223
- }
224
-
225
- private startGenerationSpan({
226
- llm,
227
- input,
228
- runId,
229
- extraParams,
230
- metadata,
231
- name,
232
- }: {
233
- llm: Serialized;
234
- input: unknown;
235
- runId: string;
236
- extraParams?: Record<string, unknown>;
237
- metadata?: Record<string, unknown>;
238
- name?: string;
239
- }): void {
240
- if (this.spans.has(runId)) {
241
- return;
242
- }
243
-
244
- const tracer = this.provider.getTracer(LANGFUSE_TRACER_NAME);
245
- const spanName =
246
- typeof name === 'string' && name.trim() !== '' ? name : getModelName(llm);
247
- const span = tracer.startSpan(spanName, {
248
- attributes: {
249
- ...getTraceAttributes({
250
- userId: this.userId,
251
- sessionId: this.sessionId,
252
- traceMetadata: this.traceMetadata,
253
- tags: this.tags,
254
- }),
255
- ...createObservationAttributes('generation', {
256
- input,
257
- model: getModelName(llm),
258
- modelParameters: getModelParameters(extraParams),
259
- metadata: {
260
- ...metadata,
261
- ...this.traceMetadata,
262
- },
263
- }),
264
- },
265
- });
266
- this.spans.set(runId, span);
267
- }
268
-
269
- async handleChatModelStart(
270
- llm: Serialized,
271
- messages: BaseMessage[][],
272
- runId: string,
273
- _parentRunId?: string,
274
- extraParams?: Record<string, unknown>,
275
- _tags?: string[],
276
- metadata?: Record<string, unknown>,
277
- name?: string
278
- ): Promise<void> {
279
- this.startGenerationSpan({
280
- llm,
281
- input: messages,
282
- runId,
283
- extraParams,
284
- metadata,
285
- name,
286
- });
287
- }
288
-
289
- async handleLLMStart(
290
- llm: Serialized,
291
- prompts: string[],
292
- runId: string,
293
- _parentRunId?: string,
294
- extraParams?: Record<string, unknown>,
295
- _tags?: string[],
296
- metadata?: Record<string, unknown>,
297
- name?: string
298
- ): Promise<void> {
299
- this.startGenerationSpan({
300
- llm,
301
- input: prompts,
302
- runId,
303
- extraParams,
304
- metadata,
305
- name,
306
- });
307
- }
308
-
309
- async handleLLMEnd(output: LLMResult, runId: string): Promise<void> {
310
- const span = this.spans.get(runId);
311
- if (!span) {
312
- return;
313
- }
314
-
315
- span.setAttributes(
316
- createObservationAttributes('generation', {
317
- output: getOutput(output),
318
- usageDetails: getUsageDetails(output),
319
- })
320
- );
321
- span.end();
322
- this.spans.delete(runId);
323
- await this.flush();
324
- }
325
-
326
- async handleLLMError(err: unknown, runId: string): Promise<void> {
327
- const span = this.spans.get(runId);
328
- if (!span) {
329
- return;
330
- }
331
-
332
- const message = err instanceof Error ? err.message : String(err);
333
- span.setStatus({ code: SpanStatusCode.ERROR, message });
334
- span.setAttributes(
335
- createObservationAttributes('generation', {
336
- level: 'ERROR',
337
- statusMessage: message,
338
- })
339
- );
340
- span.end();
341
- this.spans.delete(runId);
342
- await this.flush();
343
- }
344
-
345
- private async flush(): Promise<void> {
346
- try {
347
- await this.provider.forceFlush();
348
- } catch (error) {
349
- process.emitWarning(
350
- `[LangfuseAgentCallbackHandler] Failed to flush Langfuse spans: ${
351
- error instanceof Error ? error.message : String(error)
352
- }`
353
- );
354
- }
355
- }
356
-
357
- async dispose(): Promise<void> {
358
- for (const span of this.spans.values()) {
359
- span.end();
360
- }
361
- this.spans.clear();
362
- await this.flush();
363
- try {
364
- await this.provider.shutdown();
365
- } catch (error) {
366
- process.emitWarning(
367
- `[LangfuseAgentCallbackHandler] Failed to shut down Langfuse provider: ${
368
- error instanceof Error ? error.message : String(error)
369
- }`
370
- );
371
- }
372
- }
111
+ export function hasLangfuseEnvCredentials(): boolean {
112
+ return (
113
+ isPresent(process.env.LANGFUSE_SECRET_KEY) &&
114
+ isPresent(process.env.LANGFUSE_PUBLIC_KEY)
115
+ );
373
116
  }
374
117
 
375
- function hasRequiredLangfuseConfig(
118
+ export function shouldCreateLangfuseHandler(
376
119
  langfuse?: t.LangfuseConfig
377
- ): langfuse is ResolvedLangfuseConfig {
120
+ ): boolean {
121
+ if (langfuse?.enabled === false) {
122
+ return false;
123
+ }
378
124
  return (
379
- langfuse?.enabled === true &&
380
- isPresent(langfuse.publicKey) &&
381
- isPresent(langfuse.secretKey)
125
+ hasLangfuseEnvConfig() ||
126
+ hasLangfuseConfigCredentials(langfuse) ||
127
+ (hasLangfuseConfigBaseUrl(langfuse) && hasLangfuseEnvCredentials())
382
128
  );
383
129
  }
384
130
 
@@ -394,13 +140,11 @@ export function createLangfuseHandler({
394
140
  sessionId,
395
141
  traceMetadata,
396
142
  tags,
397
- }: AgentLangfuseHandlerParams): LangfuseAgentCallbackHandler | undefined {
398
- if (!hasRequiredLangfuseConfig(langfuse)) {
143
+ }: AgentLangfuseHandlerParams): CallbackHandler | undefined {
144
+ if (!shouldCreateLangfuseHandler(langfuse)) {
399
145
  return undefined;
400
146
  }
401
-
402
- return new LangfuseAgentCallbackHandler({
403
- langfuse,
147
+ return new CallbackHandler({
404
148
  userId,
405
149
  sessionId,
406
150
  traceMetadata,
@@ -412,30 +156,21 @@ export function hasExplicitLangfuseConfig(
412
156
  contexts: Iterable<{ langfuse?: t.LangfuseConfig }>
413
157
  ): boolean {
414
158
  for (const context of contexts) {
415
- if (context.langfuse != null) {
159
+ if (isExplicitLangfuseConfig(context.langfuse)) {
416
160
  return true;
417
161
  }
418
162
  }
419
163
  return false;
420
164
  }
421
165
 
422
- export function hasLangfuseEnvConfig(): boolean {
423
- return (
424
- isPresent(process.env.LANGFUSE_SECRET_KEY) &&
425
- isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
426
- isPresent(getEnvLangfuseBaseUrl())
427
- );
428
- }
429
-
430
166
  export function isLangfuseCallbackHandler(value: unknown): boolean {
431
- return (
432
- value instanceof CallbackHandler ||
433
- value instanceof LangfuseAgentCallbackHandler
434
- );
167
+ return value instanceof CallbackHandler;
435
168
  }
436
169
 
437
170
  export async function disposeLangfuseHandler(value: unknown): Promise<void> {
438
- if (value instanceof LangfuseAgentCallbackHandler) {
439
- await value.dispose();
171
+ if (value == null) {
172
+ return;
440
173
  }
174
+ const provider = getLangfuseTracerProvider() as FlushableTracerProvider;
175
+ await provider.forceFlush?.();
441
176
  }