oh-langfuse 0.1.44 → 0.1.45
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/langfuse_hook.py
CHANGED
|
@@ -10,7 +10,7 @@ import re
|
|
|
10
10
|
import sys
|
|
11
11
|
import time
|
|
12
12
|
import hashlib
|
|
13
|
-
from dataclasses import dataclass
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
14
|
from datetime import datetime, timezone
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
from typing import Any, Dict, List, Optional, Tuple
|
|
@@ -526,7 +526,7 @@ def _dedupe_turn_skill_usages(usages: List[Dict[str, str]]) -> List[Dict[str, st
|
|
|
526
526
|
|
|
527
527
|
def detect_turn_skill_usages(turn: "Turn", tool_calls: List[Dict[str, Any]], known_skills: set) -> List[Dict[str, str]]:
|
|
528
528
|
found = list(detect_skill_usages(tool_calls, known_skills))
|
|
529
|
-
rows = [turn.user_msg, *turn.assistant_msgs]
|
|
529
|
+
rows = [turn.user_msg, *getattr(turn, "context_msgs", []), *turn.assistant_msgs]
|
|
530
530
|
|
|
531
531
|
for row in rows:
|
|
532
532
|
attributed = _accept_skill_candidate(_attribution_skill_from_row(row), known_skills, trusted=True)
|
|
@@ -694,13 +694,20 @@ def read_new_jsonl(transcript_path: Path, ss: SessionState) -> Tuple[List[Dict[s
|
|
|
694
694
|
return msgs, ss
|
|
695
695
|
|
|
696
696
|
# ----------------- Turn assembly -----------------
|
|
697
|
-
@dataclass
|
|
698
|
-
class Turn:
|
|
699
|
-
user_msg: Dict[str, Any]
|
|
700
|
-
assistant_msgs: List[Dict[str, Any]]
|
|
701
|
-
tool_results_by_id: Dict[str, Any]
|
|
702
|
-
|
|
703
|
-
|
|
697
|
+
@dataclass
|
|
698
|
+
class Turn:
|
|
699
|
+
user_msg: Dict[str, Any]
|
|
700
|
+
assistant_msgs: List[Dict[str, Any]]
|
|
701
|
+
tool_results_by_id: Dict[str, Any]
|
|
702
|
+
context_msgs: List[Dict[str, Any]] = field(default_factory=list)
|
|
703
|
+
|
|
704
|
+
def is_skill_context_user_msg(msg: Dict[str, Any]) -> bool:
|
|
705
|
+
if get_role(msg) != "user" or is_tool_result(msg):
|
|
706
|
+
return False
|
|
707
|
+
text = extract_text(get_content(msg)).lstrip()
|
|
708
|
+
return text.startswith("Base directory for this skill:")
|
|
709
|
+
|
|
710
|
+
def build_turns(messages: List[Dict[str, Any]]) -> List[Turn]:
|
|
704
711
|
"""
|
|
705
712
|
Groups incremental transcript rows into turns:
|
|
706
713
|
user (non-tool-result) -> assistant messages -> (tool_result rows, possibly interleaved)
|
|
@@ -715,38 +722,50 @@ def build_turns(messages: List[Dict[str, Any]]) -> List[Turn]:
|
|
|
715
722
|
assistant_order: List[str] = [] # message ids in order of first appearance (or synthetic)
|
|
716
723
|
assistant_latest: Dict[str, Dict[str, Any]] = {} # id -> latest msg
|
|
717
724
|
|
|
718
|
-
tool_results_by_id: Dict[str, Any] = {} # tool_use_id -> content
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
725
|
+
tool_results_by_id: Dict[str, Any] = {} # tool_use_id -> content
|
|
726
|
+
context_msgs: List[Dict[str, Any]] = []
|
|
727
|
+
|
|
728
|
+
def flush_turn():
|
|
729
|
+
nonlocal current_user, assistant_order, assistant_latest, tool_results_by_id, context_msgs, turns
|
|
730
|
+
if current_user is None:
|
|
731
|
+
return
|
|
732
|
+
if not assistant_latest:
|
|
733
|
+
return
|
|
734
|
+
assistants = [assistant_latest[mid] for mid in assistant_order if mid in assistant_latest]
|
|
735
|
+
turns.append(Turn(
|
|
736
|
+
user_msg=current_user,
|
|
737
|
+
assistant_msgs=assistants,
|
|
738
|
+
tool_results_by_id=dict(tool_results_by_id),
|
|
739
|
+
context_msgs=list(context_msgs),
|
|
740
|
+
))
|
|
728
741
|
|
|
729
742
|
for msg in messages:
|
|
730
743
|
role = get_role(msg)
|
|
731
744
|
|
|
732
745
|
# tool_result rows show up as role=user with content blocks of type tool_result
|
|
733
|
-
if is_tool_result(msg):
|
|
734
|
-
for tr in iter_tool_results(get_content(msg)):
|
|
735
|
-
tid = tr.get("tool_use_id")
|
|
736
|
-
if tid:
|
|
737
|
-
tool_results_by_id[str(tid)] = tr.get("content")
|
|
738
|
-
continue
|
|
739
|
-
|
|
740
|
-
if
|
|
741
|
-
|
|
742
|
-
|
|
746
|
+
if is_tool_result(msg):
|
|
747
|
+
for tr in iter_tool_results(get_content(msg)):
|
|
748
|
+
tid = tr.get("tool_use_id")
|
|
749
|
+
if tid:
|
|
750
|
+
tool_results_by_id[str(tid)] = tr.get("content")
|
|
751
|
+
continue
|
|
752
|
+
|
|
753
|
+
if is_skill_context_user_msg(msg):
|
|
754
|
+
if current_user is not None:
|
|
755
|
+
context_msgs.append(msg)
|
|
756
|
+
continue
|
|
757
|
+
|
|
758
|
+
if role == "user":
|
|
759
|
+
# new user message -> finalize previous turn
|
|
760
|
+
flush_turn()
|
|
743
761
|
|
|
744
762
|
# start a new turn
|
|
745
763
|
current_user = msg
|
|
746
|
-
assistant_order = []
|
|
747
|
-
assistant_latest = {}
|
|
748
|
-
tool_results_by_id = {}
|
|
749
|
-
|
|
764
|
+
assistant_order = []
|
|
765
|
+
assistant_latest = {}
|
|
766
|
+
tool_results_by_id = {}
|
|
767
|
+
context_msgs = []
|
|
768
|
+
continue
|
|
750
769
|
|
|
751
770
|
if role == "assistant":
|
|
752
771
|
if current_user is None:
|
package/package.json
CHANGED
|
@@ -10,6 +10,21 @@ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
|
10
10
|
const packageJson = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
|
11
11
|
const ALLOWED_TARGETS = new Set(["claude", "opencode", "codex"]);
|
|
12
12
|
|
|
13
|
+
const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
14
|
+
const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
|
|
15
|
+
const t = {
|
|
16
|
+
reset: ansi(0),
|
|
17
|
+
bold: ansi(1),
|
|
18
|
+
cyan: ansi("96"),
|
|
19
|
+
green: ansi("92"),
|
|
20
|
+
gold: ansi("93"),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function paint(text, ...styles) {
|
|
24
|
+
if (!colorEnabled) return text;
|
|
25
|
+
return `${styles.join("")}${text}${t.reset}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
function parseArgs(argv) {
|
|
14
29
|
const args = { _: [] };
|
|
15
30
|
for (const raw of argv) {
|
|
@@ -45,6 +60,26 @@ function npxCommand() {
|
|
|
45
60
|
return process.platform === "win32" ? "npx.cmd" : "npx";
|
|
46
61
|
}
|
|
47
62
|
|
|
63
|
+
function targetLabel(target) {
|
|
64
|
+
if (target === "claude") return "Claude";
|
|
65
|
+
if (target === "codex") return "Codex";
|
|
66
|
+
if (target === "opencode") return "OpenCode";
|
|
67
|
+
return target;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printUpdateAvailable(target, message, updateCommand) {
|
|
71
|
+
const label = targetLabel(target);
|
|
72
|
+
console.log(paint(`[UPDATE] \u68c0\u6d4b\u5230 ${label} Langfuse \u53ef\u66f4\u65b0`, t.bold, t.gold));
|
|
73
|
+
console.log(`[INFO] ${message}`);
|
|
74
|
+
console.log(`${paint("[CMD]", t.bold, t.cyan)} ${updateCommand}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function printUpdateCommand(target, updateCommand) {
|
|
78
|
+
const label = targetLabel(target);
|
|
79
|
+
console.log(paint(`[UPDATE] \u6b63\u5728\u6267\u884c\u66f4\u65b0\u547d\u4ee4\uff1a${label} Langfuse`, t.bold, t.cyan));
|
|
80
|
+
console.log(`${paint("[CMD]", t.bold, t.cyan)} ${updateCommand}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
48
83
|
function runUpdate(target, args) {
|
|
49
84
|
const updateArgs = ["-y", "oh-langfuse@latest", "update", target];
|
|
50
85
|
if (args["skip-check"]) updateArgs.push("--skip-check");
|
|
@@ -89,24 +124,27 @@ async function main() {
|
|
|
89
124
|
const updateCommand = `npx oh-langfuse@latest update ${target}`;
|
|
90
125
|
|
|
91
126
|
if (args["notify-only"] || args.notifyOnly) {
|
|
92
|
-
|
|
127
|
+
printUpdateAvailable(target, message, updateCommand);
|
|
93
128
|
console.log(`[INFO] To update safely, close the agent and run: ${updateCommand}`);
|
|
94
129
|
return 0;
|
|
95
130
|
}
|
|
96
131
|
|
|
97
132
|
if (args.yes || args.y) {
|
|
98
|
-
|
|
133
|
+
printUpdateAvailable(target, message, updateCommand);
|
|
134
|
+
printUpdateCommand(target, updateCommand);
|
|
99
135
|
const code = runUpdate(target, args);
|
|
100
136
|
return args.strict ? code : 0;
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
104
|
-
|
|
140
|
+
printUpdateAvailable(target, message, updateCommand);
|
|
105
141
|
return 0;
|
|
106
142
|
}
|
|
107
143
|
|
|
108
|
-
|
|
144
|
+
printUpdateAvailable(target, message, updateCommand);
|
|
145
|
+
const answer = await question(`${paint("[CONFIRM]", t.bold, t.gold)} Update now? (y/N) `);
|
|
109
146
|
if (!/^(y|yes)$/i.test(answer)) return 0;
|
|
147
|
+
printUpdateCommand(target, updateCommand);
|
|
110
148
|
const code = runUpdate(target, args);
|
|
111
149
|
return args.strict ? code : 0;
|
|
112
150
|
}
|
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { buildUpdatePlan, extractVersionFromNpmMetadata } from "./update-utils.mjs";
|
|
7
7
|
import { writeRuntimeInstallRecord } from "./runtime-state-utils.mjs";
|
|
8
8
|
|
|
9
9
|
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -13,6 +13,23 @@ const DEFAULT_LANGFUSE_BASE_URL = "http://120.46.221.227:3000";
|
|
|
13
13
|
const DEFAULT_LANGFUSE_PUBLIC_KEY = "pk-lf-da0c90a7-6e93-4eb7-bb86-c1047c8d187d";
|
|
14
14
|
const DEFAULT_LANGFUSE_SECRET_KEY = "sk-lf-0269b85d-bfdc-442c-bfa3-e737954e3315";
|
|
15
15
|
|
|
16
|
+
const colorEnabled = process.stdout.isTTY && process.env.NO_COLOR !== "1";
|
|
17
|
+
const ansi = (code) => (colorEnabled ? `\x1b[${code}m` : "");
|
|
18
|
+
const t = {
|
|
19
|
+
reset: ansi(0),
|
|
20
|
+
bold: ansi(1),
|
|
21
|
+
dim: ansi(2),
|
|
22
|
+
cyan: ansi("96"),
|
|
23
|
+
green: ansi("92"),
|
|
24
|
+
gold: ansi("93"),
|
|
25
|
+
red: ansi("91"),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function paint(text, ...styles) {
|
|
29
|
+
if (!colorEnabled) return text;
|
|
30
|
+
return `${styles.join("")}${text}${t.reset}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
function parseArgs(argv) {
|
|
17
34
|
const args = { _: [] };
|
|
18
35
|
for (const raw of argv) {
|
|
@@ -142,14 +159,72 @@ function checkScript(target) {
|
|
|
142
159
|
return "opencode-langfuse-check.mjs";
|
|
143
160
|
}
|
|
144
161
|
|
|
162
|
+
function targetLabel(target) {
|
|
163
|
+
if (target === "claude") return "Claude";
|
|
164
|
+
if (target === "codex") return "Codex";
|
|
165
|
+
if (target === "opencode") return "OpenCode";
|
|
166
|
+
return target;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function updatedItems(target, args) {
|
|
170
|
+
const items = [];
|
|
171
|
+
if (target === "claude") {
|
|
172
|
+
items.push("Claude Code Langfuse hook", "direct claude auto-update command shim", "Claude launcher", "runtime version record");
|
|
173
|
+
} else if (target === "codex") {
|
|
174
|
+
items.push("Codex notify hook", "direct codex auto-update command shim", "Codex launcher", "runtime version record");
|
|
175
|
+
} else {
|
|
176
|
+
items.push("OpenCode Langfuse plugin/config", "direct opencode auto-update command shim", "OpenCode launcher", "runtime version record");
|
|
177
|
+
if (args["skip-plugin-install"] || args.skipPluginInstall) {
|
|
178
|
+
items[0] = "OpenCode Langfuse config";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!args["skip-check"]) items.push("post-update configuration check");
|
|
182
|
+
return items;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function printSkippedTarget(target) {
|
|
186
|
+
const label = targetLabel(target);
|
|
187
|
+
console.log(paint(`[WARN] \u5f53\u524d\u672a\u68c0\u6d4b\u5230 ${label} \u5b89\u88c5\uff0c\u8df3\u8fc7\u66f4\u65b0 ${label} Langfuse \u63d2\u4ef6\u3002`, t.bold, t.gold));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function printUpdateStart(target) {
|
|
191
|
+
const label = targetLabel(target);
|
|
192
|
+
console.log("");
|
|
193
|
+
console.log(paint(`[UPDATE] \u6b63\u5728\u66f4\u65b0 ${label} Langfuse ...`, t.bold, t.cyan));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function printUpdateCheck(target) {
|
|
197
|
+
const label = targetLabel(target);
|
|
198
|
+
console.log(paint(`[CHECK] \u6b63\u5728\u6821\u9a8c ${label} Langfuse \u914d\u7f6e ...`, t.bold, t.gold));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function printUpdateSuccess(target, args) {
|
|
202
|
+
const label = targetLabel(target);
|
|
203
|
+
console.log(paint(`[SUCCESS] ${label} Langfuse \u66f4\u65b0\u6210\u529f\uff1a${packageJson.name}@${packageJson.version}`, t.bold, t.green));
|
|
204
|
+
console.log(`${paint("[DETAIL]", t.bold, t.gold)} \u66f4\u65b0\u5185\u5bb9\uff1a${updatedItems(target, args).join("\u3001")}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function printUpdateSummary(targets) {
|
|
208
|
+
console.log("");
|
|
209
|
+
console.log(paint(`[SUCCESS] Langfuse \u66f4\u65b0\u5b8c\u6210\uff1a${targets.map(targetLabel).join("\u3001")} -> ${packageJson.name}@${packageJson.version}`, t.bold, t.green));
|
|
210
|
+
}
|
|
211
|
+
|
|
145
212
|
async function main() {
|
|
146
213
|
const args = parseArgs(process.argv.slice(2));
|
|
147
214
|
const targetArg = args._[0] || "all";
|
|
148
215
|
const installed = detectInstalledTargets();
|
|
149
|
-
const
|
|
150
|
-
|
|
216
|
+
const plan = buildUpdatePlan(targetArg, installed);
|
|
217
|
+
const targets = plan.targets;
|
|
218
|
+
for (const item of plan.skipped) {
|
|
219
|
+
if (item.reason === "not_detected") printSkippedTarget(item.target);
|
|
220
|
+
}
|
|
221
|
+
if (!targets.length && !plan.skipped.length) {
|
|
151
222
|
throw new Error("No installed oh-langfuse targets were detected. Run setup first, or specify a target with credentials.");
|
|
152
223
|
}
|
|
224
|
+
if (!targets.length) {
|
|
225
|
+
console.log("[INFO] \u6ca1\u6709\u53ef\u66f4\u65b0\u7684 Langfuse agent runtime\u3002");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
153
228
|
|
|
154
229
|
console.log(`[INFO] Running package: ${packageJson.name}@${packageJson.version}`);
|
|
155
230
|
try {
|
|
@@ -163,8 +238,7 @@ async function main() {
|
|
|
163
238
|
}
|
|
164
239
|
|
|
165
240
|
for (const target of targets) {
|
|
166
|
-
|
|
167
|
-
console.log(`[INFO] Updating ${target} runtime...`);
|
|
241
|
+
printUpdateStart(target);
|
|
168
242
|
const config = mergedConfig(target, args);
|
|
169
243
|
runNodeScript(setupScript(target), setupArgs(target, config, args));
|
|
170
244
|
writeRuntimeInstallRecord(target, {
|
|
@@ -172,9 +246,12 @@ async function main() {
|
|
|
172
246
|
packageVersion: packageJson.version,
|
|
173
247
|
});
|
|
174
248
|
if (!args["skip-check"]) {
|
|
249
|
+
printUpdateCheck(target);
|
|
175
250
|
runNodeScript(checkScript(target), []);
|
|
176
251
|
}
|
|
252
|
+
printUpdateSuccess(target, args);
|
|
177
253
|
}
|
|
254
|
+
printUpdateSummary(targets);
|
|
178
255
|
}
|
|
179
256
|
|
|
180
257
|
main().catch((error) => {
|
package/scripts/update-utils.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const ALLOWED_TARGETS = new Set(["claude", "opencode", "codex"]);
|
|
2
|
+
const ALL_TARGETS = ["claude", "opencode", "codex"];
|
|
2
3
|
|
|
3
4
|
export function extractVersionFromNpmMetadata(metadata) {
|
|
4
5
|
const latest = metadata?.["dist-tags"]?.latest;
|
|
@@ -38,10 +39,35 @@ export function isNewerVersion(candidate, current) {
|
|
|
38
39
|
export function selectUpdateTargets(target = "all", installed = {}) {
|
|
39
40
|
const normalized = String(target || "all").trim().toLowerCase();
|
|
40
41
|
if (normalized === "all") {
|
|
41
|
-
return
|
|
42
|
+
return ALL_TARGETS.filter((name) => Boolean(installed[name]));
|
|
42
43
|
}
|
|
43
44
|
if (!ALLOWED_TARGETS.has(normalized)) {
|
|
44
45
|
throw new Error(`Unsupported update target: ${target}`);
|
|
45
46
|
}
|
|
46
47
|
return [normalized];
|
|
47
48
|
}
|
|
49
|
+
|
|
50
|
+
export function buildUpdatePlan(target = "all", installed = {}) {
|
|
51
|
+
const normalized = String(target || "all").trim().toLowerCase();
|
|
52
|
+
if (normalized === "all") {
|
|
53
|
+
return {
|
|
54
|
+
targets: ALL_TARGETS.filter((name) => Boolean(installed[name])),
|
|
55
|
+
skipped: ALL_TARGETS
|
|
56
|
+
.filter((name) => !installed[name])
|
|
57
|
+
.map((name) => ({ target: name, reason: "not_detected" })),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (!ALLOWED_TARGETS.has(normalized)) {
|
|
61
|
+
throw new Error(`Unsupported update target: ${target}`);
|
|
62
|
+
}
|
|
63
|
+
if (!installed[normalized]) {
|
|
64
|
+
return {
|
|
65
|
+
targets: [],
|
|
66
|
+
skipped: [{ target: normalized, reason: "not_detected" }],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
targets: [normalized],
|
|
71
|
+
skipped: [],
|
|
72
|
+
};
|
|
73
|
+
}
|