@librechat/agents 3.1.90 → 3.1.91
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/agents/AgentContext.cjs +9 -5
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +46 -14
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +234 -0
- package/dist/cjs/langfuse.cjs.map +1 -0
- package/dist/cjs/main.cjs +25 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +44 -27
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +10 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
- package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
- package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
- package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
- package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
- package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +9 -5
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +46 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +226 -0
- package/dist/esm/langfuse.mjs.map +1 -0
- package/dist/esm/main.mjs +5 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/run.mjs +44 -27
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +10 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
- package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
- package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
- package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
- package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
- package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
- package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +4 -1
- package/dist/types/graphs/Graph.d.ts +6 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/langfuse.d.ts +48 -0
- package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
- package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
- package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
- package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
- package/dist/types/tools/cloudflare/index.d.ts +4 -0
- package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
- package/dist/types/types/graph.d.ts +8 -0
- package/dist/types/types/tools.d.ts +118 -2
- package/package.json +4 -4
- package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
- package/src/agents/AgentContext.ts +13 -3
- package/src/graphs/Graph.ts +53 -16
- package/src/index.ts +1 -0
- package/src/langfuse.ts +358 -0
- package/src/run.ts +60 -38
- package/src/specs/langfuse-config.test.ts +57 -0
- package/src/specs/langfuse-metadata.test.ts +19 -1
- package/src/stream.ts +13 -3
- package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
- package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
- package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
- package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
- package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
- package/src/tools/cloudflare/index.ts +4 -0
- package/src/tools/local/LocalExecutionEngine.ts +20 -4
- package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
- package/src/types/graph.ts +9 -0
- package/src/types/tools.ts +141 -2
|
@@ -2847,6 +2847,72 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2847
2847
|
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2848
2848
|
});
|
|
2849
2849
|
|
|
2850
|
+
it('does not prestart streamed Cloudflare sandbox direct coding tools', async () => {
|
|
2851
|
+
const graph = createGraph({
|
|
2852
|
+
toolExecution: {
|
|
2853
|
+
engine: 'cloudflare-sandbox',
|
|
2854
|
+
cloudflare: { sandbox: {} },
|
|
2855
|
+
} as StandardGraph['toolExecution'],
|
|
2856
|
+
getAgentContext: jest.fn(
|
|
2857
|
+
(): Partial<AgentContext> => ({
|
|
2858
|
+
provider: Providers.OPENAI,
|
|
2859
|
+
reasoningKey: 'reasoning_content',
|
|
2860
|
+
toolDefinitions: [{ name: Constants.BASH_TOOL }, { name: 'weather' }],
|
|
2861
|
+
graphTools: [],
|
|
2862
|
+
agentId: 'agent_1',
|
|
2863
|
+
})
|
|
2864
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2865
|
+
});
|
|
2866
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
2867
|
+
const handler = new ChatModelStreamHandler();
|
|
2868
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2869
|
+
|
|
2870
|
+
await handler.handle(
|
|
2871
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2872
|
+
{
|
|
2873
|
+
chunk: {
|
|
2874
|
+
content: '',
|
|
2875
|
+
tool_call_chunks: [
|
|
2876
|
+
{
|
|
2877
|
+
id: 'call_weather',
|
|
2878
|
+
name: 'weather',
|
|
2879
|
+
args: '{"city":"NYC"}',
|
|
2880
|
+
index: 0,
|
|
2881
|
+
},
|
|
2882
|
+
],
|
|
2883
|
+
} as unknown as t.StreamChunk,
|
|
2884
|
+
},
|
|
2885
|
+
metadata,
|
|
2886
|
+
graph
|
|
2887
|
+
);
|
|
2888
|
+
await handler.handle(
|
|
2889
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2890
|
+
{
|
|
2891
|
+
chunk: {
|
|
2892
|
+
content: '',
|
|
2893
|
+
tool_call_chunks: [
|
|
2894
|
+
{
|
|
2895
|
+
id: 'call_bash',
|
|
2896
|
+
name: Constants.BASH_TOOL,
|
|
2897
|
+
args: '{"command":"echo ok"}',
|
|
2898
|
+
index: 1,
|
|
2899
|
+
},
|
|
2900
|
+
],
|
|
2901
|
+
} as unknown as t.StreamChunk,
|
|
2902
|
+
},
|
|
2903
|
+
metadata,
|
|
2904
|
+
graph
|
|
2905
|
+
);
|
|
2906
|
+
|
|
2907
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
2908
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
2909
|
+
expect.anything(),
|
|
2910
|
+
expect.anything()
|
|
2911
|
+
);
|
|
2912
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2913
|
+
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2914
|
+
});
|
|
2915
|
+
|
|
2850
2916
|
it('prestarts streamed remote bash tools when the next Anthropic tool call begins', async () => {
|
|
2851
2917
|
const graph = createGraph({
|
|
2852
2918
|
getAgentContext: jest.fn(
|
|
@@ -53,6 +53,7 @@ export class AgentContext {
|
|
|
53
53
|
name,
|
|
54
54
|
provider,
|
|
55
55
|
clientOptions,
|
|
56
|
+
langfuse,
|
|
56
57
|
tools,
|
|
57
58
|
toolMap,
|
|
58
59
|
toolEnd,
|
|
@@ -80,6 +81,7 @@ export class AgentContext {
|
|
|
80
81
|
name: name ?? agentId,
|
|
81
82
|
provider,
|
|
82
83
|
clientOptions,
|
|
84
|
+
langfuse,
|
|
83
85
|
maxContextTokens,
|
|
84
86
|
streamBuffer,
|
|
85
87
|
tools,
|
|
@@ -149,6 +151,8 @@ export class AgentContext {
|
|
|
149
151
|
provider: Providers;
|
|
150
152
|
/** Client options for this agent */
|
|
151
153
|
clientOptions?: t.ClientOptions;
|
|
154
|
+
/** Per-agent Langfuse tracing configuration. */
|
|
155
|
+
langfuse?: t.LangfuseConfig;
|
|
152
156
|
/** Token count map indexed by message position */
|
|
153
157
|
indexTokenCountMap: Record<string, number | undefined> = {};
|
|
154
158
|
/** Canonical pre-run token map used to restore token accounting on reset */
|
|
@@ -309,6 +313,7 @@ export class AgentContext {
|
|
|
309
313
|
name,
|
|
310
314
|
provider,
|
|
311
315
|
clientOptions,
|
|
316
|
+
langfuse,
|
|
312
317
|
maxContextTokens,
|
|
313
318
|
streamBuffer,
|
|
314
319
|
tokenCounter,
|
|
@@ -332,6 +337,7 @@ export class AgentContext {
|
|
|
332
337
|
name?: string;
|
|
333
338
|
provider: Providers;
|
|
334
339
|
clientOptions?: t.ClientOptions;
|
|
340
|
+
langfuse?: t.LangfuseConfig;
|
|
335
341
|
maxContextTokens?: number;
|
|
336
342
|
streamBuffer?: number;
|
|
337
343
|
tokenCounter?: t.TokenCounter;
|
|
@@ -355,6 +361,7 @@ export class AgentContext {
|
|
|
355
361
|
this.name = name;
|
|
356
362
|
this.provider = provider;
|
|
357
363
|
this.clientOptions = clientOptions;
|
|
364
|
+
this.langfuse = langfuse;
|
|
358
365
|
this.maxContextTokens = maxContextTokens;
|
|
359
366
|
this.streamBuffer = streamBuffer;
|
|
360
367
|
this.tokenCounter = tokenCounter;
|
|
@@ -458,11 +465,14 @@ export class AgentContext {
|
|
|
458
465
|
}
|
|
459
466
|
|
|
460
467
|
private hasAvailableTool(name: string): boolean {
|
|
461
|
-
if (this.toolDefinitions?.some((tool) => tool.name === name)
|
|
462
|
-
|
|
468
|
+
if (this.toolDefinitions?.some((tool) => tool.name === name) === true)
|
|
469
|
+
return true;
|
|
470
|
+
if (
|
|
471
|
+
this.tools?.some((tool) => 'name' in tool && tool.name === name) === true
|
|
472
|
+
) {
|
|
463
473
|
return true;
|
|
464
474
|
}
|
|
465
|
-
if (this.toolMap?.has(name)) return true;
|
|
475
|
+
if (this.toolMap?.has(name) === true) return true;
|
|
466
476
|
return this.toolRegistry?.has(name) === true;
|
|
467
477
|
}
|
|
468
478
|
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -58,8 +58,10 @@ import { createFakeStreamingLLM } from '@/llm/fake';
|
|
|
58
58
|
import { handleToolCalls } from '@/tools/handlers';
|
|
59
59
|
import { resolveLocalToolsForBinding } from '@/tools/local';
|
|
60
60
|
import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
|
|
61
|
+
import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
|
|
61
62
|
import { isThinkingEnabled } from '@/llm/request';
|
|
62
63
|
import { initializeModel } from '@/llm/init';
|
|
64
|
+
import { createLangfuseHandler, disposeLangfuseHandler } from '@/langfuse';
|
|
63
65
|
import { HandlerRegistry } from '@/events';
|
|
64
66
|
import { ChatOpenAI } from '@/llm/openai';
|
|
65
67
|
import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
|
|
@@ -337,11 +339,12 @@ export abstract class Graph<
|
|
|
337
339
|
/**
|
|
338
340
|
* Single per-Run file checkpointer shared across every ToolNode the
|
|
339
341
|
* graph compiles. Lazily constructed when
|
|
340
|
-
* `toolExecution.local.fileCheckpointing === true`
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
342
|
+
* `toolExecution.local.fileCheckpointing === true` or
|
|
343
|
+
* `toolExecution.cloudflare.fileCheckpointing === true` so
|
|
344
|
+
* multi-agent graphs see ONE snapshot store, not one-per-agent.
|
|
345
|
+
* Returns undefined when checkpointing is disabled or a supported
|
|
346
|
+
* coding-tool engine isn't selected. Exposed via
|
|
347
|
+
* `Run.getFileCheckpointer()` / `Run.rewindFiles()`.
|
|
345
348
|
*/
|
|
346
349
|
private _fileCheckpointer?: t.LocalFileCheckpointer;
|
|
347
350
|
/**
|
|
@@ -364,20 +367,32 @@ export abstract class Graph<
|
|
|
364
367
|
if (this._fileCheckpointer != null) {
|
|
365
368
|
return this._fileCheckpointer;
|
|
366
369
|
}
|
|
367
|
-
if (
|
|
368
|
-
this.toolExecution?.engine !== 'local' ||
|
|
369
|
-
this.toolExecution.local?.fileCheckpointing !== true
|
|
370
|
-
) {
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
370
|
// Eagerly create via the bundle factory so the construction path
|
|
374
371
|
// matches the bundle-only callers (and future bundle-internal
|
|
375
372
|
// cleanup hooks fire). The bundle factory itself accepts a pre-
|
|
376
373
|
// supplied checkpointer when present, so re-injecting this one
|
|
377
374
|
// into every ToolNode is idempotent.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
375
|
+
if (
|
|
376
|
+
this.toolExecution?.engine === 'local' &&
|
|
377
|
+
this.toolExecution.local?.fileCheckpointing === true
|
|
378
|
+
) {
|
|
379
|
+
const bundle = createLocalCodingToolBundle(
|
|
380
|
+
this.toolExecution.local ?? {}
|
|
381
|
+
);
|
|
382
|
+
this._fileCheckpointer = bundle.checkpointer;
|
|
383
|
+
return this._fileCheckpointer;
|
|
384
|
+
}
|
|
385
|
+
if (
|
|
386
|
+
this.toolExecution?.engine === 'cloudflare-sandbox' &&
|
|
387
|
+
this.toolExecution.cloudflare?.fileCheckpointing === true
|
|
388
|
+
) {
|
|
389
|
+
const bundle = createCloudflareCodingToolBundle(
|
|
390
|
+
this.toolExecution.cloudflare
|
|
391
|
+
);
|
|
392
|
+
this._fileCheckpointer = bundle.checkpointer;
|
|
393
|
+
return this._fileCheckpointer;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
381
396
|
}
|
|
382
397
|
}
|
|
383
398
|
|
|
@@ -1318,6 +1333,26 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1318
1333
|
{ force: true }
|
|
1319
1334
|
);
|
|
1320
1335
|
|
|
1336
|
+
const langfuseHandler = createLangfuseHandler({
|
|
1337
|
+
langfuse: agentContext.langfuse,
|
|
1338
|
+
userId: config.configurable?.user_id as string | undefined,
|
|
1339
|
+
sessionId: config.configurable?.thread_id as string | undefined,
|
|
1340
|
+
traceMetadata: {
|
|
1341
|
+
messageId: this.runId,
|
|
1342
|
+
parentMessageId: config.configurable?.requestBody?.parentMessageId,
|
|
1343
|
+
agentId,
|
|
1344
|
+
agentName: agentContext.name,
|
|
1345
|
+
},
|
|
1346
|
+
});
|
|
1347
|
+
const invokeConfig = langfuseHandler
|
|
1348
|
+
? {
|
|
1349
|
+
...config,
|
|
1350
|
+
callbacks: ((config.callbacks as t.ProvidedCallbacks) ?? []).concat(
|
|
1351
|
+
[langfuseHandler]
|
|
1352
|
+
),
|
|
1353
|
+
}
|
|
1354
|
+
: config;
|
|
1355
|
+
|
|
1321
1356
|
try {
|
|
1322
1357
|
result = await attemptInvoke(
|
|
1323
1358
|
{
|
|
@@ -1326,17 +1361,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1326
1361
|
provider: agentContext.provider,
|
|
1327
1362
|
context: this,
|
|
1328
1363
|
},
|
|
1329
|
-
|
|
1364
|
+
invokeConfig
|
|
1330
1365
|
);
|
|
1331
1366
|
} catch (primaryError) {
|
|
1332
1367
|
result = await tryFallbackProviders({
|
|
1333
1368
|
fallbacks,
|
|
1334
1369
|
tools: agentContext.tools,
|
|
1335
1370
|
messages: finalMessages,
|
|
1336
|
-
config,
|
|
1371
|
+
config: invokeConfig,
|
|
1337
1372
|
primaryError,
|
|
1338
1373
|
context: this,
|
|
1339
1374
|
});
|
|
1375
|
+
} finally {
|
|
1376
|
+
await disposeLangfuseHandler(langfuseHandler);
|
|
1340
1377
|
}
|
|
1341
1378
|
|
|
1342
1379
|
if (!result) {
|
package/src/index.ts
CHANGED
package/src/langfuse.ts
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { CallbackHandler } from '@langfuse/langchain';
|
|
2
|
+
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
3
|
+
import {
|
|
4
|
+
createObservationAttributes,
|
|
5
|
+
createTraceAttributes,
|
|
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 { Span } from '@opentelemetry/api';
|
|
14
|
+
import type * as t from '@/types';
|
|
15
|
+
import { isPresent } from '@/utils/misc';
|
|
16
|
+
|
|
17
|
+
type TraceMetadata = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
type LangfuseHandlerParams = {
|
|
20
|
+
userId?: string;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
traceMetadata?: TraceMetadata;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type AgentLangfuseHandlerParams = LangfuseHandlerParams & {
|
|
26
|
+
langfuse?: t.LangfuseConfig;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type ResolvedLangfuseConfig = t.LangfuseConfig & {
|
|
30
|
+
enabled: true;
|
|
31
|
+
publicKey: string;
|
|
32
|
+
secretKey: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function getModelName(serialized: Serialized): string {
|
|
36
|
+
const serializedRecord = serialized as unknown as Record<string, unknown>;
|
|
37
|
+
const kwargs = serializedRecord.kwargs as Record<string, unknown> | undefined;
|
|
38
|
+
const modelName =
|
|
39
|
+
kwargs?.model ??
|
|
40
|
+
kwargs?.model_name ??
|
|
41
|
+
kwargs?.modelName ??
|
|
42
|
+
kwargs?.model_id ??
|
|
43
|
+
kwargs?.modelId ??
|
|
44
|
+
serializedRecord.name;
|
|
45
|
+
|
|
46
|
+
if (typeof modelName === 'string' && modelName.trim() !== '') {
|
|
47
|
+
return modelName;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(serializedRecord.id) && serializedRecord.id.length > 0) {
|
|
51
|
+
return String(serializedRecord.id[serializedRecord.id.length - 1]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return 'ChatModel';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getModelParameters(
|
|
58
|
+
extraParams?: Record<string, unknown>
|
|
59
|
+
): Record<string, string | number> {
|
|
60
|
+
const invocationParams = extraParams?.invocation_params;
|
|
61
|
+
const params =
|
|
62
|
+
invocationParams != null && typeof invocationParams === 'object'
|
|
63
|
+
? (invocationParams as Record<string, unknown>)
|
|
64
|
+
: (extraParams ?? {});
|
|
65
|
+
|
|
66
|
+
return Object.fromEntries(
|
|
67
|
+
Object.entries(params).filter(([, value]) => {
|
|
68
|
+
return typeof value === 'string' || typeof value === 'number';
|
|
69
|
+
})
|
|
70
|
+
) as Record<string, string | number>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getOutput(output: LLMResult): unknown {
|
|
74
|
+
return output.generations.map((generation) =>
|
|
75
|
+
generation.map((item) => {
|
|
76
|
+
if ('message' in item && item.message != null) {
|
|
77
|
+
return (item.message as { content?: unknown }).content;
|
|
78
|
+
}
|
|
79
|
+
return item.text;
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getUsageDetails(
|
|
85
|
+
output: LLMResult
|
|
86
|
+
): Record<string, number> | undefined {
|
|
87
|
+
const llmOutput = output.llmOutput as Record<string, unknown> | undefined;
|
|
88
|
+
const usage = llmOutput?.tokenUsage ?? llmOutput?.usage;
|
|
89
|
+
if (usage == null || typeof usage !== 'object') {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const usageEntries = Object.entries(usage as Record<string, unknown>).filter(
|
|
94
|
+
([, value]) => typeof value === 'number'
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return usageEntries.length > 0
|
|
98
|
+
? (Object.fromEntries(usageEntries) as Record<string, number>)
|
|
99
|
+
: undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getTraceName(traceMetadata?: TraceMetadata): string {
|
|
103
|
+
const agentName = traceMetadata?.agentName;
|
|
104
|
+
return typeof agentName === 'string' && agentName.trim() !== ''
|
|
105
|
+
? `LibreChat Agent: ${agentName}`
|
|
106
|
+
: 'LibreChat Agent';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class LangfuseAgentCallbackHandler extends BaseCallbackHandler {
|
|
110
|
+
name = 'librechat_langfuse_agent_handler';
|
|
111
|
+
|
|
112
|
+
private readonly provider: BasicTracerProvider;
|
|
113
|
+
private readonly processor: LangfuseSpanProcessor;
|
|
114
|
+
private readonly userId?: string;
|
|
115
|
+
private readonly sessionId?: string;
|
|
116
|
+
private readonly traceMetadata?: TraceMetadata;
|
|
117
|
+
private readonly spans = new Map<string, Span>();
|
|
118
|
+
|
|
119
|
+
constructor({
|
|
120
|
+
langfuse,
|
|
121
|
+
userId,
|
|
122
|
+
sessionId,
|
|
123
|
+
traceMetadata,
|
|
124
|
+
}: LangfuseHandlerParams & { langfuse: ResolvedLangfuseConfig }) {
|
|
125
|
+
super();
|
|
126
|
+
this.userId = userId;
|
|
127
|
+
this.sessionId = sessionId;
|
|
128
|
+
this.traceMetadata = traceMetadata;
|
|
129
|
+
this.processor = new LangfuseSpanProcessor({
|
|
130
|
+
publicKey: langfuse.publicKey,
|
|
131
|
+
secretKey: langfuse.secretKey,
|
|
132
|
+
...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
|
|
133
|
+
environment:
|
|
134
|
+
process.env.LANGFUSE_TRACING_ENVIRONMENT ??
|
|
135
|
+
process.env.NODE_ENV ??
|
|
136
|
+
'development',
|
|
137
|
+
exportMode: 'immediate',
|
|
138
|
+
});
|
|
139
|
+
this.provider = new BasicTracerProvider({
|
|
140
|
+
spanProcessors: [this.processor],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private startGenerationSpan({
|
|
145
|
+
llm,
|
|
146
|
+
input,
|
|
147
|
+
runId,
|
|
148
|
+
extraParams,
|
|
149
|
+
metadata,
|
|
150
|
+
name,
|
|
151
|
+
}: {
|
|
152
|
+
llm: Serialized;
|
|
153
|
+
input: unknown;
|
|
154
|
+
runId: string;
|
|
155
|
+
extraParams?: Record<string, unknown>;
|
|
156
|
+
metadata?: Record<string, unknown>;
|
|
157
|
+
name?: string;
|
|
158
|
+
}): void {
|
|
159
|
+
if (this.spans.has(runId)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const tracer = this.provider.getTracer('librechat-agents-langfuse');
|
|
164
|
+
const spanName =
|
|
165
|
+
typeof name === 'string' && name.trim() !== '' ? name : getModelName(llm);
|
|
166
|
+
const span = tracer.startSpan(spanName, {
|
|
167
|
+
attributes: {
|
|
168
|
+
...createTraceAttributes({
|
|
169
|
+
name: getTraceName(this.traceMetadata),
|
|
170
|
+
userId: this.userId,
|
|
171
|
+
sessionId: this.sessionId,
|
|
172
|
+
metadata: this.traceMetadata,
|
|
173
|
+
}),
|
|
174
|
+
...createObservationAttributes('generation', {
|
|
175
|
+
input,
|
|
176
|
+
model: getModelName(llm),
|
|
177
|
+
modelParameters: getModelParameters(extraParams),
|
|
178
|
+
metadata: {
|
|
179
|
+
...metadata,
|
|
180
|
+
...this.traceMetadata,
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
this.spans.set(runId, span);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async handleChatModelStart(
|
|
189
|
+
llm: Serialized,
|
|
190
|
+
messages: BaseMessage[][],
|
|
191
|
+
runId: string,
|
|
192
|
+
_parentRunId?: string,
|
|
193
|
+
extraParams?: Record<string, unknown>,
|
|
194
|
+
_tags?: string[],
|
|
195
|
+
metadata?: Record<string, unknown>,
|
|
196
|
+
name?: string
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
this.startGenerationSpan({
|
|
199
|
+
llm,
|
|
200
|
+
input: messages,
|
|
201
|
+
runId,
|
|
202
|
+
extraParams,
|
|
203
|
+
metadata,
|
|
204
|
+
name,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async handleLLMStart(
|
|
209
|
+
llm: Serialized,
|
|
210
|
+
prompts: string[],
|
|
211
|
+
runId: string,
|
|
212
|
+
_parentRunId?: string,
|
|
213
|
+
extraParams?: Record<string, unknown>,
|
|
214
|
+
_tags?: string[],
|
|
215
|
+
metadata?: Record<string, unknown>,
|
|
216
|
+
name?: string
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
this.startGenerationSpan({
|
|
219
|
+
llm,
|
|
220
|
+
input: prompts,
|
|
221
|
+
runId,
|
|
222
|
+
extraParams,
|
|
223
|
+
metadata,
|
|
224
|
+
name,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async handleLLMEnd(output: LLMResult, runId: string): Promise<void> {
|
|
229
|
+
const span = this.spans.get(runId);
|
|
230
|
+
if (!span) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
span.setAttributes(
|
|
235
|
+
createObservationAttributes('generation', {
|
|
236
|
+
output: getOutput(output),
|
|
237
|
+
usageDetails: getUsageDetails(output),
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
span.end();
|
|
241
|
+
this.spans.delete(runId);
|
|
242
|
+
await this.flush();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async handleLLMError(err: unknown, runId: string): Promise<void> {
|
|
246
|
+
const span = this.spans.get(runId);
|
|
247
|
+
if (!span) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
252
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
253
|
+
span.setAttributes(
|
|
254
|
+
createObservationAttributes('generation', {
|
|
255
|
+
level: 'ERROR',
|
|
256
|
+
statusMessage: message,
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
span.end();
|
|
260
|
+
this.spans.delete(runId);
|
|
261
|
+
await this.flush();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async flush(): Promise<void> {
|
|
265
|
+
try {
|
|
266
|
+
await this.provider.forceFlush();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
process.emitWarning(
|
|
269
|
+
`[LangfuseAgentCallbackHandler] Failed to flush Langfuse spans: ${
|
|
270
|
+
error instanceof Error ? error.message : String(error)
|
|
271
|
+
}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async dispose(): Promise<void> {
|
|
277
|
+
for (const span of this.spans.values()) {
|
|
278
|
+
span.end();
|
|
279
|
+
}
|
|
280
|
+
this.spans.clear();
|
|
281
|
+
await this.flush();
|
|
282
|
+
try {
|
|
283
|
+
await this.provider.shutdown();
|
|
284
|
+
} catch (error) {
|
|
285
|
+
process.emitWarning(
|
|
286
|
+
`[LangfuseAgentCallbackHandler] Failed to shut down Langfuse provider: ${
|
|
287
|
+
error instanceof Error ? error.message : String(error)
|
|
288
|
+
}`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function hasRequiredLangfuseConfig(
|
|
295
|
+
langfuse?: t.LangfuseConfig
|
|
296
|
+
): langfuse is ResolvedLangfuseConfig {
|
|
297
|
+
return (
|
|
298
|
+
langfuse?.enabled === true &&
|
|
299
|
+
isPresent(langfuse.publicKey) &&
|
|
300
|
+
isPresent(langfuse.secretKey)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function createLegacyLangfuseHandler(
|
|
305
|
+
params: LangfuseHandlerParams
|
|
306
|
+
): CallbackHandler {
|
|
307
|
+
return new CallbackHandler(params);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function createLangfuseHandler({
|
|
311
|
+
langfuse,
|
|
312
|
+
userId,
|
|
313
|
+
sessionId,
|
|
314
|
+
traceMetadata,
|
|
315
|
+
}: AgentLangfuseHandlerParams): LangfuseAgentCallbackHandler | undefined {
|
|
316
|
+
if (!hasRequiredLangfuseConfig(langfuse)) {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return new LangfuseAgentCallbackHandler({
|
|
321
|
+
langfuse,
|
|
322
|
+
userId,
|
|
323
|
+
sessionId,
|
|
324
|
+
traceMetadata,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function hasExplicitLangfuseConfig(
|
|
329
|
+
contexts: Iterable<{ langfuse?: t.LangfuseConfig }>
|
|
330
|
+
): boolean {
|
|
331
|
+
for (const context of contexts) {
|
|
332
|
+
if (context.langfuse != null) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function hasLangfuseEnvConfig(): boolean {
|
|
340
|
+
return (
|
|
341
|
+
isPresent(process.env.LANGFUSE_SECRET_KEY) &&
|
|
342
|
+
isPresent(process.env.LANGFUSE_PUBLIC_KEY) &&
|
|
343
|
+
isPresent(process.env.LANGFUSE_BASE_URL)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function isLangfuseCallbackHandler(value: unknown): boolean {
|
|
348
|
+
return (
|
|
349
|
+
value instanceof CallbackHandler ||
|
|
350
|
+
value instanceof LangfuseAgentCallbackHandler
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function disposeLangfuseHandler(value: unknown): Promise<void> {
|
|
355
|
+
if (value instanceof LangfuseAgentCallbackHandler) {
|
|
356
|
+
await value.dispose();
|
|
357
|
+
}
|
|
358
|
+
}
|