oh-langfuse 0.1.52 → 0.1.53
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
package/package.json
CHANGED
|
@@ -312,15 +312,17 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
312
312
|
"const skillAgentPath = (detectedBy) => {",
|
|
313
313
|
" if (detectedBy === 'tool_call') return 'opencode_skill_tool';",
|
|
314
314
|
" if (detectedBy === 'slash_command') return 'opencode_slash_prompt';",
|
|
315
|
+
" if (detectedBy === 'natural_language_request') return 'opencode_prompt_skill_name';",
|
|
315
316
|
" if (detectedBy === 'skill_file_path') return 'skill_file_path';",
|
|
316
317
|
" return detectedBy || 'metadata';",
|
|
317
318
|
"};",
|
|
318
319
|
"const skillInvocationMode = (detectedBy) => {",
|
|
319
320
|
" if (detectedBy === 'slash_command') return 'explicit_request';",
|
|
321
|
+
" if (detectedBy === 'natural_language_request') return 'implicit_request';",
|
|
320
322
|
" if (detectedBy === 'tool_call' || detectedBy === 'plugin_event') return 'implicit';",
|
|
321
323
|
" return 'detected';",
|
|
322
324
|
"};",
|
|
323
|
-
"const skillEventType = (detectedBy) => detectedBy === 'slash_command' ? 'requested' : (detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected');",
|
|
325
|
+
"const skillEventType = (detectedBy) => (detectedBy === 'slash_command' || detectedBy === 'natural_language_request') ? 'requested' : (detectedBy === 'tool_call' || detectedBy === 'plugin_event' ? 'invoked' : 'detected');",
|
|
324
326
|
"const skillIdSegment = (name) => String(name || 'unknown').trim().replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 96) || 'unknown';",
|
|
325
327
|
"",
|
|
326
328
|
"const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');",
|
|
@@ -373,6 +375,18 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
373
375
|
" return out;",
|
|
374
376
|
"};",
|
|
375
377
|
"",
|
|
378
|
+
"const detectPromptSkillRequests = (haystack, skills) => {",
|
|
379
|
+
" const text = String(haystack || '');",
|
|
380
|
+
" if (!text.trim()) return [];",
|
|
381
|
+
" const out = [];",
|
|
382
|
+
" for (const skillName of skills) {",
|
|
383
|
+
" const escaped = escapeRegExp(skillName);",
|
|
384
|
+
" const pattern = new RegExp(`(?:请\\\\s*)?(?:使用|调用|启用|采用)\\\\s*[\\\"'“”‘’]?${escaped}(?=$|[\\\\s,,。;;::、])|\\\\b(?:use|invoke|run|apply)\\\\s+[\\\"']?${escaped}(?=$|[\\\\s,,。;;::])`, 'i');",
|
|
385
|
+
" if (pattern.test(text)) out.push({ name: skillName, skill_namespace: skillNamespace(skillName), detected_by: 'natural_language_request', skill_call_id: '' });",
|
|
386
|
+
" }",
|
|
387
|
+
" return out;",
|
|
388
|
+
"};",
|
|
389
|
+
"",
|
|
376
390
|
"const detectOpencodeSkillUsages = (source, knownSkills = []) => {",
|
|
377
391
|
" const skills = normalizeSkillNames(knownSkills);",
|
|
378
392
|
" if (skills.length === 0) return [];",
|
|
@@ -393,6 +407,12 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
393
407
|
" }",
|
|
394
408
|
" found.push(...explicit);",
|
|
395
409
|
" const haystack = collectStrings(source).join('\\n');",
|
|
410
|
+
" const promptSeen = new Set(explicit.map((usage) => usage.name));",
|
|
411
|
+
" for (const usage of detectPromptSkillRequests(haystack, skills)) {",
|
|
412
|
+
" if (promptSeen.has(usage.name)) continue;",
|
|
413
|
+
" promptSeen.add(usage.name);",
|
|
414
|
+
" found.push(usage);",
|
|
415
|
+
" }",
|
|
396
416
|
" const pathSeen = new Set();",
|
|
397
417
|
" for (const match of haystack.matchAll(/(?:^|[\"'\\s])(?:[A-Za-z]:)?[^\"'\\n\\r]*[\\\\/]+([^\\\\/\"'\\n\\r]+)[\\\\/]+SKILL\\.md(?=$|[\"'\\s])/gi)) {",
|
|
398
418
|
" const skillName = match[1];",
|
|
@@ -495,9 +515,11 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
495
515
|
" sdk.start();",
|
|
496
516
|
" const metricsTracer = trace.getTracer('oh-langfuse-opencode-metrics');",
|
|
497
517
|
" const knownSkillNames = await collectKnownSkillNames();",
|
|
518
|
+
" const startupSkillUsages = detectOpencodeSkillUsages(process.argv.join('\\n'), knownSkillNames);",
|
|
498
519
|
" const messageTextById = new Map();",
|
|
499
520
|
" const skillUsagesByMessageId = new Map();",
|
|
500
521
|
" const skillUsagesBySessionId = new Map();",
|
|
522
|
+
" const startupSkillSessionIds = new Set();",
|
|
501
523
|
" const toolCallIdsByMessageId = new Map();",
|
|
502
524
|
" const toolCallIdsBySessionId = new Map();",
|
|
503
525
|
" const toolResultIdsByMessageId = new Map();",
|
|
@@ -566,6 +588,10 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
566
588
|
" const partType = part?.type ?? '';",
|
|
567
589
|
" const sessionId = pickEventString(part?.sessionID, part?.sessionId, payload?.sessionID, payload?.sessionId, payload?.session?.id, event?.sessionID, event?.sessionId);",
|
|
568
590
|
" const messageId = pickEventString(part?.messageID, part?.messageId, payload?.messageID, payload?.messageId, payload?.message?.id, event?.messageID, event?.messageId);",
|
|
591
|
+
" if (sessionId && startupSkillUsages.length && !startupSkillSessionIds.has(sessionId)) {",
|
|
592
|
+
" startupSkillSessionIds.add(sessionId);",
|
|
593
|
+
" rememberSkillUsages(skillUsagesBySessionId, sessionId, startupSkillUsages);",
|
|
594
|
+
" }",
|
|
569
595
|
" const skillDetectionSources = [part, payload, part?.state?.input, part?.input, payload?.input, part?.state?.metadata, payload?.metadata, part?.state?.file, part?.file];",
|
|
570
596
|
" const eventSkillUsages = detectOpencodeSkillUsages(skillDetectionSources, knownSkillNames);",
|
|
571
597
|
" rememberSkillUsages(skillUsagesByMessageId, messageId, eventSkillUsages);",
|
|
@@ -339,6 +339,12 @@ function hasMetadataKey(item, key) {
|
|
|
339
339
|
return metadataValue(item, key) !== undefined;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
function metricInteractionId(item, target) {
|
|
343
|
+
return target === "opencode"
|
|
344
|
+
? directMetadataValue(item, "interaction_id") || metadataValue(item, "interaction_id")
|
|
345
|
+
: metadataValue(item, "interaction_id");
|
|
346
|
+
}
|
|
347
|
+
|
|
342
348
|
function isAgentTurnObservation(item) {
|
|
343
349
|
if (item?.name === "Agent Turn" || metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1") {
|
|
344
350
|
return true;
|
|
@@ -363,33 +369,76 @@ async function observationsForTrace(config, traceId, since) {
|
|
|
363
369
|
return fallback.filter((item) => item.traceId === traceId || item.trace_id === traceId);
|
|
364
370
|
}
|
|
365
371
|
|
|
366
|
-
|
|
372
|
+
function mergeMetricCandidates(items) {
|
|
373
|
+
const out = [];
|
|
374
|
+
const seen = new Set();
|
|
375
|
+
for (const item of items || []) {
|
|
376
|
+
if (!item || typeof item !== "object") continue;
|
|
377
|
+
const key = [idOf(item), item.name || "", item.traceId || item.trace_id || ""].join(":");
|
|
378
|
+
if (seen.has(key)) continue;
|
|
379
|
+
seen.add(key);
|
|
380
|
+
out.push(item);
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function recentMetricCandidates(config, since) {
|
|
386
|
+
const baseParams = { limit: 100, fromTimestamp: since.toISOString() };
|
|
387
|
+
const candidates = [];
|
|
388
|
+
for (const pathname of ["/traces"]) {
|
|
389
|
+
for (const params of [{ ...baseParams, userId: config.userId }, baseParams]) {
|
|
390
|
+
try {
|
|
391
|
+
candidates.push(...dataArray(await langfuseGetLenient(config, pathname, params)));
|
|
392
|
+
} catch (error) {
|
|
393
|
+
if (error.name === "AbortError" || error.status === 404) continue;
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return mergeMetricCandidates(candidates);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function verifyMetricObservations(config, found, { since, target, marker = "" }) {
|
|
367
402
|
const traceId = traceIdOfFound(found);
|
|
368
|
-
let observations = await observationsForTrace(config, traceId, since);
|
|
403
|
+
let observations = mergeMetricCandidates([...(await observationsForTrace(config, traceId, since)), found?.item]);
|
|
369
404
|
|
|
370
405
|
if (!observations.length && Array.isArray(found?.item?.observations)) {
|
|
371
|
-
observations = found.item.observations.filter((item) => typeof item === "object");
|
|
406
|
+
observations = mergeMetricCandidates(found.item.observations.filter((item) => typeof item === "object"));
|
|
407
|
+
} else if (Array.isArray(found?.item?.observations)) {
|
|
408
|
+
observations = mergeMetricCandidates([
|
|
409
|
+
...observations,
|
|
410
|
+
...found.item.observations.filter((item) => typeof item === "object"),
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
|
|
415
|
+
let interactions = observations.filter(isAgentTurnObservation);
|
|
416
|
+
|
|
417
|
+
if (!interactions.length) {
|
|
418
|
+
const recentRelated = (await recentMetricCandidates(config, since)).filter((item) => marker && containsMarker(item, marker));
|
|
419
|
+
observations = mergeMetricCandidates([...observations, ...recentRelated]);
|
|
420
|
+
skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
|
|
421
|
+
interactions = observations.filter(isAgentTurnObservation);
|
|
372
422
|
}
|
|
373
423
|
|
|
374
|
-
const skillUseObservations = observations.filter((item) => item?.name === "Skill Use");
|
|
375
424
|
if (skillUseObservations.length) {
|
|
376
|
-
throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted
|
|
425
|
+
throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted as standalone metrics.`);
|
|
377
426
|
}
|
|
378
427
|
|
|
379
|
-
const interactions = observations.filter(isAgentTurnObservation);
|
|
380
428
|
if (!interactions.length) {
|
|
381
429
|
throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
|
|
382
430
|
}
|
|
383
431
|
|
|
384
432
|
const byInteractionId = new Map();
|
|
433
|
+
const seenInteractionIds = new Set();
|
|
385
434
|
for (const item of interactions) {
|
|
386
|
-
const interactionId = target
|
|
387
|
-
? directMetadataValue(item, "interaction_id") || metadataValue(item, "interaction_id")
|
|
388
|
-
: metadataValue(item, "interaction_id");
|
|
435
|
+
const interactionId = metricInteractionId(item, target);
|
|
389
436
|
if (!interactionId) {
|
|
390
437
|
throw new Error(`Metric verification failed for ${target}: Agent Turn is missing interaction_id.`);
|
|
391
438
|
}
|
|
392
|
-
|
|
439
|
+
if (seenInteractionIds.has(interactionId)) continue;
|
|
440
|
+
seenInteractionIds.add(interactionId);
|
|
441
|
+
byInteractionId.set(interactionId, 1);
|
|
393
442
|
for (const key of ["user_id", "token_metrics_available", "tool_call_count", "skill_use_count", "metrics_schema_version"]) {
|
|
394
443
|
if (!hasMetadataKey(item, key)) {
|
|
395
444
|
throw new Error(`Metric verification failed for ${target}: Agent Turn is missing ${key}.`);
|
|
@@ -415,12 +464,6 @@ async function verifyMetricObservations(config, found, { since, target }) {
|
|
|
415
464
|
}
|
|
416
465
|
}
|
|
417
466
|
|
|
418
|
-
for (const [interactionId, count] of byInteractionId.entries()) {
|
|
419
|
-
if (count !== 1) {
|
|
420
|
-
throw new Error(`Metric verification failed for ${target}: interaction_id ${interactionId} appeared ${count} times.`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
467
|
return { traceId, interactionCount: interactions.length };
|
|
425
468
|
}
|
|
426
469
|
|
|
@@ -558,7 +601,7 @@ async function main() {
|
|
|
558
601
|
|
|
559
602
|
const found = await pollLangfuse(config, marker, { ...args, since, target });
|
|
560
603
|
console.log(`[OK] Langfuse marker found for ${target}: ${found.kind} ${found.id || ""}`.trim());
|
|
561
|
-
const metrics = await verifyMetricObservations(config, found, { since, target });
|
|
604
|
+
const metrics = await verifyMetricObservations(config, found, { since, target, marker });
|
|
562
605
|
console.log(`[OK] Langfuse metrics found for ${target}: Agent Turn x${metrics.interactionCount}`);
|
|
563
606
|
results.push({ target, marker, langfuse: { kind: found.kind, id: found.id || "", traceId: metrics.traceId }, metrics });
|
|
564
607
|
}
|