@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.
- package/dist/cjs/graphs/Graph.cjs +54 -21
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +120 -9
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +30 -226
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +465 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -0
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/run.cjs +142 -69
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +29 -2
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +20 -8
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +10 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +56 -23
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +118 -9
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +28 -224
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +457 -0
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -0
- package/dist/esm/main.mjs +1 -1
- package/dist/esm/run.mjs +144 -71
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +29 -3
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +20 -8
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +10 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +5 -1
- package/dist/types/instrumentation.d.ts +5 -1
- package/dist/types/langfuse.d.ts +6 -28
- package/dist/types/langfuseToolOutputTracing.d.ts +20 -0
- package/dist/types/run.d.ts +5 -1
- package/dist/types/tools/BashProgrammaticToolCalling.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +4 -1
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +2 -0
- package/dist/types/types/graph.d.ts +30 -0
- package/dist/types/types/run.d.ts +6 -0
- package/dist/types/types/tools.d.ts +7 -0
- package/package.json +2 -1
- package/src/graphs/Graph.ts +90 -34
- package/src/instrumentation.ts +172 -11
- package/src/langfuse.ts +59 -324
- package/src/langfuseToolOutputTracing.ts +683 -0
- package/src/run.ts +190 -87
- package/src/specs/langfuse-callbacks.test.ts +178 -1
- package/src/specs/langfuse-config.test.ts +112 -76
- package/src/specs/langfuse-instrumentation.test.ts +283 -0
- package/src/specs/langfuse-metadata.test.ts +54 -1
- package/src/specs/langfuse-tool-output-tracing.test.ts +588 -0
- package/src/tools/BashProgrammaticToolCalling.ts +39 -5
- package/src/tools/ToolNode.ts +28 -7
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +54 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +72 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +32 -0
- package/src/tools/__tests__/ToolNode.langfuse.test.ts +41 -0
- package/src/tools/subagent/SubagentExecutor.ts +11 -6
- package/src/types/graph.ts +32 -0
- package/src/types/run.ts +6 -0
- package/src/types/tools.ts +7 -0
package/src/run.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/run.ts
|
|
2
|
-
import './instrumentation';
|
|
2
|
+
import { initializeLangfuseTracing } from './instrumentation';
|
|
3
3
|
import { PromptTemplate } from '@langchain/core/prompts';
|
|
4
4
|
import { RunnableLambda } from '@langchain/core/runnables';
|
|
5
5
|
import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
|
|
@@ -36,15 +36,16 @@ import {
|
|
|
36
36
|
type CallbackEntry,
|
|
37
37
|
} from '@/utils/callbacks';
|
|
38
38
|
import {
|
|
39
|
-
createLegacyLangfuseHandler,
|
|
40
39
|
createLangfuseTraceMetadata,
|
|
41
40
|
createLangfuseHandler,
|
|
42
41
|
disposeLangfuseHandler,
|
|
43
42
|
getLangfuseTraceName,
|
|
44
|
-
hasExplicitLangfuseConfig,
|
|
45
|
-
hasLangfuseEnvConfig,
|
|
46
43
|
isLangfuseCallbackHandler,
|
|
47
44
|
} from '@/langfuse';
|
|
45
|
+
import {
|
|
46
|
+
resolveLangfuseConfig,
|
|
47
|
+
withLangfuseToolOutputTracingConfig,
|
|
48
|
+
} from '@/langfuseToolOutputTracing';
|
|
48
49
|
import type { HookRegistry } from '@/hooks';
|
|
49
50
|
|
|
50
51
|
export const defaultOmitOptions = new Set([
|
|
@@ -120,6 +121,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
120
121
|
private handlerRegistry?: HandlerRegistry;
|
|
121
122
|
private hookRegistry?: HookRegistry;
|
|
122
123
|
private humanInTheLoop?: t.HumanInTheLoopConfig;
|
|
124
|
+
private langfuse?: t.LangfuseConfig;
|
|
123
125
|
private toolOutputReferences?: t.ToolOutputReferencesConfig;
|
|
124
126
|
private eagerEventToolExecution?: t.EagerEventToolExecutionConfig;
|
|
125
127
|
private toolExecution?: t.ToolExecutionConfig;
|
|
@@ -166,6 +168,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
166
168
|
this.handlerRegistry = handlerRegistry;
|
|
167
169
|
this.hookRegistry = config.hooks;
|
|
168
170
|
this.humanInTheLoop = config.humanInTheLoop;
|
|
171
|
+
this.langfuse = config.langfuse;
|
|
169
172
|
this.toolOutputReferences = config.toolOutputReferences;
|
|
170
173
|
this.eagerEventToolExecution = config.eagerEventToolExecution;
|
|
171
174
|
this.toolExecution = config.toolExecution;
|
|
@@ -238,6 +241,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
238
241
|
signal,
|
|
239
242
|
runId: this.id,
|
|
240
243
|
agents: [agentConfig],
|
|
244
|
+
langfuse: this.langfuse,
|
|
241
245
|
tokenCounter: this.tokenCounter,
|
|
242
246
|
indexTokenCountMap: this.indexTokenCountMap,
|
|
243
247
|
calibrationRatio: this.calibrationRatio,
|
|
@@ -264,6 +268,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
264
268
|
runId: this.id,
|
|
265
269
|
agents,
|
|
266
270
|
edges,
|
|
271
|
+
langfuse: this.langfuse,
|
|
267
272
|
tokenCounter: this.tokenCounter,
|
|
268
273
|
indexTokenCountMap: this.indexTokenCountMap,
|
|
269
274
|
calibrationRatio: this.calibrationRatio,
|
|
@@ -546,6 +551,97 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
546
551
|
};
|
|
547
552
|
}
|
|
548
553
|
|
|
554
|
+
private shouldClearHookSession(streamThrew: boolean): boolean {
|
|
555
|
+
return (
|
|
556
|
+
this._interrupt == null || this._haltedReason != null || streamThrew
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private isAwaitingResume(streamThrew: boolean): boolean {
|
|
561
|
+
return (
|
|
562
|
+
this._interrupt != null && this._haltedReason == null && !streamThrew
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private getStreamLangfuseConfig(
|
|
567
|
+
graph: StandardGraph | MultiAgentGraph
|
|
568
|
+
): t.LangfuseConfig | undefined {
|
|
569
|
+
const primaryContext = graph.agentContexts.get(graph.defaultAgentId);
|
|
570
|
+
if (primaryContext != null) {
|
|
571
|
+
return resolveLangfuseConfig(this.langfuse, primaryContext.langfuse);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const context of graph.agentContexts.values()) {
|
|
575
|
+
const langfuse = resolveLangfuseConfig(this.langfuse, context.langfuse);
|
|
576
|
+
if (langfuse != null) {
|
|
577
|
+
return langfuse;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return this.langfuse;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private getStreamToolOutputTracingLangfuseConfig(
|
|
585
|
+
graph: StandardGraph | MultiAgentGraph
|
|
586
|
+
): t.LangfuseConfig | undefined {
|
|
587
|
+
const toolOutputTracingConfigs = Array.from(
|
|
588
|
+
graph.agentContexts.values()
|
|
589
|
+
)
|
|
590
|
+
.map((context) => {
|
|
591
|
+
return resolveLangfuseConfig(this.langfuse, context.langfuse)
|
|
592
|
+
?.toolOutputTracing;
|
|
593
|
+
})
|
|
594
|
+
.filter((config): config is t.LangfuseToolOutputTracingConfig => {
|
|
595
|
+
return config != null;
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (toolOutputTracingConfigs.length === 0) {
|
|
599
|
+
return this.langfuse?.toolOutputTracing != null
|
|
600
|
+
? { toolOutputTracing: this.langfuse.toolOutputTracing }
|
|
601
|
+
: undefined;
|
|
602
|
+
}
|
|
603
|
+
if (toolOutputTracingConfigs.length === 1) {
|
|
604
|
+
return { toolOutputTracing: toolOutputTracingConfigs[0] };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let enabled: boolean | undefined;
|
|
608
|
+
let redactionText: string | undefined;
|
|
609
|
+
let redactedToolNameMatchMode: 'exact' | 'partial' | undefined;
|
|
610
|
+
const redactedToolNames = new Set<string>();
|
|
611
|
+
|
|
612
|
+
for (const config of toolOutputTracingConfigs) {
|
|
613
|
+
if (config.enabled === false) {
|
|
614
|
+
enabled = false;
|
|
615
|
+
} else if (enabled !== false && config.enabled != null) {
|
|
616
|
+
enabled = config.enabled;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
redactionText ??= config.redactionText;
|
|
620
|
+
if (config.redactedToolNameMatchMode === 'partial') {
|
|
621
|
+
redactedToolNameMatchMode = 'partial';
|
|
622
|
+
} else {
|
|
623
|
+
redactedToolNameMatchMode ??= config.redactedToolNameMatchMode;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
for (const toolName of config.redactedToolNames ?? []) {
|
|
627
|
+
redactedToolNames.add(toolName);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
toolOutputTracing: {
|
|
633
|
+
...(enabled != null ? { enabled } : {}),
|
|
634
|
+
...(redactedToolNames.size > 0
|
|
635
|
+
? { redactedToolNames: Array.from(redactedToolNames) }
|
|
636
|
+
: {}),
|
|
637
|
+
...(redactedToolNameMatchMode != null
|
|
638
|
+
? { redactedToolNameMatchMode }
|
|
639
|
+
: {}),
|
|
640
|
+
...(redactionText != null ? { redactionText } : {}),
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
549
645
|
async processStream(
|
|
550
646
|
inputs: t.IState | Command,
|
|
551
647
|
callerConfig: Partial<RunnableConfig> & {
|
|
@@ -564,6 +660,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
564
660
|
'Graph not initialized. Make sure to use Run.create() to instantiate the Run.'
|
|
565
661
|
);
|
|
566
662
|
}
|
|
663
|
+
const graphRunnable = this.graphRunnable;
|
|
664
|
+
const graph = this.Graph;
|
|
567
665
|
|
|
568
666
|
/**
|
|
569
667
|
* `Command` inputs (currently only `Command({ resume })`) are
|
|
@@ -596,7 +694,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
596
694
|
* boundary.
|
|
597
695
|
*/
|
|
598
696
|
if (!isResume) {
|
|
599
|
-
|
|
697
|
+
graph.resetValues(streamOptions?.keepContent);
|
|
600
698
|
}
|
|
601
699
|
this._interrupt = undefined;
|
|
602
700
|
this._haltedReason = undefined;
|
|
@@ -619,34 +717,33 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
619
717
|
streamCallbacks ? [streamCallbacks, customHandler] : [customHandler]
|
|
620
718
|
);
|
|
621
719
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
});
|
|
720
|
+
const primaryContext = graph.agentContexts.get(graph.defaultAgentId);
|
|
721
|
+
const userId =
|
|
722
|
+
typeof config.configurable?.user_id === 'string'
|
|
723
|
+
? config.configurable.user_id
|
|
724
|
+
: undefined;
|
|
725
|
+
const sessionId =
|
|
726
|
+
typeof config.configurable?.thread_id === 'string'
|
|
727
|
+
? config.configurable.thread_id
|
|
728
|
+
: undefined;
|
|
729
|
+
const traceMetadata = createLangfuseTraceMetadata({
|
|
730
|
+
messageId: this.id,
|
|
731
|
+
parentMessageId: config.configurable?.requestBody?.parentMessageId,
|
|
732
|
+
agentId: graph.defaultAgentId,
|
|
733
|
+
agentName: primaryContext?.name,
|
|
734
|
+
});
|
|
735
|
+
const streamLangfuseConfig = this.getStreamLangfuseConfig(graph);
|
|
736
|
+
initializeLangfuseTracing(streamLangfuseConfig);
|
|
737
|
+
const langfuseHandler = createLangfuseHandler({
|
|
738
|
+
langfuse: streamLangfuseConfig,
|
|
739
|
+
userId,
|
|
740
|
+
sessionId,
|
|
741
|
+
traceMetadata,
|
|
742
|
+
tags: ['librechat', 'agent'],
|
|
743
|
+
});
|
|
744
|
+
if (langfuseHandler != null) {
|
|
648
745
|
config.runName = config.runName ?? getLangfuseTraceName(traceMetadata);
|
|
649
|
-
config.callbacks = appendCallbacks(config.callbacks, [
|
|
746
|
+
config.callbacks = appendCallbacks(config.callbacks, [langfuseHandler]);
|
|
650
747
|
}
|
|
651
748
|
|
|
652
749
|
if (!this.id) {
|
|
@@ -671,25 +768,6 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
671
768
|
}
|
|
672
769
|
}
|
|
673
770
|
|
|
674
|
-
/**
|
|
675
|
-
* `streamEvents` accepts both state inputs and `Command` (resume) at
|
|
676
|
-
* runtime, but our `CompiledStateWorkflow` type narrows the first
|
|
677
|
-
* arg to `BaseGraphState`. Cast on the call so the resume path
|
|
678
|
-
* type-checks without widening the wrapper for every caller.
|
|
679
|
-
*/
|
|
680
|
-
const stream = this.graphRunnable.streamEvents(inputs as t.IState, config, {
|
|
681
|
-
raiseError: true,
|
|
682
|
-
/**
|
|
683
|
-
* Prevent EventStreamCallbackHandler from processing custom events.
|
|
684
|
-
* Custom events are already handled via our createCustomEventCallback()
|
|
685
|
-
* which routes them through the handlerRegistry.
|
|
686
|
-
* Without this flag, EventStreamCallbackHandler throws errors when
|
|
687
|
-
* custom events are dispatched for run IDs not in its internal map
|
|
688
|
-
* (due to timing issues in parallel execution or after run cleanup).
|
|
689
|
-
*/
|
|
690
|
-
ignoreCustomEvent: true,
|
|
691
|
-
});
|
|
692
|
-
|
|
693
771
|
/**
|
|
694
772
|
* Tracks whether the stream loop threw. Used by the `finally`
|
|
695
773
|
* block to decide whether to honor the interrupt-preservation
|
|
@@ -701,7 +779,26 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
701
779
|
*/
|
|
702
780
|
let streamThrew = false;
|
|
703
781
|
|
|
704
|
-
|
|
782
|
+
const consumeStream = async (): Promise<void> => {
|
|
783
|
+
/**
|
|
784
|
+
* `streamEvents` accepts both state inputs and `Command` (resume) at
|
|
785
|
+
* runtime, but our `CompiledStateWorkflow` type narrows the first
|
|
786
|
+
* arg to `BaseGraphState`. Cast on the call so the resume path
|
|
787
|
+
* type-checks without widening the wrapper for every caller.
|
|
788
|
+
*/
|
|
789
|
+
const stream = graphRunnable.streamEvents(inputs as t.IState, config, {
|
|
790
|
+
raiseError: true,
|
|
791
|
+
/**
|
|
792
|
+
* Prevent EventStreamCallbackHandler from processing custom events.
|
|
793
|
+
* Custom events are already handled via our createCustomEventCallback()
|
|
794
|
+
* which routes them through the handlerRegistry.
|
|
795
|
+
* Without this flag, EventStreamCallbackHandler throws errors when
|
|
796
|
+
* custom events are dispatched for run IDs not in its internal map
|
|
797
|
+
* (due to timing issues in parallel execution or after run cleanup).
|
|
798
|
+
*/
|
|
799
|
+
ignoreCustomEvent: true,
|
|
800
|
+
});
|
|
801
|
+
|
|
705
802
|
for await (const event of stream) {
|
|
706
803
|
const { data, metadata, ...info } = event;
|
|
707
804
|
|
|
@@ -797,9 +894,8 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
797
894
|
hook_event_name: 'Stop',
|
|
798
895
|
runId: this.id,
|
|
799
896
|
threadId,
|
|
800
|
-
agentId:
|
|
801
|
-
messages:
|
|
802
|
-
this.Graph.getRunMessages() ?? stateInputs?.messages ?? [],
|
|
897
|
+
agentId: graph.defaultAgentId,
|
|
898
|
+
messages: graph.getRunMessages() ?? stateInputs?.messages ?? [],
|
|
803
899
|
stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
|
|
804
900
|
},
|
|
805
901
|
sessionId: this.id,
|
|
@@ -807,6 +903,14 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
807
903
|
/* Stop hook errors must not masquerade as stream failures */
|
|
808
904
|
});
|
|
809
905
|
}
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
await withLangfuseToolOutputTracingConfig(
|
|
910
|
+
streamLangfuseConfig,
|
|
911
|
+
consumeStream,
|
|
912
|
+
this.getStreamToolOutputTracingLangfuseConfig(graph)
|
|
913
|
+
);
|
|
810
914
|
} catch (err) {
|
|
811
915
|
streamThrew = true;
|
|
812
916
|
if (this.hookRegistry?.hasHookFor('StopFailure', this.id) === true) {
|
|
@@ -842,11 +946,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
842
946
|
* expected, sessions must drop). Every state where no resume
|
|
843
947
|
* is expected clears.
|
|
844
948
|
*/
|
|
845
|
-
if (
|
|
846
|
-
this._interrupt == null ||
|
|
847
|
-
this._haltedReason != null ||
|
|
848
|
-
streamThrew
|
|
849
|
-
) {
|
|
949
|
+
if (this.shouldClearHookSession(streamThrew)) {
|
|
850
950
|
this.hookRegistry?.clearSession(this.id);
|
|
851
951
|
}
|
|
852
952
|
/**
|
|
@@ -858,6 +958,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
858
958
|
* unaffected — their entries live under their own session ids.
|
|
859
959
|
*/
|
|
860
960
|
this.hookRegistry?.clearHaltSignal(this.id);
|
|
961
|
+
await disposeLangfuseHandler(langfuseHandler);
|
|
861
962
|
|
|
862
963
|
/**
|
|
863
964
|
* Break the reference chain that keeps heavy data alive via
|
|
@@ -915,8 +1016,7 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
915
1016
|
* Run from scratch) is a separate concern; see
|
|
916
1017
|
* `HumanInTheLoopConfig` JSDoc.
|
|
917
1018
|
*/
|
|
918
|
-
const awaitingResume =
|
|
919
|
-
this._interrupt != null && this._haltedReason == null && !streamThrew;
|
|
1019
|
+
const awaitingResume = this.isAwaitingResume(streamThrew);
|
|
920
1020
|
if (!this.skipCleanup && !awaitingResume) {
|
|
921
1021
|
this.Graph.clearHeavyState();
|
|
922
1022
|
}
|
|
@@ -1172,25 +1272,18 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1172
1272
|
typeof chainOptions.configurable?.thread_id === 'string'
|
|
1173
1273
|
? chainOptions.configurable.thread_id
|
|
1174
1274
|
: undefined;
|
|
1175
|
-
const
|
|
1176
|
-
this.
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
}
|
|
1187
|
-
titleLangfuseHandler = createLegacyLangfuseHandler({
|
|
1188
|
-
userId,
|
|
1189
|
-
sessionId,
|
|
1190
|
-
traceMetadata,
|
|
1191
|
-
tags: ['librechat', 'title'],
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1275
|
+
const titleLangfuseConfig = resolveLangfuseConfig(
|
|
1276
|
+
this.langfuse,
|
|
1277
|
+
titleContext?.langfuse
|
|
1278
|
+
);
|
|
1279
|
+
initializeLangfuseTracing(titleLangfuseConfig);
|
|
1280
|
+
titleLangfuseHandler = createLangfuseHandler({
|
|
1281
|
+
langfuse: titleLangfuseConfig,
|
|
1282
|
+
userId,
|
|
1283
|
+
sessionId,
|
|
1284
|
+
traceMetadata,
|
|
1285
|
+
tags: ['librechat', 'title'],
|
|
1286
|
+
});
|
|
1194
1287
|
|
|
1195
1288
|
if (titleLangfuseHandler != null) {
|
|
1196
1289
|
chainOptions.callbacks = appendCallbacks(chainOptions.callbacks, [
|
|
@@ -1263,9 +1356,14 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1263
1356
|
|
|
1264
1357
|
try {
|
|
1265
1358
|
try {
|
|
1266
|
-
return await
|
|
1267
|
-
|
|
1268
|
-
|
|
1359
|
+
return await withLangfuseToolOutputTracingConfig(
|
|
1360
|
+
this.langfuse,
|
|
1361
|
+
() =>
|
|
1362
|
+
fullChain.invoke(
|
|
1363
|
+
{ input: inputText, output: response },
|
|
1364
|
+
invokeConfig
|
|
1365
|
+
),
|
|
1366
|
+
titleContext?.langfuse
|
|
1269
1367
|
);
|
|
1270
1368
|
} catch (_e) {
|
|
1271
1369
|
// Fallback: strip callbacks to avoid EventStream tracer errors in certain environments
|
|
@@ -1278,9 +1376,14 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
1278
1376
|
const safeConfig = Object.assign({}, rest, {
|
|
1279
1377
|
callbacks: langfuseHandler ? [langfuseHandler] : [],
|
|
1280
1378
|
});
|
|
1281
|
-
return await
|
|
1282
|
-
|
|
1283
|
-
|
|
1379
|
+
return await withLangfuseToolOutputTracingConfig(
|
|
1380
|
+
this.langfuse,
|
|
1381
|
+
() =>
|
|
1382
|
+
fullChain.invoke(
|
|
1383
|
+
{ input: inputText, output: response },
|
|
1384
|
+
safeConfig as Partial<RunnableConfig>
|
|
1385
|
+
),
|
|
1386
|
+
titleContext?.langfuse
|
|
1284
1387
|
);
|
|
1285
1388
|
}
|
|
1286
1389
|
} finally {
|
|
@@ -6,10 +6,23 @@ import type * as t from '@/types';
|
|
|
6
6
|
|
|
7
7
|
const mockSpan = {
|
|
8
8
|
end: jest.fn(),
|
|
9
|
+
spanContext: jest.fn(() => ({
|
|
10
|
+
traceId: 'trace-id',
|
|
11
|
+
spanId: 'span-id',
|
|
12
|
+
traceFlags: 1,
|
|
13
|
+
})),
|
|
9
14
|
setAttributes: jest.fn(),
|
|
10
15
|
setStatus: jest.fn(),
|
|
11
16
|
};
|
|
12
17
|
const mockStartSpan = jest.fn(() => mockSpan);
|
|
18
|
+
const mockStartActiveSpan = jest.fn(
|
|
19
|
+
(
|
|
20
|
+
_name: string,
|
|
21
|
+
_options: unknown,
|
|
22
|
+
_context: unknown,
|
|
23
|
+
callback: (span: typeof mockSpan) => unknown
|
|
24
|
+
) => callback(mockSpan)
|
|
25
|
+
);
|
|
13
26
|
const mockForceFlush = jest.fn();
|
|
14
27
|
const mockShutdown = jest.fn();
|
|
15
28
|
|
|
@@ -22,6 +35,7 @@ jest.mock('@opentelemetry/sdk-trace-base', () => ({
|
|
|
22
35
|
BasicTracerProvider: jest.fn().mockImplementation(() => ({
|
|
23
36
|
forceFlush: mockForceFlush,
|
|
24
37
|
getTracer: jest.fn(() => ({
|
|
38
|
+
startActiveSpan: mockStartActiveSpan,
|
|
25
39
|
startSpan: mockStartSpan,
|
|
26
40
|
})),
|
|
27
41
|
shutdown: mockShutdown,
|
|
@@ -70,6 +84,169 @@ describe('Langfuse callback composition', () => {
|
|
|
70
84
|
|
|
71
85
|
await run.processStream({ messages: [new HumanMessage('hello')] }, config);
|
|
72
86
|
|
|
73
|
-
expect(
|
|
87
|
+
expect(mockStartActiveSpan).toHaveBeenCalled();
|
|
88
|
+
expect(mockForceFlush).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('attaches Langfuse callbacks for direct graph invocations', async () => {
|
|
92
|
+
const run = await Run.create<t.IState>({
|
|
93
|
+
runId: 'test-langfuse-direct-graph',
|
|
94
|
+
graphConfig: {
|
|
95
|
+
type: 'standard',
|
|
96
|
+
agents: [
|
|
97
|
+
{
|
|
98
|
+
agentId: 'agent_abc123',
|
|
99
|
+
name: 'DWAINE',
|
|
100
|
+
provider: Providers.OPENAI,
|
|
101
|
+
clientOptions: { model: 'gpt-4' },
|
|
102
|
+
tools: [],
|
|
103
|
+
langfuse: {
|
|
104
|
+
enabled: true,
|
|
105
|
+
publicKey: 'pk-test',
|
|
106
|
+
secretKey: 'sk-test',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
skipCleanup: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
run.Graph?.overrideTestModel(['hello']);
|
|
115
|
+
const workflow = run.Graph?.createWorkflow();
|
|
116
|
+
await workflow?.invoke(
|
|
117
|
+
{ messages: [new HumanMessage('hello')] },
|
|
118
|
+
{
|
|
119
|
+
callbacks: [],
|
|
120
|
+
configurable: { thread_id: 'thread-1', user_id: 'user-1' },
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(mockStartActiveSpan).toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('preserves per-agent Langfuse config when a stream callback already exists', async () => {
|
|
128
|
+
const { LangfuseSpanProcessor } = await import('@langfuse/otel');
|
|
129
|
+
const { initializeLangfuseTracing } = await import('@/instrumentation');
|
|
130
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
131
|
+
initializeLangfuseTracing({
|
|
132
|
+
publicKey: 'pk-run',
|
|
133
|
+
secretKey: 'sk-run',
|
|
134
|
+
baseUrl: 'https://langfuse.run',
|
|
135
|
+
});
|
|
136
|
+
const streamHandler = createLangfuseHandler({
|
|
137
|
+
langfuse: {
|
|
138
|
+
publicKey: 'pk-run',
|
|
139
|
+
secretKey: 'sk-run',
|
|
140
|
+
baseUrl: 'https://langfuse.run',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const run = await Run.create<t.IState>({
|
|
144
|
+
runId: 'test-langfuse-agent-callback-override',
|
|
145
|
+
graphConfig: {
|
|
146
|
+
type: 'standard',
|
|
147
|
+
agents: [
|
|
148
|
+
{
|
|
149
|
+
agentId: 'agent_abc123',
|
|
150
|
+
name: 'DWAINE',
|
|
151
|
+
provider: Providers.OPENAI,
|
|
152
|
+
clientOptions: { model: 'gpt-4' },
|
|
153
|
+
tools: [],
|
|
154
|
+
langfuse: {
|
|
155
|
+
enabled: true,
|
|
156
|
+
publicKey: 'pk-agent',
|
|
157
|
+
secretKey: 'sk-agent',
|
|
158
|
+
baseUrl: 'https://langfuse.agent',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
skipCleanup: true,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
run.Graph?.overrideTestModel(['hello']);
|
|
167
|
+
const workflow = run.Graph?.createWorkflow();
|
|
168
|
+
await workflow?.invoke(
|
|
169
|
+
{ messages: [new HumanMessage('hello')] },
|
|
170
|
+
{
|
|
171
|
+
callbacks: streamHandler != null ? [streamHandler] : [],
|
|
172
|
+
configurable: { thread_id: 'thread-1', user_id: 'user-1' },
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(LangfuseSpanProcessor).toHaveBeenCalledWith(
|
|
177
|
+
expect.objectContaining({
|
|
178
|
+
publicKey: 'pk-agent',
|
|
179
|
+
secretKey: 'sk-agent',
|
|
180
|
+
baseUrl: 'https://langfuse.agent',
|
|
181
|
+
})
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('adds current agent metadata when a stream Langfuse callback already exists', async () => {
|
|
186
|
+
const metadataSpy = jest.fn();
|
|
187
|
+
const { createLangfuseHandler } = await import('@/langfuse');
|
|
188
|
+
const streamHandler = createLangfuseHandler({
|
|
189
|
+
langfuse: {
|
|
190
|
+
publicKey: 'pk-run',
|
|
191
|
+
secretKey: 'sk-run',
|
|
192
|
+
baseUrl: 'https://langfuse.run',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
const run = await Run.create<t.IState>({
|
|
196
|
+
runId: 'test-langfuse-agent-metadata-with-stream-callback',
|
|
197
|
+
graphConfig: {
|
|
198
|
+
type: 'multi-agent',
|
|
199
|
+
agents: [
|
|
200
|
+
{
|
|
201
|
+
agentId: 'agent_default',
|
|
202
|
+
name: 'Default Agent',
|
|
203
|
+
provider: Providers.OPENAI,
|
|
204
|
+
clientOptions: { model: 'gpt-4' },
|
|
205
|
+
tools: [],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
agentId: 'agent_specialist',
|
|
209
|
+
name: 'Specialist Agent',
|
|
210
|
+
provider: Providers.OPENAI,
|
|
211
|
+
clientOptions: { model: 'gpt-4' },
|
|
212
|
+
tools: [],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
edges: [],
|
|
216
|
+
},
|
|
217
|
+
skipCleanup: true,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
run.Graph?.overrideTestModel(['hello from specialist']);
|
|
221
|
+
const agentNode = run.Graph?.createAgentNode('agent_specialist');
|
|
222
|
+
await agentNode?.invoke(
|
|
223
|
+
{ messages: [new HumanMessage('hello')] },
|
|
224
|
+
{
|
|
225
|
+
callbacks: [
|
|
226
|
+
...(streamHandler != null ? [streamHandler] : []),
|
|
227
|
+
{
|
|
228
|
+
handleChatModelStart: async (
|
|
229
|
+
_llm: unknown,
|
|
230
|
+
_messages: unknown,
|
|
231
|
+
_runId: string,
|
|
232
|
+
_parentRunId?: string,
|
|
233
|
+
_extraParams?: unknown,
|
|
234
|
+
_tags?: string[],
|
|
235
|
+
metadata?: Record<string, unknown>
|
|
236
|
+
): Promise<void> => {
|
|
237
|
+
metadataSpy(metadata);
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
configurable: { thread_id: 'thread-1', user_id: 'user-1' },
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(metadataSpy).toHaveBeenCalledWith(
|
|
246
|
+
expect.objectContaining({
|
|
247
|
+
agentId: 'agent_specialist',
|
|
248
|
+
agentName: 'Specialist Agent',
|
|
249
|
+
})
|
|
250
|
+
);
|
|
74
251
|
});
|
|
75
252
|
});
|