pi-langfuse 1.4.3 → 1.4.5
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/README.md +104 -248
- package/README_CN.md +108 -248
- package/image.png +0 -0
- package/index.ts +20 -3
- package/package.json +2 -1
- package/src/capture-policy.ts +141 -0
- package/src/commands.ts +261 -0
- package/src/config.ts +26 -16
- package/src/handlers/agent.ts +59 -29
- package/src/handlers/generation.ts +52 -53
- package/src/handlers/tool.ts +37 -24
- package/src/handlers/turn.ts +21 -19
- package/src/langfuse.ts +58 -14
- package/src/observation.ts +21 -0
- package/src/redaction.ts +115 -0
- package/src/state.ts +16 -2
- package/src/types.ts +3 -0
- package/src/utils.ts +20 -2
package/src/commands.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { CONFIG_PATH } from "./constants.js";
|
|
4
|
+
import { loadConfig, saveConfig, ensureConfig } from "./config.js";
|
|
5
|
+
import { createCapturePolicy, type PrivacyPreset, type CapturePolicy } from "./capture-policy.js";
|
|
6
|
+
import { getRuntime, forceShutdownRuntime as shutdownLangfuseRuntime } from "./langfuse.js";
|
|
7
|
+
import { state } from "./state.js";
|
|
8
|
+
import type { LangfuseRuntime } from "./types.js";
|
|
9
|
+
|
|
10
|
+
const PRIVACY_PRESETS = ["metadata-only", "prompts-only", "conversations", "full-debug"] as const;
|
|
11
|
+
|
|
12
|
+
export interface CommandContextLike {
|
|
13
|
+
hasUI?: boolean;
|
|
14
|
+
ui?: {
|
|
15
|
+
notify?: (message: string, level?: "info" | "warning" | "error") => void;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface CommandDeps {
|
|
20
|
+
configPath?: string;
|
|
21
|
+
getRuntime?: () => Promise<LangfuseRuntime>;
|
|
22
|
+
forceShutdownRuntime?: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function notify(ctx: CommandContextLike, message: string, level: "info" | "warning" | "error" = "info") {
|
|
26
|
+
if (ctx.hasUI && ctx.ui?.notify) {
|
|
27
|
+
ctx.ui.notify(message, level);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const prefix = level === "error" ? "❌" : level === "warning" ? "⚠️" : "📊";
|
|
31
|
+
console.log(`${prefix} Langfuse: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseCommandArgs(args: string): { values: Record<string, string>; positional: string[]; malformed: string[] } {
|
|
35
|
+
const values: Record<string, string> = {};
|
|
36
|
+
const positional: string[] = [];
|
|
37
|
+
const malformed: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const part of args.trim().split(/\s+/)) {
|
|
40
|
+
if (!part) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const eq = part.indexOf("=");
|
|
44
|
+
if (eq === -1) {
|
|
45
|
+
positional.push(part);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (eq === 0) {
|
|
49
|
+
malformed.push(part);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
values[part.slice(0, eq)] = part.slice(eq + 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { values, positional, malformed };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isPrivacyPreset(value: string | undefined): value is PrivacyPreset {
|
|
59
|
+
return PRIVACY_PRESETS.includes(value as PrivacyPreset);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function inferPreset(policy: CapturePolicy): PrivacyPreset | "custom" {
|
|
63
|
+
const entries: Array<[PrivacyPreset, CapturePolicy]> = [
|
|
64
|
+
[
|
|
65
|
+
"metadata-only",
|
|
66
|
+
{
|
|
67
|
+
captureInputs: false,
|
|
68
|
+
captureOutputs: false,
|
|
69
|
+
captureToolIo: false,
|
|
70
|
+
captureSystemPrompt: false,
|
|
71
|
+
captureCwd: false,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
"prompts-only",
|
|
76
|
+
{
|
|
77
|
+
captureInputs: true,
|
|
78
|
+
captureOutputs: false,
|
|
79
|
+
captureToolIo: false,
|
|
80
|
+
captureSystemPrompt: false,
|
|
81
|
+
captureCwd: false,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
"conversations",
|
|
86
|
+
{
|
|
87
|
+
captureInputs: true,
|
|
88
|
+
captureOutputs: true,
|
|
89
|
+
captureToolIo: false,
|
|
90
|
+
captureSystemPrompt: false,
|
|
91
|
+
captureCwd: false,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"full-debug",
|
|
96
|
+
{
|
|
97
|
+
captureInputs: true,
|
|
98
|
+
captureOutputs: true,
|
|
99
|
+
captureToolIo: true,
|
|
100
|
+
captureSystemPrompt: true,
|
|
101
|
+
captureCwd: true,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
for (const [preset, presetPolicy] of entries) {
|
|
107
|
+
if (
|
|
108
|
+
policy.captureInputs === presetPolicy.captureInputs &&
|
|
109
|
+
policy.captureOutputs === presetPolicy.captureOutputs &&
|
|
110
|
+
policy.captureToolIo === presetPolicy.captureToolIo &&
|
|
111
|
+
policy.captureSystemPrompt === presetPolicy.captureSystemPrompt &&
|
|
112
|
+
policy.captureCwd === presetPolicy.captureCwd
|
|
113
|
+
) {
|
|
114
|
+
return preset;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return "custom";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function describePolicy(policy: CapturePolicy) {
|
|
121
|
+
return [
|
|
122
|
+
`captureInputs: ${policy.captureInputs}`,
|
|
123
|
+
`captureOutputs: ${policy.captureOutputs}`,
|
|
124
|
+
`captureToolIo: ${policy.captureToolIo}`,
|
|
125
|
+
`captureSystemPrompt: ${policy.captureSystemPrompt}`,
|
|
126
|
+
`captureCwd: ${policy.captureCwd}`,
|
|
127
|
+
].join("\n");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function readPersistedConfig(path: string) {
|
|
131
|
+
if (!existsSync(path)) {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
|
|
136
|
+
} catch {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function hasActiveAgentObservation() {
|
|
142
|
+
for (const sessionState of state.sessionStates.values()) {
|
|
143
|
+
if (sessionState.agentState?.root) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function handleLangfusePrivacyCommand(
|
|
151
|
+
args: string,
|
|
152
|
+
ctx: CommandContextLike,
|
|
153
|
+
deps: CommandDeps = {},
|
|
154
|
+
): Promise<boolean> {
|
|
155
|
+
const configPath = deps.configPath ?? CONFIG_PATH;
|
|
156
|
+
const parsed = parseCommandArgs(args);
|
|
157
|
+
if (parsed.malformed.length > 0) {
|
|
158
|
+
notify(ctx, `Couldn't understand '${parsed.malformed[0]}'. Use /langfuse-privacy preset=metadata-only.`, "warning");
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const requestedPreset = parsed.values.preset ?? parsed.positional[0];
|
|
163
|
+
if (!requestedPreset) {
|
|
164
|
+
state.config = state.config ?? loadConfig(process.env, configPath);
|
|
165
|
+
const policy = state.config?.capturePolicy ?? createCapturePolicy();
|
|
166
|
+
notify(ctx, `Current Langfuse privacy preset: ${inferPreset(policy)}\n${describePolicy(policy)}`);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!isPrivacyPreset(requestedPreset)) {
|
|
171
|
+
notify(
|
|
172
|
+
ctx,
|
|
173
|
+
`Unknown privacy preset '${requestedPreset}'. Use one of: ${PRIVACY_PRESETS.join(", ")}.`,
|
|
174
|
+
"warning",
|
|
175
|
+
);
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const existing = readPersistedConfig(configPath);
|
|
180
|
+
const loaded = state.config ?? loadConfig(process.env, configPath);
|
|
181
|
+
|
|
182
|
+
const publicKey = existing.publicKey ?? loaded?.publicKey;
|
|
183
|
+
const secretKey = existing.secretKey ?? loaded?.secretKey;
|
|
184
|
+
const host = existing.host ?? loaded?.host;
|
|
185
|
+
|
|
186
|
+
if (!publicKey || !secretKey || !host) {
|
|
187
|
+
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup before changing privacy settings.", "warning");
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const nextConfig = {
|
|
192
|
+
publicKey: String(publicKey),
|
|
193
|
+
secretKey: String(secretKey),
|
|
194
|
+
host: String(host),
|
|
195
|
+
privacyPreset: requestedPreset,
|
|
196
|
+
};
|
|
197
|
+
saveConfig(nextConfig, configPath);
|
|
198
|
+
state.config = loadConfig(process.env, configPath);
|
|
199
|
+
|
|
200
|
+
notify(ctx, `Langfuse privacy preset saved: ${requestedPreset}\n${describePolicy(state.config?.capturePolicy ?? createCapturePolicy())}`);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function handleLangfuseTestCommand(
|
|
205
|
+
_args: string,
|
|
206
|
+
ctx: CommandContextLike,
|
|
207
|
+
deps: CommandDeps = {},
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
if (!state.config && !(await ensureConfig(ctx))) {
|
|
210
|
+
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup first.", "warning");
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (hasActiveAgentObservation()) {
|
|
215
|
+
notify(ctx, "Langfuse test skipped because an agent run is active. Try again after the run finishes.", "warning");
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let runtimeInitialized = false;
|
|
220
|
+
try {
|
|
221
|
+
const rt = await (deps.getRuntime ?? getRuntime)();
|
|
222
|
+
runtimeInitialized = true;
|
|
223
|
+
rt.propagateAttributes(
|
|
224
|
+
{
|
|
225
|
+
traceName: "pi-langfuse-test",
|
|
226
|
+
metadata: {
|
|
227
|
+
source: "pi-langfuse",
|
|
228
|
+
command: "langfuse-test",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
() => {
|
|
232
|
+
const observation = rt.startObservation(
|
|
233
|
+
"pi-langfuse-test",
|
|
234
|
+
{
|
|
235
|
+
input: { command: "/langfuse-test" },
|
|
236
|
+
output: "ok",
|
|
237
|
+
metadata: {
|
|
238
|
+
source: "pi-langfuse",
|
|
239
|
+
command: "langfuse-test",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{ asType: "span" },
|
|
243
|
+
);
|
|
244
|
+
observation.end();
|
|
245
|
+
return observation;
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
await rt.tracerProvider?.forceFlush?.();
|
|
249
|
+
await rt.scoreClient.flush?.();
|
|
250
|
+
notify(ctx, `Langfuse test succeeded. Test trace sent to ${state.config?.host}.`);
|
|
251
|
+
return true;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
254
|
+
notify(ctx, `Langfuse test failed: ${message}`, "error");
|
|
255
|
+
return false;
|
|
256
|
+
} finally {
|
|
257
|
+
if (runtimeInitialized) {
|
|
258
|
+
await (deps.forceShutdownRuntime ?? shutdownLangfuseRuntime)();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
2
3
|
import type { Config } from "./types.js";
|
|
3
|
-
import {
|
|
4
|
+
import { CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
|
|
4
5
|
import { state } from "./state.js";
|
|
5
|
-
import {
|
|
6
|
+
import { forceShutdownRuntime } from "./langfuse.js";
|
|
7
|
+
import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
|
|
6
8
|
|
|
7
|
-
export function loadConfigFromFile(): Config | null {
|
|
8
|
-
if (existsSync(
|
|
9
|
+
export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.env as EnvLike): Config | null {
|
|
10
|
+
if (existsSync(path)) {
|
|
9
11
|
try {
|
|
10
|
-
const content = readFileSync(
|
|
11
|
-
const config = JSON.parse(content) as Config;
|
|
12
|
+
const content = readFileSync(path, "utf-8");
|
|
13
|
+
const config = JSON.parse(content) as Config & { capture?: EnvLike; privacyPreset?: string };
|
|
12
14
|
if (config.publicKey && config.secretKey) {
|
|
15
|
+
const captureSource: EnvLike = {
|
|
16
|
+
...(config.capture ?? {}),
|
|
17
|
+
...(config.privacyPreset ? { LANGFUSE_PRIVACY_PRESET: config.privacyPreset } : {}),
|
|
18
|
+
...env,
|
|
19
|
+
};
|
|
13
20
|
return {
|
|
14
21
|
publicKey: config.publicKey,
|
|
15
22
|
secretKey: config.secretKey,
|
|
16
23
|
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
24
|
+
capturePolicy: createCapturePolicy(captureSource),
|
|
17
25
|
};
|
|
18
26
|
}
|
|
19
27
|
} catch (e) {
|
|
@@ -24,9 +32,9 @@ export function loadConfigFromFile(): Config | null {
|
|
|
24
32
|
return null;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
export function loadConfigFromEnv(): Config | null {
|
|
28
|
-
const publicKey =
|
|
29
|
-
const secretKey =
|
|
35
|
+
export function loadConfigFromEnv(env: EnvLike = process.env as EnvLike): Config | null {
|
|
36
|
+
const publicKey = env.LANGFUSE_PUBLIC_KEY || "";
|
|
37
|
+
const secretKey = env.LANGFUSE_SECRET_KEY || "";
|
|
30
38
|
if (!publicKey || !secretKey) {
|
|
31
39
|
return null;
|
|
32
40
|
}
|
|
@@ -34,17 +42,18 @@ export function loadConfigFromEnv(): Config | null {
|
|
|
34
42
|
return {
|
|
35
43
|
publicKey,
|
|
36
44
|
secretKey,
|
|
37
|
-
host:
|
|
45
|
+
host: env.LANGFUSE_BASE_URL || env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
46
|
+
capturePolicy: createCapturePolicy(env),
|
|
38
47
|
};
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
export function loadConfig(): Config | null {
|
|
42
|
-
return loadConfigFromFile() || loadConfigFromEnv();
|
|
50
|
+
export function loadConfig(env: EnvLike = process.env as EnvLike, path = CONFIG_PATH): Config | null {
|
|
51
|
+
return loadConfigFromFile(path, env) || loadConfigFromEnv(env);
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
export function saveConfig(config: Config) {
|
|
46
|
-
mkdirSync(
|
|
47
|
-
writeFileSync(
|
|
54
|
+
export function saveConfig(config: Config, path = CONFIG_PATH) {
|
|
55
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
56
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | null> {
|
|
@@ -72,6 +81,7 @@ async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | n
|
|
|
72
81
|
publicKey,
|
|
73
82
|
secretKey,
|
|
74
83
|
host: hostInput || DEFAULT_LANGFUSE_HOST,
|
|
84
|
+
capturePolicy: createCapturePolicy(),
|
|
75
85
|
};
|
|
76
86
|
}
|
|
77
87
|
|
|
@@ -115,7 +125,7 @@ export async function ensureConfig(ctx: any): Promise<boolean> {
|
|
|
115
125
|
export async function promptForConfig(ctx: any): Promise<boolean> {
|
|
116
126
|
state.setupAttemptedThisSession = false;
|
|
117
127
|
state.config = null;
|
|
118
|
-
await
|
|
128
|
+
await forceShutdownRuntime();
|
|
119
129
|
|
|
120
130
|
const config = await collectConfigFromUI(ctx, "Manual setup requested");
|
|
121
131
|
if (!config) {
|
package/src/handlers/agent.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { state, resetRunState, computeEvaluationScores } from "../state.js";
|
|
2
2
|
import { getRuntime, sendScore } from "../langfuse.js";
|
|
3
3
|
import { ensureConfig } from "../config.js";
|
|
4
|
-
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput } from "../utils.js";
|
|
4
|
+
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput, getCapturePolicy } from "../utils.js";
|
|
5
5
|
import { closeDanglingObservations } from "./tool.js";
|
|
6
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
7
|
+
|
|
8
|
+
function stringMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
|
|
9
|
+
if (!metadata) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const output: Record<string, string> = {};
|
|
14
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
output[key] = value;
|
|
17
|
+
} else if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
18
|
+
output[key] = String(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Object.keys(output).length > 0 ? output : undefined;
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
export function updateTraceIO(input?: unknown, output?: unknown) {
|
|
8
25
|
const root = state.agentState?.root;
|
|
@@ -45,15 +62,28 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
45
62
|
// Ignore if getSystemPrompt is not available or fails
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
const
|
|
65
|
+
const rawPromptInput = shapePayload({
|
|
49
66
|
prompt: event.prompt,
|
|
50
67
|
images: event.images,
|
|
51
68
|
context: event.context ?? event.attachments,
|
|
52
69
|
});
|
|
70
|
+
const captured = applyCapturePolicy(
|
|
71
|
+
{
|
|
72
|
+
input: rawPromptInput,
|
|
73
|
+
metadata: {
|
|
74
|
+
cwd,
|
|
75
|
+
...(state.currentModel ? { model: state.currentModel } : {}),
|
|
76
|
+
...(state.currentProvider ? { provider: state.currentProvider } : {}),
|
|
77
|
+
sessionId: state.currentSessionId || undefined,
|
|
78
|
+
},
|
|
79
|
+
systemPrompt: systemPrompt ? truncate(String(systemPrompt), 20000) : undefined,
|
|
80
|
+
},
|
|
81
|
+
getCapturePolicy(),
|
|
82
|
+
);
|
|
53
83
|
|
|
54
84
|
state.agentState = {
|
|
55
85
|
cwd,
|
|
56
|
-
promptInput,
|
|
86
|
+
promptInput: captured.input,
|
|
57
87
|
generationSeq: 0,
|
|
58
88
|
activeGenerations: new Map(),
|
|
59
89
|
generationOrder: [],
|
|
@@ -65,32 +95,25 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
65
95
|
{
|
|
66
96
|
sessionId: state.currentSessionId ? truncate(state.currentSessionId, 200) : undefined,
|
|
67
97
|
traceName: "pi-agent",
|
|
68
|
-
metadata:
|
|
69
|
-
cwd: truncate(cwd, 200),
|
|
70
|
-
...(state.currentModel ? { model: truncate(state.currentModel, 200) } : {}),
|
|
71
|
-
...(state.currentProvider ? { provider: truncate(state.currentProvider, 200) } : {}),
|
|
72
|
-
},
|
|
98
|
+
metadata: stringMetadata(captured.metadata),
|
|
73
99
|
},
|
|
74
100
|
() =>
|
|
75
101
|
rt.startObservation(
|
|
76
102
|
"pi-agent",
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sessionId: state.currentSessionId || undefined,
|
|
84
|
-
...(systemPrompt ? { systemPrompt: truncate(String(systemPrompt), 20000) } : {}),
|
|
103
|
+
{
|
|
104
|
+
input: captured.input,
|
|
105
|
+
metadata: {
|
|
106
|
+
...(captured.metadata ?? {}),
|
|
107
|
+
...(captured.systemPrompt ? { systemPrompt: captured.systemPrompt } : {}),
|
|
108
|
+
},
|
|
85
109
|
},
|
|
86
|
-
},
|
|
87
110
|
{ asType: "agent" },
|
|
88
111
|
),
|
|
89
112
|
);
|
|
90
113
|
|
|
91
114
|
state.agentState.root = root;
|
|
92
115
|
state.agentState.traceId = root.traceId;
|
|
93
|
-
updateTraceIO(
|
|
116
|
+
updateTraceIO(captured.input, undefined);
|
|
94
117
|
} catch (e) {
|
|
95
118
|
console.warn("📊 Langfuse: Failed to create agent observation", e);
|
|
96
119
|
state.isTracingDisabled = true;
|
|
@@ -104,7 +127,21 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
const lastAssistant = extractFinalAssistant(event.messages);
|
|
107
|
-
const
|
|
130
|
+
const rawOutput = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
|
|
131
|
+
const captured = applyCapturePolicy(
|
|
132
|
+
{
|
|
133
|
+
output: rawOutput,
|
|
134
|
+
metadata: {
|
|
135
|
+
cwd: state.agentState.cwd,
|
|
136
|
+
completed: true,
|
|
137
|
+
model: state.currentModel || undefined,
|
|
138
|
+
provider: state.currentProvider || undefined,
|
|
139
|
+
totalTools: state.toolCallCount,
|
|
140
|
+
...computeEvaluationScores(),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
getCapturePolicy(),
|
|
144
|
+
);
|
|
108
145
|
const scores = computeEvaluationScores();
|
|
109
146
|
|
|
110
147
|
closeDanglingObservations("Agent run ended before observation finalized");
|
|
@@ -112,18 +149,11 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
112
149
|
try {
|
|
113
150
|
state.agentState.root
|
|
114
151
|
.update({
|
|
115
|
-
output,
|
|
116
|
-
metadata:
|
|
117
|
-
cwd: state.agentState.cwd,
|
|
118
|
-
completed: true,
|
|
119
|
-
model: state.currentModel || undefined,
|
|
120
|
-
provider: state.currentProvider || undefined,
|
|
121
|
-
totalTools: state.toolCallCount,
|
|
122
|
-
...scores,
|
|
123
|
-
},
|
|
152
|
+
output: captured.output,
|
|
153
|
+
metadata: captured.metadata,
|
|
124
154
|
})
|
|
125
155
|
.end();
|
|
126
|
-
updateTraceIO(state.agentState.promptInput, output);
|
|
156
|
+
updateTraceIO(state.agentState.promptInput, captured.output);
|
|
127
157
|
|
|
128
158
|
await sendScore("tool_call_count", scores.tool_call_count, { traceId: state.agentState.traceId });
|
|
129
159
|
await sendScore("turn_count", scores.turn_count, { traceId: state.agentState.traceId });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { state } from "../state.js";
|
|
2
2
|
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
3
4
|
import {
|
|
4
5
|
getRequestKey,
|
|
5
6
|
getProviderPayload,
|
|
@@ -9,8 +10,10 @@ import {
|
|
|
9
10
|
extractAssistantOutput,
|
|
10
11
|
extractUsage,
|
|
11
12
|
extractCostDetails,
|
|
13
|
+
getCapturePolicy,
|
|
12
14
|
} from "../utils.js";
|
|
13
15
|
import type { GenerationState, ObservationUpdate } from "../types.js";
|
|
16
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
14
17
|
|
|
15
18
|
export function getOpenGeneration(): GenerationState | undefined {
|
|
16
19
|
if (state.isTracingDisabled || !state.agentState) {
|
|
@@ -44,33 +47,32 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
44
47
|
url: event.url,
|
|
45
48
|
method: event.method,
|
|
46
49
|
}) as Record<string, unknown>;
|
|
50
|
+
const captured = applyCapturePolicy(
|
|
51
|
+
{
|
|
52
|
+
input: shapePayload(payload),
|
|
53
|
+
metadata,
|
|
54
|
+
},
|
|
55
|
+
getCapturePolicy(),
|
|
56
|
+
);
|
|
47
57
|
|
|
48
58
|
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
49
|
-
const generation =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"llm-generation",
|
|
61
|
-
{
|
|
62
|
-
input: shapePayload(payload),
|
|
63
|
-
model: model || undefined,
|
|
64
|
-
metadata,
|
|
65
|
-
},
|
|
66
|
-
{ asType: "generation" },
|
|
67
|
-
);
|
|
59
|
+
const generation = await startChildObservation({
|
|
60
|
+
parent,
|
|
61
|
+
runtime: getRuntime,
|
|
62
|
+
name: "llm-generation",
|
|
63
|
+
body: {
|
|
64
|
+
input: captured.input,
|
|
65
|
+
model: model || undefined,
|
|
66
|
+
metadata: captured.metadata,
|
|
67
|
+
},
|
|
68
|
+
asType: "generation",
|
|
69
|
+
});
|
|
68
70
|
|
|
69
71
|
state.agentState.activeGenerations.set(key, {
|
|
70
72
|
observation: generation,
|
|
71
73
|
requestKey: key,
|
|
72
74
|
ended: false,
|
|
73
|
-
metadata,
|
|
75
|
+
metadata: captured.metadata ?? {},
|
|
74
76
|
});
|
|
75
77
|
state.agentState.generationOrder.push(key);
|
|
76
78
|
} catch (e) {
|
|
@@ -84,7 +86,7 @@ export function updateGenerationMetadata(event: Record<string, unknown>) {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
const key = getRequestKey(event, "");
|
|
87
|
-
const metadata = extractResponseMetadata(event);
|
|
89
|
+
const metadata = applyCapturePolicy({ metadata: extractResponseMetadata(event) }, getCapturePolicy()).metadata ?? {};
|
|
88
90
|
if (!key) {
|
|
89
91
|
const generation = getOpenGeneration();
|
|
90
92
|
if (generation) {
|
|
@@ -160,7 +162,9 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
const generation = getOpenGeneration();
|
|
163
|
-
const
|
|
165
|
+
const rawOutput = extractAssistantOutput(message);
|
|
166
|
+
const captured = applyCapturePolicy({ output: rawOutput }, getCapturePolicy());
|
|
167
|
+
const output = captured.output;
|
|
164
168
|
state.agentState.latestAssistantOutput = output;
|
|
165
169
|
|
|
166
170
|
if (!generation) {
|
|
@@ -180,6 +184,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
180
184
|
finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
|
|
181
185
|
},
|
|
182
186
|
};
|
|
187
|
+
update.metadata = applyCapturePolicy({ metadata: update.metadata }, getCapturePolicy()).metadata;
|
|
183
188
|
|
|
184
189
|
try {
|
|
185
190
|
generation.observation.update(update).end();
|
|
@@ -198,38 +203,32 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
198
203
|
const usageDetails = extractUsage({ ...event, message });
|
|
199
204
|
const costDetails = extractCostDetails({ ...event, message });
|
|
200
205
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
206
|
+
const captured = applyCapturePolicy(
|
|
207
|
+
{
|
|
208
|
+
input: state.agentState.promptInput,
|
|
209
|
+
output: extractAssistantOutput(message),
|
|
210
|
+
metadata: {
|
|
211
|
+
provider: state.currentProvider || undefined,
|
|
212
|
+
sourceEvent: "turn_end",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
getCapturePolicy(),
|
|
216
|
+
);
|
|
201
217
|
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
202
|
-
const generation =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{ asType: "generation" },
|
|
217
|
-
)
|
|
218
|
-
: (await getRuntime()).startObservation(
|
|
219
|
-
"llm-generation",
|
|
220
|
-
{
|
|
221
|
-
input: state.agentState.promptInput,
|
|
222
|
-
output: extractAssistantOutput(message),
|
|
223
|
-
model: model || undefined,
|
|
224
|
-
usageDetails,
|
|
225
|
-
costDetails,
|
|
226
|
-
metadata: {
|
|
227
|
-
provider: state.currentProvider || undefined,
|
|
228
|
-
sourceEvent: "turn_end",
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
{ asType: "generation" },
|
|
232
|
-
);
|
|
218
|
+
const generation = await startChildObservation({
|
|
219
|
+
parent,
|
|
220
|
+
runtime: getRuntime,
|
|
221
|
+
name: "llm-generation",
|
|
222
|
+
body: {
|
|
223
|
+
input: captured.input,
|
|
224
|
+
output: captured.output,
|
|
225
|
+
model: model || undefined,
|
|
226
|
+
usageDetails,
|
|
227
|
+
costDetails,
|
|
228
|
+
metadata: captured.metadata,
|
|
229
|
+
},
|
|
230
|
+
asType: "generation",
|
|
231
|
+
});
|
|
233
232
|
|
|
234
233
|
generation.end();
|
|
235
234
|
state.agentState.generationOrder.push("turn-end-fallback");
|