pi-langfuse 1.4.4 → 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/index.ts +15 -0
- package/package.json +1 -1
- package/src/commands.ts +261 -0
- package/src/config.ts +5 -4
package/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import { state, resetRunState, runWithSession, setCurrentSession } from "./src/state.js";
|
|
14
14
|
import { ensureConfig, promptForConfig, loadConfig } from "./src/config.js";
|
|
15
15
|
import { shutdownRuntime } from "./src/langfuse.js";
|
|
16
|
+
import { handleLangfusePrivacyCommand, handleLangfuseTestCommand } from "./src/commands.js";
|
|
16
17
|
import { getMessageFromEvent, extractAssistantOutput, getCapturePolicy } from "./src/utils.js";
|
|
17
18
|
import { applyCapturePolicy } from "./src/capture-policy.js";
|
|
18
19
|
import { startAgentRun, finishAgentRun } from "./src/handlers/agent.js";
|
|
@@ -52,6 +53,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
52
53
|
},
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
pi.registerCommand("langfuse-test", {
|
|
57
|
+
description: "Send a test trace to Langfuse to verify configuration",
|
|
58
|
+
handler: async (args, ctx) => {
|
|
59
|
+
await handleLangfuseTestCommand(String(args ?? ""), ctx);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.registerCommand("langfuse-privacy", {
|
|
64
|
+
description: "View or set Langfuse telemetry privacy preset",
|
|
65
|
+
handler: async (args, ctx) => {
|
|
66
|
+
await handleLangfusePrivacyCommand(String(args ?? ""), ctx);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
55
70
|
const getSessionId = (ctx?: any) => {
|
|
56
71
|
try {
|
|
57
72
|
const sessionFile = ctx?.sessionManager?.getSessionFile?.();
|
package/package.json
CHANGED
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,6 +1,7 @@
|
|
|
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
6
|
import { forceShutdownRuntime } from "./langfuse.js";
|
|
6
7
|
import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
|
|
@@ -50,9 +51,9 @@ export function loadConfig(env: EnvLike = process.env as EnvLike, path = CONFIG_
|
|
|
50
51
|
return loadConfigFromFile(path, env) || loadConfigFromEnv(env);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
export function saveConfig(config: Config) {
|
|
54
|
-
mkdirSync(
|
|
55
|
-
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");
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | null> {
|