pi-langfuse 1.0.0 → 1.2.0
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/.trae/documents/optimize_langfuse_reporting.md +68 -0
- package/.trae/documents/pi-langfuse-refactor.md +78 -0
- package/AGENTS.md +33 -42
- package/README.md +270 -59
- package/README_CN.md +273 -61
- package/image.png +0 -0
- package/index.ts +109 -491
- package/package.json +18 -3
- package/src/config.ts +98 -0
- package/src/constants.ts +15 -0
- package/src/handlers/agent.ts +136 -0
- package/src/handlers/generation.ts +239 -0
- package/src/handlers/tool.ts +126 -0
- package/src/handlers/turn.ts +53 -0
- package/src/langfuse.ts +75 -0
- package/src/state.ts +38 -0
- package/src/types.ts +94 -0
- package/src/utils.ts +273 -0
- package/tsconfig.json +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-langfuse",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Langfuse extension for Pi coding agent",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/gooyoung/pi-langfuse.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/gooyoung/pi-langfuse/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/gooyoung/pi-langfuse#readme",
|
|
5
13
|
"type": "module",
|
|
6
14
|
"main": "index.ts",
|
|
7
15
|
"scripts": {
|
|
@@ -19,10 +27,14 @@
|
|
|
19
27
|
"pi": {
|
|
20
28
|
"extensions": [
|
|
21
29
|
"./index.ts"
|
|
22
|
-
]
|
|
30
|
+
],
|
|
31
|
+
"image": "https://github.com/gooyoung/pi-langfuse/raw/main/media/image.png"
|
|
23
32
|
},
|
|
24
33
|
"dependencies": {
|
|
25
|
-
"langfuse": "^3.0
|
|
34
|
+
"@langfuse/client": "^5.3.0",
|
|
35
|
+
"@langfuse/otel": "^5.3.0",
|
|
36
|
+
"@langfuse/tracing": "^5.3.0",
|
|
37
|
+
"@opentelemetry/sdk-node": "^0.218.0"
|
|
26
38
|
},
|
|
27
39
|
"peerDependencies": {
|
|
28
40
|
"@earendil-works/pi-coding-agent": "*"
|
|
@@ -34,5 +46,8 @@
|
|
|
34
46
|
"license": "MIT",
|
|
35
47
|
"engines": {
|
|
36
48
|
"node": ">=22"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"typescript": "^6.0.3"
|
|
37
52
|
}
|
|
38
53
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { Config } from "./types.js";
|
|
3
|
+
import { CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
|
|
4
|
+
import { state } from "./state.js";
|
|
5
|
+
import { shutdownRuntime } from "./langfuse.js";
|
|
6
|
+
|
|
7
|
+
export function loadConfigFromFile(): Config | null {
|
|
8
|
+
if (existsSync(CONFIG_PATH)) {
|
|
9
|
+
try {
|
|
10
|
+
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
11
|
+
const config = JSON.parse(content) as Config;
|
|
12
|
+
if (config.publicKey && config.secretKey) {
|
|
13
|
+
return {
|
|
14
|
+
publicKey: config.publicKey,
|
|
15
|
+
secretKey: config.secretKey,
|
|
16
|
+
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.warn("📊 Langfuse: Failed to load config.json", e);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadConfigFromEnv(): Config | null {
|
|
28
|
+
const publicKey = process.env.LANGFUSE_PUBLIC_KEY || "";
|
|
29
|
+
const secretKey = process.env.LANGFUSE_SECRET_KEY || "";
|
|
30
|
+
if (!publicKey || !secretKey) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
publicKey,
|
|
36
|
+
secretKey,
|
|
37
|
+
host: process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function saveConfig(config: Config) {
|
|
42
|
+
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function ensureConfig(ctx: any): Promise<boolean> {
|
|
46
|
+
if (state.config) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (state.setupAttemptedThisSession) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
state.setupAttemptedThisSession = true;
|
|
54
|
+
|
|
55
|
+
if (!ctx.hasUI) {
|
|
56
|
+
console.log("📊 Langfuse: Missing config. Run this extension in Pi UI to complete setup, or set LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL.");
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ctx.ui.notify("Langfuse setup required. Enter your API keys to enable tracing.", "info");
|
|
61
|
+
|
|
62
|
+
const publicKey = (await ctx.ui.input("Langfuse public key:", "pk-lf-..."))?.trim();
|
|
63
|
+
if (!publicKey) {
|
|
64
|
+
ctx.ui.notify("Langfuse setup cancelled.", "warning");
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const secretKey = (await ctx.ui.input("Langfuse secret key:", "sk-lf-..."))?.trim();
|
|
69
|
+
if (!secretKey) {
|
|
70
|
+
ctx.ui.notify("Langfuse setup cancelled.", "warning");
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const hostInput = (await ctx.ui.input("Langfuse host:", DEFAULT_LANGFUSE_HOST))?.trim();
|
|
75
|
+
state.config = {
|
|
76
|
+
publicKey,
|
|
77
|
+
secretKey,
|
|
78
|
+
host: hostInput || DEFAULT_LANGFUSE_HOST,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
saveConfig(state.config);
|
|
83
|
+
ctx.ui.notify(`Langfuse config saved to ${CONFIG_PATH}`, "info");
|
|
84
|
+
return true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.warn("📊 Langfuse: Failed to save config.json", error);
|
|
87
|
+
ctx.ui.notify("Failed to save Langfuse config.json. Check extension directory permissions.", "error");
|
|
88
|
+
state.config = null;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function promptForConfig(ctx: any): Promise<boolean> {
|
|
94
|
+
state.setupAttemptedThisSession = false;
|
|
95
|
+
state.config = null;
|
|
96
|
+
await shutdownRuntime();
|
|
97
|
+
return ensureConfig(ctx);
|
|
98
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
// Since we are in src/constants.ts, EXT_DIR should point to the parent of src/ (the root of the extension)
|
|
5
|
+
// We use `import.meta.url` which points to `src/constants.ts`.
|
|
6
|
+
// `dirname` gives us `src/`, and `resolve(..., '..')` gives us the root.
|
|
7
|
+
export const EXT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
export const CONFIG_PATH = resolve(EXT_DIR, "config.json");
|
|
9
|
+
export const DEFAULT_LANGFUSE_HOST = "https://cloud.langfuse.com";
|
|
10
|
+
|
|
11
|
+
export const MAX_STRING_LENGTH = 12_000;
|
|
12
|
+
export const MAX_TOOL_PAYLOAD_LENGTH = 24_000;
|
|
13
|
+
export const MAX_DEPTH = 6;
|
|
14
|
+
export const MAX_ARRAY_ITEMS = 50;
|
|
15
|
+
export const MAX_OBJECT_KEYS = 80;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { state, resetRunState, computeEvaluationScores } from "../state.js";
|
|
2
|
+
import { getRuntime, sendScore } from "../langfuse.js";
|
|
3
|
+
import { ensureConfig } from "../config.js";
|
|
4
|
+
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput } from "../utils.js";
|
|
5
|
+
import { closeDanglingObservations } from "./tool.js";
|
|
6
|
+
|
|
7
|
+
export function updateTraceIO(input?: unknown, output?: unknown) {
|
|
8
|
+
const root = state.agentState?.root;
|
|
9
|
+
if (!root?.setTraceIO) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
root.setTraceIO({ input, output });
|
|
15
|
+
} catch {
|
|
16
|
+
// Older SDKs may omit setTraceIO; root IO still mirrors trace IO in current Langfuse.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
21
|
+
if (!(await ensureConfig(ctx))) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const rt = await getRuntime();
|
|
27
|
+
const cwd = String(
|
|
28
|
+
(event.systemPromptOptions && typeof event.systemPromptOptions === "object"
|
|
29
|
+
? (event.systemPromptOptions as Record<string, unknown>).cwd
|
|
30
|
+
: undefined) ?? process.cwd(),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!state.currentModel && ctx.model) {
|
|
34
|
+
state.currentModel = ctx.model.id || "";
|
|
35
|
+
state.currentProvider = ctx.model.provider || "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let systemPrompt = undefined;
|
|
39
|
+
try {
|
|
40
|
+
if (ctx.getSystemPrompt) {
|
|
41
|
+
systemPrompt = await ctx.getSystemPrompt();
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore if getSystemPrompt is not available or fails
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const promptInput = shapePayload({
|
|
48
|
+
prompt: event.prompt,
|
|
49
|
+
images: event.images,
|
|
50
|
+
context: event.context ?? event.attachments,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
state.agentState = {
|
|
54
|
+
cwd,
|
|
55
|
+
promptInput,
|
|
56
|
+
generationSeq: 0,
|
|
57
|
+
activeGenerations: new Map(),
|
|
58
|
+
generationOrder: [],
|
|
59
|
+
activeTools: new Map(),
|
|
60
|
+
providerMetadataByRequest: new Map(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const root = rt.propagateAttributes(
|
|
64
|
+
{
|
|
65
|
+
sessionId: state.currentSessionId ? truncate(state.currentSessionId, 200) : undefined,
|
|
66
|
+
traceName: "pi-agent",
|
|
67
|
+
metadata: {
|
|
68
|
+
cwd: truncate(cwd, 200),
|
|
69
|
+
...(state.currentModel ? { model: truncate(state.currentModel, 200) } : {}),
|
|
70
|
+
...(state.currentProvider ? { provider: truncate(state.currentProvider, 200) } : {}),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
() =>
|
|
74
|
+
rt.startObservation(
|
|
75
|
+
"pi-agent",
|
|
76
|
+
{
|
|
77
|
+
input: promptInput,
|
|
78
|
+
metadata: {
|
|
79
|
+
cwd,
|
|
80
|
+
model: state.currentModel || undefined,
|
|
81
|
+
provider: state.currentProvider || undefined,
|
|
82
|
+
sessionId: state.currentSessionId || undefined,
|
|
83
|
+
...(systemPrompt ? { systemPrompt: truncate(String(systemPrompt), 20000) } : {}),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{ asType: "agent" },
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
state.agentState.root = root;
|
|
91
|
+
state.agentState.traceId = root.traceId;
|
|
92
|
+
updateTraceIO(promptInput, undefined);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.warn("📊 Langfuse: Failed to create agent observation", e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
99
|
+
if (!state.agentState?.root) {
|
|
100
|
+
resetRunState();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const lastAssistant = extractFinalAssistant(event.messages);
|
|
105
|
+
const output = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
|
|
106
|
+
const scores = computeEvaluationScores();
|
|
107
|
+
|
|
108
|
+
closeDanglingObservations("Agent run ended before observation finalized");
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
state.agentState.root
|
|
112
|
+
.update({
|
|
113
|
+
output,
|
|
114
|
+
metadata: {
|
|
115
|
+
cwd: state.agentState.cwd,
|
|
116
|
+
completed: true,
|
|
117
|
+
model: state.currentModel || undefined,
|
|
118
|
+
provider: state.currentProvider || undefined,
|
|
119
|
+
totalTools: state.toolCallCount,
|
|
120
|
+
...scores,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
.end();
|
|
124
|
+
updateTraceIO(state.agentState.promptInput, output);
|
|
125
|
+
|
|
126
|
+
await sendScore("tool_call_count", scores.tool_call_count, { traceId: state.agentState.traceId });
|
|
127
|
+
await sendScore("turn_count", scores.turn_count, { traceId: state.agentState.traceId });
|
|
128
|
+
await sendScore("total_tool_errors", scores.total_tool_errors, { traceId: state.agentState.traceId });
|
|
129
|
+
await sendScore("tool_success_rate", scores.tool_success_rate, { traceId: state.agentState.traceId });
|
|
130
|
+
await sendScore("session_had_errors", scores.session_had_errors, { traceId: state.agentState.traceId });
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn("📊 Langfuse: Failed to finish agent observation", e);
|
|
133
|
+
} finally {
|
|
134
|
+
resetRunState();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { state } from "../state.js";
|
|
2
|
+
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import {
|
|
4
|
+
getRequestKey,
|
|
5
|
+
getProviderPayload,
|
|
6
|
+
shapePayload,
|
|
7
|
+
extractResponseMetadata,
|
|
8
|
+
getMessageFromEvent,
|
|
9
|
+
extractAssistantOutput,
|
|
10
|
+
extractUsage,
|
|
11
|
+
extractCostDetails,
|
|
12
|
+
} from "../utils.js";
|
|
13
|
+
import type { GenerationState, ObservationUpdate } from "../types.js";
|
|
14
|
+
|
|
15
|
+
export function getOpenGeneration(): GenerationState | undefined {
|
|
16
|
+
if (!state.agentState) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (let i = state.agentState.generationOrder.length - 1; i >= 0; i--) {
|
|
21
|
+
const key = state.agentState.generationOrder[i];
|
|
22
|
+
const genState = state.agentState.activeGenerations.get(key);
|
|
23
|
+
if (genState && !genState.ended) {
|
|
24
|
+
return genState;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function startGeneration(event: Record<string, unknown>) {
|
|
32
|
+
if (!state.agentState?.root) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const key = getRequestKey(event, `generation-${++state.agentState.generationSeq}`);
|
|
38
|
+
const payload = getProviderPayload(event);
|
|
39
|
+
const model = String(event.model ?? event.modelId ?? state.currentModel ?? "");
|
|
40
|
+
const provider = String(event.provider ?? state.currentProvider ?? "");
|
|
41
|
+
const metadata = shapePayload({
|
|
42
|
+
provider,
|
|
43
|
+
requestId: key,
|
|
44
|
+
url: event.url,
|
|
45
|
+
method: event.method,
|
|
46
|
+
}) as Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
49
|
+
const generation = parent.startObservation
|
|
50
|
+
? parent.startObservation(
|
|
51
|
+
"llm-generation",
|
|
52
|
+
{
|
|
53
|
+
input: shapePayload(payload),
|
|
54
|
+
model: model || undefined,
|
|
55
|
+
metadata,
|
|
56
|
+
},
|
|
57
|
+
{ asType: "generation" },
|
|
58
|
+
)
|
|
59
|
+
: (await getRuntime()).startObservation(
|
|
60
|
+
"llm-generation",
|
|
61
|
+
{
|
|
62
|
+
input: shapePayload(payload),
|
|
63
|
+
model: model || undefined,
|
|
64
|
+
metadata,
|
|
65
|
+
},
|
|
66
|
+
{ asType: "generation" },
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
state.agentState.activeGenerations.set(key, {
|
|
70
|
+
observation: generation,
|
|
71
|
+
requestKey: key,
|
|
72
|
+
ended: false,
|
|
73
|
+
metadata,
|
|
74
|
+
});
|
|
75
|
+
state.agentState.generationOrder.push(key);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.warn("📊 Langfuse: Failed to start generation", e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function updateGenerationMetadata(event: Record<string, unknown>) {
|
|
82
|
+
if (!state.agentState) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const key = getRequestKey(event, "");
|
|
87
|
+
const metadata = extractResponseMetadata(event);
|
|
88
|
+
if (!key) {
|
|
89
|
+
const generation = getOpenGeneration();
|
|
90
|
+
if (generation) {
|
|
91
|
+
generation.metadata = { ...generation.metadata, ...metadata };
|
|
92
|
+
|
|
93
|
+
const isError =
|
|
94
|
+
(typeof metadata.status === "number" && metadata.status >= 400) ||
|
|
95
|
+
event.error ||
|
|
96
|
+
event.isError;
|
|
97
|
+
|
|
98
|
+
if (isError) {
|
|
99
|
+
generation.observation.update({
|
|
100
|
+
metadata: generation.metadata,
|
|
101
|
+
level: "ERROR",
|
|
102
|
+
statusMessage: String(event.error ?? metadata.statusMessage ?? "Provider request failed")
|
|
103
|
+
}).end();
|
|
104
|
+
generation.ended = true;
|
|
105
|
+
} else {
|
|
106
|
+
generation.observation.update({ metadata: generation.metadata });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const generation = state.agentState.activeGenerations.get(key) ?? getOpenGeneration();
|
|
113
|
+
if (generation) {
|
|
114
|
+
generation.metadata = { ...generation.metadata, ...metadata };
|
|
115
|
+
|
|
116
|
+
const isError =
|
|
117
|
+
(typeof metadata.status === "number" && metadata.status >= 400) ||
|
|
118
|
+
event.error ||
|
|
119
|
+
event.isError;
|
|
120
|
+
|
|
121
|
+
if (isError) {
|
|
122
|
+
generation.observation.update({
|
|
123
|
+
metadata: generation.metadata,
|
|
124
|
+
level: "ERROR",
|
|
125
|
+
statusMessage: String(event.error ?? metadata.statusMessage ?? "Provider request failed")
|
|
126
|
+
}).end();
|
|
127
|
+
generation.ended = true;
|
|
128
|
+
} else {
|
|
129
|
+
generation.observation.update({ metadata: generation.metadata });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function recordTTFT(event: Record<string, unknown>) {
|
|
135
|
+
if (!state.agentState) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const key = getRequestKey(event, "");
|
|
140
|
+
const generation = key ? state.agentState.activeGenerations.get(key) : getOpenGeneration();
|
|
141
|
+
|
|
142
|
+
if (generation && !generation.ttftRecorded && !generation.ended) {
|
|
143
|
+
generation.ttftRecorded = true;
|
|
144
|
+
try {
|
|
145
|
+
generation.observation.update({ completionStartTime: new Date() });
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// Ignore
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function finishGenerationFromMessage(event: Record<string, unknown>) {
|
|
153
|
+
if (!state.agentState) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const message = getMessageFromEvent(event);
|
|
158
|
+
if (!message || message.role !== "assistant") {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const generation = getOpenGeneration();
|
|
163
|
+
const output = extractAssistantOutput(message);
|
|
164
|
+
state.agentState.latestAssistantOutput = output;
|
|
165
|
+
|
|
166
|
+
if (!generation) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const usageDetails = extractUsage({ ...event, message });
|
|
171
|
+
const costDetails = extractCostDetails({ ...event, message });
|
|
172
|
+
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
173
|
+
const update: ObservationUpdate = {
|
|
174
|
+
output,
|
|
175
|
+
model: model || undefined,
|
|
176
|
+
usageDetails,
|
|
177
|
+
costDetails,
|
|
178
|
+
metadata: {
|
|
179
|
+
...generation.metadata,
|
|
180
|
+
finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
generation.observation.update(update).end();
|
|
186
|
+
generation.ended = true;
|
|
187
|
+
} catch (e) {
|
|
188
|
+
console.warn("📊 Langfuse: Failed to finish generation", e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function createFallbackGenerationFromTurn(event: Record<string, unknown>, message: Record<string, unknown>) {
|
|
193
|
+
if (!state.agentState?.root || state.agentState.generationOrder.length > 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const usageDetails = extractUsage({ ...event, message });
|
|
199
|
+
const costDetails = extractCostDetails({ ...event, message });
|
|
200
|
+
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
201
|
+
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
202
|
+
const generation = parent.startObservation
|
|
203
|
+
? parent.startObservation(
|
|
204
|
+
"llm-generation",
|
|
205
|
+
{
|
|
206
|
+
input: state.agentState.promptInput,
|
|
207
|
+
output: extractAssistantOutput(message),
|
|
208
|
+
model: model || undefined,
|
|
209
|
+
usageDetails,
|
|
210
|
+
costDetails,
|
|
211
|
+
metadata: {
|
|
212
|
+
provider: state.currentProvider || undefined,
|
|
213
|
+
sourceEvent: "turn_end",
|
|
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
|
+
);
|
|
233
|
+
|
|
234
|
+
generation.end();
|
|
235
|
+
state.agentState.generationOrder.push("turn-end-fallback");
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.warn("📊 Langfuse: Failed to create fallback generation", e);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { state } from "../state.js";
|
|
2
|
+
import { getRuntime, sendScore } from "../langfuse.js";
|
|
3
|
+
import {
|
|
4
|
+
getToolCallId,
|
|
5
|
+
getToolName,
|
|
6
|
+
getToolInput,
|
|
7
|
+
shapePayload,
|
|
8
|
+
extractTextContent,
|
|
9
|
+
truncate,
|
|
10
|
+
} from "../utils.js";
|
|
11
|
+
import { MAX_TOOL_PAYLOAD_LENGTH } from "../constants.js";
|
|
12
|
+
|
|
13
|
+
export async function startToolObservation(event: Record<string, unknown>) {
|
|
14
|
+
if (!state.agentState?.root) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const toolCallId = getToolCallId(event);
|
|
19
|
+
if (!toolCallId || state.agentState.activeTools.has(toolCallId)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const toolName = getToolName(event);
|
|
25
|
+
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
26
|
+
const tool = parent.startObservation
|
|
27
|
+
? parent.startObservation(
|
|
28
|
+
toolName,
|
|
29
|
+
{
|
|
30
|
+
input: shapePayload(getToolInput(event), { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
|
|
31
|
+
metadata: { toolName, toolCallId },
|
|
32
|
+
},
|
|
33
|
+
{ asType: "tool" },
|
|
34
|
+
)
|
|
35
|
+
: (await getRuntime()).startObservation(
|
|
36
|
+
toolName,
|
|
37
|
+
{
|
|
38
|
+
input: shapePayload(getToolInput(event), { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
|
|
39
|
+
metadata: { toolName, toolCallId },
|
|
40
|
+
},
|
|
41
|
+
{ asType: "tool" },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
state.toolCallCount++;
|
|
45
|
+
state.agentState.activeTools.set(toolCallId, { observation: tool, toolName, ended: false });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.warn("📊 Langfuse: Failed to start tool observation", e);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function finishToolObservation(event: Record<string, unknown>) {
|
|
52
|
+
if (!state.agentState) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const toolCallId = getToolCallId(event);
|
|
57
|
+
if (!toolCallId) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const activeTool = state.agentState.activeTools.get(toolCallId);
|
|
62
|
+
if (!activeTool || activeTool.ended) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isError = Boolean(event.isError ?? event.error ?? event.status === "error");
|
|
67
|
+
const output =
|
|
68
|
+
extractTextContent(event.content, MAX_TOOL_PAYLOAD_LENGTH) ??
|
|
69
|
+
event.output ??
|
|
70
|
+
event.result ??
|
|
71
|
+
event.error ??
|
|
72
|
+
event.content ??
|
|
73
|
+
event;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
activeTool.observation
|
|
77
|
+
.update({
|
|
78
|
+
output: shapePayload(output, { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
|
|
79
|
+
level: isError ? "ERROR" : "DEFAULT",
|
|
80
|
+
statusMessage: isError ? truncate(String(event.error ?? output), 1_000) : undefined,
|
|
81
|
+
metadata: {
|
|
82
|
+
toolName: activeTool.toolName,
|
|
83
|
+
toolCallId,
|
|
84
|
+
isError,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.end();
|
|
88
|
+
activeTool.ended = true;
|
|
89
|
+
|
|
90
|
+
if (isError) {
|
|
91
|
+
state.errorCount++;
|
|
92
|
+
await sendScore("tool_is_error", 1, {
|
|
93
|
+
traceId: state.agentState.traceId,
|
|
94
|
+
observationId: activeTool.observation.id,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.warn("📊 Langfuse: Failed to finish tool observation", e);
|
|
99
|
+
} finally {
|
|
100
|
+
state.agentState.activeTools.delete(toolCallId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function closeDanglingObservations(statusMessage: string) {
|
|
105
|
+
if (!state.agentState) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const activeTool of state.agentState.activeTools.values()) {
|
|
110
|
+
if (!activeTool.ended) {
|
|
111
|
+
activeTool.observation
|
|
112
|
+
.update({ level: "WARNING", statusMessage, metadata: { toolName: activeTool.toolName, cancelled: true } })
|
|
113
|
+
.end();
|
|
114
|
+
activeTool.ended = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const generation of state.agentState.activeGenerations.values()) {
|
|
119
|
+
if (!generation.ended) {
|
|
120
|
+
generation.observation.update({ level: "WARNING", statusMessage, metadata: { ...generation.metadata, cancelled: true } }).end();
|
|
121
|
+
generation.ended = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
state.agentState.activeTools.clear();
|
|
126
|
+
}
|