@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
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { context, createContextKey } from '@opentelemetry/api';
|
|
2
|
+
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
3
|
+
import { LangfuseOtelSpanAttributes } from '@langfuse/tracing';
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
+
import type {
|
|
6
|
+
ReadableSpan,
|
|
7
|
+
Span,
|
|
8
|
+
SpanProcessor,
|
|
9
|
+
} from '@opentelemetry/sdk-trace-base';
|
|
10
|
+
import type { LangfuseSpanProcessorParams } from '@langfuse/otel';
|
|
11
|
+
import type { Context } from '@opentelemetry/api';
|
|
12
|
+
import type * as t from '@/types';
|
|
13
|
+
|
|
14
|
+
export const LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT = '[tool output redacted]';
|
|
15
|
+
|
|
16
|
+
const langfuseToolOutputTracingConfigKey = createContextKey(
|
|
17
|
+
'librechat.langfuse.tool-output-tracing'
|
|
18
|
+
);
|
|
19
|
+
const langfuseConfigKey = createContextKey('librechat.langfuse.config');
|
|
20
|
+
const toolOutputTracingStorage =
|
|
21
|
+
new AsyncLocalStorage<ResolvedLangfuseToolOutputTracingConfig>();
|
|
22
|
+
const langfuseConfigStorage = new AsyncLocalStorage<t.LangfuseConfig>();
|
|
23
|
+
|
|
24
|
+
const CHAT_ROLES = new Set([
|
|
25
|
+
'assistant',
|
|
26
|
+
'developer',
|
|
27
|
+
'human',
|
|
28
|
+
'system',
|
|
29
|
+
'user',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export type ResolvedLangfuseToolOutputTracingConfig = {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
redactedToolNames: Set<string>;
|
|
35
|
+
redactedToolNameMatchMode: 'exact' | 'partial';
|
|
36
|
+
redactionText: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type SpanWithAttributes = ReadableSpan & {
|
|
40
|
+
attributes: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type RedactionResult = {
|
|
44
|
+
value: unknown;
|
|
45
|
+
changed: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type RedactionContext = {
|
|
49
|
+
toolNamesByCallId: Map<string, string>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const TOOL_OUTPUT_FIELD_KEYS = ['content', 'artifact'];
|
|
53
|
+
|
|
54
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
55
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isPresent(value: unknown): value is string {
|
|
59
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseBoolean(value: string | undefined): boolean | undefined {
|
|
63
|
+
if (value == null) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const normalized = value.trim().toLowerCase();
|
|
68
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeToolName(name: string): string {
|
|
79
|
+
return name.trim().toLowerCase();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeToolNames(names: string[] | undefined): Set<string> {
|
|
83
|
+
const normalized = new Set<string>();
|
|
84
|
+
for (const name of names ?? []) {
|
|
85
|
+
if (isPresent(name)) {
|
|
86
|
+
normalized.add(normalizeToolName(name));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseToolNames(value: string | undefined): string[] | undefined {
|
|
93
|
+
if (!isPresent(value)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value
|
|
98
|
+
.split(',')
|
|
99
|
+
.map((name) => name.trim())
|
|
100
|
+
.filter((name) => name !== '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getEnvToolOutputTracingEnabled(): boolean | undefined {
|
|
104
|
+
const traceToolOutputs = parseBoolean(
|
|
105
|
+
process.env.LANGFUSE_TRACE_TOOL_OUTPUTS
|
|
106
|
+
);
|
|
107
|
+
if (traceToolOutputs != null) {
|
|
108
|
+
return traceToolOutputs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const redactToolOutputs = parseBoolean(
|
|
112
|
+
process.env.LANGFUSE_REDACT_TOOL_OUTPUTS
|
|
113
|
+
);
|
|
114
|
+
if (redactToolOutputs != null) {
|
|
115
|
+
return !redactToolOutputs;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parseBoolean(process.env.LANGFUSE_TOOL_OUTPUT_TRACING_ENABLED);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getEnvRedactedToolNames(): string[] | undefined {
|
|
122
|
+
return (
|
|
123
|
+
parseToolNames(process.env.LANGFUSE_REDACT_TOOL_OUTPUT_NAMES) ??
|
|
124
|
+
parseToolNames(process.env.LANGFUSE_REDACT_TOOL_NAMES)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getEnvRedactionText(): string | undefined {
|
|
129
|
+
return isPresent(process.env.LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT)
|
|
130
|
+
? process.env.LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT
|
|
131
|
+
: undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getEnvToolNameMatchMode(): 'exact' | 'partial' | undefined {
|
|
135
|
+
const mode = (
|
|
136
|
+
process.env.LANGFUSE_REDACT_TOOL_OUTPUT_NAME_MATCH_MODE ??
|
|
137
|
+
process.env.LANGFUSE_REDACT_TOOL_NAME_MATCH_MODE
|
|
138
|
+
)
|
|
139
|
+
?.trim()
|
|
140
|
+
.toLowerCase();
|
|
141
|
+
if (mode === 'exact' || mode === 'partial') {
|
|
142
|
+
return mode;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveToolOutputTracingConfig(
|
|
148
|
+
runLangfuse?: t.LangfuseConfig,
|
|
149
|
+
agentLangfuse?: t.LangfuseConfig
|
|
150
|
+
): ResolvedLangfuseToolOutputTracingConfig {
|
|
151
|
+
const runConfig = runLangfuse?.toolOutputTracing;
|
|
152
|
+
const agentConfig = agentLangfuse?.toolOutputTracing;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
enabled:
|
|
156
|
+
agentConfig?.enabled ??
|
|
157
|
+
runConfig?.enabled ??
|
|
158
|
+
getEnvToolOutputTracingEnabled() ??
|
|
159
|
+
true,
|
|
160
|
+
redactedToolNames: normalizeToolNames(
|
|
161
|
+
agentConfig?.redactedToolNames ??
|
|
162
|
+
runConfig?.redactedToolNames ??
|
|
163
|
+
getEnvRedactedToolNames()
|
|
164
|
+
),
|
|
165
|
+
redactedToolNameMatchMode:
|
|
166
|
+
agentConfig?.redactedToolNameMatchMode ??
|
|
167
|
+
runConfig?.redactedToolNameMatchMode ??
|
|
168
|
+
getEnvToolNameMatchMode() ??
|
|
169
|
+
'exact',
|
|
170
|
+
redactionText:
|
|
171
|
+
agentConfig?.redactionText ??
|
|
172
|
+
runConfig?.redactionText ??
|
|
173
|
+
getEnvRedactionText() ??
|
|
174
|
+
LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function shouldApplyToolOutputRedaction(
|
|
179
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
180
|
+
): boolean {
|
|
181
|
+
return config.enabled === false || config.redactedToolNames.size > 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toolNameMatches(
|
|
185
|
+
toolName: string | undefined,
|
|
186
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
187
|
+
): boolean {
|
|
188
|
+
if (!isPresent(toolName)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const normalizedToolName = normalizeToolName(toolName);
|
|
193
|
+
if (config.redactedToolNameMatchMode === 'partial') {
|
|
194
|
+
for (const redactedToolName of config.redactedToolNames) {
|
|
195
|
+
if (normalizedToolName.includes(redactedToolName)) {
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return config.redactedToolNames.has(normalizedToolName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function shouldRedactTool(
|
|
206
|
+
toolName: string | undefined,
|
|
207
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
208
|
+
): boolean {
|
|
209
|
+
return config.enabled === false || toolNameMatches(toolName, config);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getStringField(
|
|
213
|
+
value: Record<string, unknown>,
|
|
214
|
+
key: string
|
|
215
|
+
): string | undefined {
|
|
216
|
+
const field = value[key];
|
|
217
|
+
return typeof field === 'string' ? field : undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getNestedStringField(
|
|
221
|
+
value: Record<string, unknown>,
|
|
222
|
+
objectKey: string,
|
|
223
|
+
fieldKey: string
|
|
224
|
+
): string | undefined {
|
|
225
|
+
const nested = value[objectKey];
|
|
226
|
+
if (!isRecord(nested)) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
return getStringField(nested, fieldKey);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getSerializedToolCallId(
|
|
233
|
+
value: Record<string, unknown>
|
|
234
|
+
): string | undefined {
|
|
235
|
+
return (
|
|
236
|
+
getStringField(value, 'tool_call_id') ??
|
|
237
|
+
getNestedStringField(value, 'kwargs', 'tool_call_id') ??
|
|
238
|
+
getNestedStringField(value, 'additional_kwargs', 'tool_call_id') ??
|
|
239
|
+
getNestedStringField(value, 'data', 'tool_call_id') ??
|
|
240
|
+
(typeof value.id === 'string' ? value.id : undefined)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getSerializedToolName(
|
|
245
|
+
value: Record<string, unknown>,
|
|
246
|
+
redactionContext?: RedactionContext
|
|
247
|
+
): string | undefined {
|
|
248
|
+
const role = getStringField(value, 'role');
|
|
249
|
+
const explicitName =
|
|
250
|
+
getStringField(value, 'name') ??
|
|
251
|
+
getStringField(value, 'tool_name') ??
|
|
252
|
+
getNestedStringField(value, 'function', 'name') ??
|
|
253
|
+
getNestedStringField(value, 'kwargs', 'name') ??
|
|
254
|
+
getNestedStringField(value, 'additional_kwargs', 'name') ??
|
|
255
|
+
getNestedStringField(value, 'data', 'name') ??
|
|
256
|
+
(role != null && role.toLowerCase() !== 'tool' ? role : undefined);
|
|
257
|
+
|
|
258
|
+
if (explicitName != null) {
|
|
259
|
+
return explicitName;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const toolCallId = getSerializedToolCallId(value);
|
|
263
|
+
return toolCallId != null
|
|
264
|
+
? redactionContext?.toolNamesByCallId.get(toolCallId)
|
|
265
|
+
: undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function hasToolMessageIdentity(value: Record<string, unknown>): boolean {
|
|
269
|
+
const type = getStringField(value, 'type') ?? getStringField(value, '_type');
|
|
270
|
+
if (type === 'tool' || type === 'tool_message') {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const id = value.id;
|
|
275
|
+
if (
|
|
276
|
+
Array.isArray(id) &&
|
|
277
|
+
id.some((part) => typeof part === 'string' && part.includes('ToolMessage'))
|
|
278
|
+
) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (
|
|
283
|
+
'tool_call_id' in value ||
|
|
284
|
+
getNestedStringField(value, 'kwargs', 'tool_call_id') != null ||
|
|
285
|
+
getNestedStringField(value, 'additional_kwargs', 'tool_call_id') != null
|
|
286
|
+
) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const role = getStringField(value, 'role');
|
|
291
|
+
return (
|
|
292
|
+
role != null &&
|
|
293
|
+
!CHAT_ROLES.has(role.toLowerCase()) &&
|
|
294
|
+
('content' in value || isRecord(value.kwargs) || isRecord(value.data))
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function redactToolContentFields(
|
|
299
|
+
value: Record<string, unknown>,
|
|
300
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
301
|
+
): Record<string, unknown> {
|
|
302
|
+
const next = { ...value };
|
|
303
|
+
|
|
304
|
+
for (const outputKey of TOOL_OUTPUT_FIELD_KEYS) {
|
|
305
|
+
if (outputKey in next) {
|
|
306
|
+
next[outputKey] = config.redactionText;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const nestedKey of ['kwargs', 'data', 'additional_kwargs']) {
|
|
311
|
+
const nested = next[nestedKey];
|
|
312
|
+
if (!isRecord(nested)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const nextNested = { ...nested };
|
|
316
|
+
let changed = false;
|
|
317
|
+
for (const outputKey of TOOL_OUTPUT_FIELD_KEYS) {
|
|
318
|
+
if (outputKey in nextNested) {
|
|
319
|
+
nextNested[outputKey] = config.redactionText;
|
|
320
|
+
changed = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (changed) {
|
|
324
|
+
next[nestedKey] = nextNested;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return next;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function collectToolCallNames(
|
|
332
|
+
value: unknown,
|
|
333
|
+
redactionContext: RedactionContext
|
|
334
|
+
): void {
|
|
335
|
+
if (Array.isArray(value)) {
|
|
336
|
+
for (const item of value) {
|
|
337
|
+
collectToolCallNames(item, redactionContext);
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!isRecord(value)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const toolCallId = getSerializedToolCallId(value);
|
|
347
|
+
const toolName = getSerializedToolName(value);
|
|
348
|
+
if (toolCallId != null && toolName != null) {
|
|
349
|
+
redactionContext.toolNamesByCallId.set(toolCallId, toolName);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const child of Object.values(value)) {
|
|
353
|
+
collectToolCallNames(child, redactionContext);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function redactValue(
|
|
358
|
+
value: unknown,
|
|
359
|
+
config: ResolvedLangfuseToolOutputTracingConfig,
|
|
360
|
+
redactionContext: RedactionContext
|
|
361
|
+
): RedactionResult {
|
|
362
|
+
if (Array.isArray(value)) {
|
|
363
|
+
let changed = false;
|
|
364
|
+
const next: unknown[] = [];
|
|
365
|
+
for (const item of value) {
|
|
366
|
+
const result = redactValue(item, config, redactionContext);
|
|
367
|
+
if (result.changed) {
|
|
368
|
+
changed = true;
|
|
369
|
+
}
|
|
370
|
+
next.push(result.value);
|
|
371
|
+
}
|
|
372
|
+
return changed ? { value: next, changed } : { value, changed };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!isRecord(value)) {
|
|
376
|
+
return { value, changed: false };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const toolName = getSerializedToolName(value, redactionContext);
|
|
380
|
+
if (hasToolMessageIdentity(value) && shouldRedactTool(toolName, config)) {
|
|
381
|
+
return {
|
|
382
|
+
value: redactToolContentFields(value, config),
|
|
383
|
+
changed: true,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let changed = false;
|
|
388
|
+
const next: Record<string, unknown> = {};
|
|
389
|
+
for (const [key, child] of Object.entries(value)) {
|
|
390
|
+
const result = redactValue(child, config, redactionContext);
|
|
391
|
+
if (result.changed) {
|
|
392
|
+
changed = true;
|
|
393
|
+
}
|
|
394
|
+
next[key] = result.value;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return changed ? { value: next, changed } : { value, changed };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function redactSerializedValue(
|
|
401
|
+
value: unknown,
|
|
402
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
403
|
+
): RedactionResult {
|
|
404
|
+
const redactionContext: RedactionContext = {
|
|
405
|
+
toolNamesByCallId: new Map(),
|
|
406
|
+
};
|
|
407
|
+
if (typeof value !== 'string') {
|
|
408
|
+
collectToolCallNames(value, redactionContext);
|
|
409
|
+
return redactValue(value, config, redactionContext);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const trimmed = value.trim();
|
|
413
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
414
|
+
return { value, changed: false };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(value) as unknown;
|
|
419
|
+
collectToolCallNames(parsed, redactionContext);
|
|
420
|
+
const result = redactValue(parsed, config, redactionContext);
|
|
421
|
+
return result.changed
|
|
422
|
+
? { value: JSON.stringify(result.value), changed: true }
|
|
423
|
+
: { value, changed: false };
|
|
424
|
+
} catch {
|
|
425
|
+
return { value, changed: false };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function redactAttribute(
|
|
430
|
+
attributes: Record<string, unknown>,
|
|
431
|
+
key: string,
|
|
432
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
433
|
+
): void {
|
|
434
|
+
if (!(key in attributes)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const result = redactSerializedValue(attributes[key], config);
|
|
439
|
+
if (result.changed) {
|
|
440
|
+
attributes[key] = result.value;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function isToolObservation(attributes: Record<string, unknown>): boolean {
|
|
445
|
+
const type = attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
|
|
446
|
+
return typeof type === 'string' && type.toLowerCase() === 'tool';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function redactToolObservationOutput(
|
|
450
|
+
span: ReadableSpan,
|
|
451
|
+
attributes: Record<string, unknown>,
|
|
452
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
453
|
+
): void {
|
|
454
|
+
if (
|
|
455
|
+
!(
|
|
456
|
+
isToolObservation(attributes) &&
|
|
457
|
+
shouldRedactTool(span.name, config) &&
|
|
458
|
+
LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT in attributes
|
|
459
|
+
)
|
|
460
|
+
) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] =
|
|
465
|
+
config.redactionText;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function redactLangfuseSpanToolOutputs(
|
|
469
|
+
span: ReadableSpan,
|
|
470
|
+
config: ResolvedLangfuseToolOutputTracingConfig
|
|
471
|
+
): void {
|
|
472
|
+
if (!shouldApplyToolOutputRedaction(config)) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const attributes = (span as SpanWithAttributes).attributes;
|
|
477
|
+
redactToolObservationOutput(span, attributes, config);
|
|
478
|
+
|
|
479
|
+
for (const key of [
|
|
480
|
+
LangfuseOtelSpanAttributes.OBSERVATION_INPUT,
|
|
481
|
+
LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT,
|
|
482
|
+
LangfuseOtelSpanAttributes.TRACE_INPUT,
|
|
483
|
+
LangfuseOtelSpanAttributes.TRACE_OUTPUT,
|
|
484
|
+
]) {
|
|
485
|
+
redactAttribute(attributes, key, config);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function getContextToolOutputTracingConfig(
|
|
490
|
+
activeContext: Context
|
|
491
|
+
): ResolvedLangfuseToolOutputTracingConfig | undefined {
|
|
492
|
+
const asyncConfig = toolOutputTracingStorage.getStore();
|
|
493
|
+
if (asyncConfig != null) {
|
|
494
|
+
return asyncConfig;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const value = activeContext.getValue(langfuseToolOutputTracingConfigKey);
|
|
498
|
+
return isRecord(value)
|
|
499
|
+
? (value as ResolvedLangfuseToolOutputTracingConfig)
|
|
500
|
+
: undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function getContextLangfuseConfig(
|
|
504
|
+
activeContext: Context
|
|
505
|
+
): t.LangfuseConfig | undefined {
|
|
506
|
+
const asyncConfig = langfuseConfigStorage.getStore();
|
|
507
|
+
if (asyncConfig != null) {
|
|
508
|
+
return asyncConfig;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const value = activeContext.getValue(langfuseConfigKey);
|
|
512
|
+
return isRecord(value) ? (value as t.LangfuseConfig) : undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
|
|
516
|
+
private readonly processor: LangfuseSpanProcessor;
|
|
517
|
+
private readonly fallbackConfig?: ResolvedLangfuseToolOutputTracingConfig;
|
|
518
|
+
private readonly spanConfigs = new WeakMap<
|
|
519
|
+
object,
|
|
520
|
+
ResolvedLangfuseToolOutputTracingConfig
|
|
521
|
+
>();
|
|
522
|
+
|
|
523
|
+
constructor(
|
|
524
|
+
params?: LangfuseSpanProcessorParams,
|
|
525
|
+
fallbackConfig?: ResolvedLangfuseToolOutputTracingConfig
|
|
526
|
+
) {
|
|
527
|
+
this.processor = new LangfuseSpanProcessor(params);
|
|
528
|
+
this.fallbackConfig = fallbackConfig;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
onStart(span: Span, parentContext: Context): void {
|
|
532
|
+
const config =
|
|
533
|
+
getContextToolOutputTracingConfig(parentContext) ?? this.fallbackConfig;
|
|
534
|
+
if (config != null) {
|
|
535
|
+
this.spanConfigs.set(span, config);
|
|
536
|
+
}
|
|
537
|
+
this.processor.onStart(span, parentContext);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
onEnd(span: ReadableSpan): void {
|
|
541
|
+
const config =
|
|
542
|
+
this.spanConfigs.get(span) ??
|
|
543
|
+
toolOutputTracingStorage.getStore() ??
|
|
544
|
+
this.fallbackConfig ??
|
|
545
|
+
resolveToolOutputTracingConfig();
|
|
546
|
+
redactLangfuseSpanToolOutputs(span, config);
|
|
547
|
+
this.processor.onEnd(span);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
forceFlush(): Promise<void> {
|
|
551
|
+
return this.processor.forceFlush();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
shutdown(): Promise<void> {
|
|
555
|
+
return this.processor.shutdown();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function createLangfuseSpanProcessor(
|
|
560
|
+
params?: LangfuseSpanProcessorParams,
|
|
561
|
+
runLangfuse?: t.LangfuseConfig,
|
|
562
|
+
agentLangfuse?: t.LangfuseConfig
|
|
563
|
+
): SpanProcessor {
|
|
564
|
+
const fallbackConfig =
|
|
565
|
+
runLangfuse != null || agentLangfuse != null
|
|
566
|
+
? resolveToolOutputTracingConfig(runLangfuse, agentLangfuse)
|
|
567
|
+
: undefined;
|
|
568
|
+
return new ToolOutputRedactingLangfuseSpanProcessor(params, fallbackConfig);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function withLangfuseToolOutputTracingConfig<T>(
|
|
572
|
+
runLangfuse: t.LangfuseConfig | undefined,
|
|
573
|
+
action: () => T,
|
|
574
|
+
agentLangfuse?: t.LangfuseConfig
|
|
575
|
+
): T {
|
|
576
|
+
const langfuse = resolveLangfuseConfig(runLangfuse, agentLangfuse);
|
|
577
|
+
const hasNoToolOutputConfig =
|
|
578
|
+
runLangfuse?.toolOutputTracing == null &&
|
|
579
|
+
agentLangfuse?.toolOutputTracing == null;
|
|
580
|
+
|
|
581
|
+
if (langfuse == null && hasNoToolOutputConfig) {
|
|
582
|
+
return action();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const config = hasNoToolOutputConfig
|
|
586
|
+
? undefined
|
|
587
|
+
: resolveToolOutputTracingConfig(runLangfuse, agentLangfuse);
|
|
588
|
+
let activeContext = context.active();
|
|
589
|
+
if (langfuse != null) {
|
|
590
|
+
activeContext = activeContext.setValue(langfuseConfigKey, langfuse);
|
|
591
|
+
}
|
|
592
|
+
if (config != null) {
|
|
593
|
+
activeContext = activeContext.setValue(
|
|
594
|
+
langfuseToolOutputTracingConfigKey,
|
|
595
|
+
config
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const runWithContext = (): T => context.with(activeContext, action);
|
|
600
|
+
const runWithToolOutputConfig = (): T =>
|
|
601
|
+
config != null
|
|
602
|
+
? toolOutputTracingStorage.run(config, runWithContext)
|
|
603
|
+
: runWithContext();
|
|
604
|
+
|
|
605
|
+
return langfuse != null
|
|
606
|
+
? langfuseConfigStorage.run(langfuse, runWithToolOutputConfig)
|
|
607
|
+
: runWithToolOutputConfig();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function hasLangfuseEnvKeys(): boolean {
|
|
611
|
+
return (
|
|
612
|
+
isPresent(process.env.LANGFUSE_SECRET_KEY) &&
|
|
613
|
+
isPresent(process.env.LANGFUSE_PUBLIC_KEY)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function hasLangfuseConfigKeys(langfuse?: t.LangfuseConfig): boolean {
|
|
618
|
+
if (langfuse == null) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
return (
|
|
622
|
+
isPresent(langfuse.secretKey) &&
|
|
623
|
+
isPresent(langfuse.publicKey)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function shouldTraceToolNodeForLangfuse({
|
|
628
|
+
runLangfuse,
|
|
629
|
+
agentLangfuse,
|
|
630
|
+
}: {
|
|
631
|
+
runLangfuse?: t.LangfuseConfig;
|
|
632
|
+
agentLangfuse?: t.LangfuseConfig;
|
|
633
|
+
}): boolean {
|
|
634
|
+
const langfuse = resolveLangfuseConfig(runLangfuse, agentLangfuse);
|
|
635
|
+
if (langfuse?.enabled === false) {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const explicit = langfuse?.toolNodeTracing?.enabled;
|
|
640
|
+
if (explicit != null) {
|
|
641
|
+
return (
|
|
642
|
+
explicit &&
|
|
643
|
+
(hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys())
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export function resolveLangfuseConfig(
|
|
651
|
+
runLangfuse?: t.LangfuseConfig,
|
|
652
|
+
agentLangfuse?: t.LangfuseConfig
|
|
653
|
+
): t.LangfuseConfig | undefined {
|
|
654
|
+
if (runLangfuse == null) {
|
|
655
|
+
return agentLangfuse;
|
|
656
|
+
}
|
|
657
|
+
if (agentLangfuse == null) {
|
|
658
|
+
return runLangfuse;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const toolNodeTracing =
|
|
662
|
+
runLangfuse.toolNodeTracing != null || agentLangfuse.toolNodeTracing != null
|
|
663
|
+
? {
|
|
664
|
+
...runLangfuse.toolNodeTracing,
|
|
665
|
+
...agentLangfuse.toolNodeTracing,
|
|
666
|
+
}
|
|
667
|
+
: undefined;
|
|
668
|
+
const toolOutputTracing =
|
|
669
|
+
runLangfuse.toolOutputTracing != null ||
|
|
670
|
+
agentLangfuse.toolOutputTracing != null
|
|
671
|
+
? {
|
|
672
|
+
...runLangfuse.toolOutputTracing,
|
|
673
|
+
...agentLangfuse.toolOutputTracing,
|
|
674
|
+
}
|
|
675
|
+
: undefined;
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
...runLangfuse,
|
|
679
|
+
...agentLangfuse,
|
|
680
|
+
...(toolNodeTracing != null ? { toolNodeTracing } : {}),
|
|
681
|
+
...(toolOutputTracing != null ? { toolOutputTracing } : {}),
|
|
682
|
+
};
|
|
683
|
+
}
|