oh-langfuse 0.1.43 → 0.1.44
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/codex_langfuse_notify.py +128 -18
- package/package.json +1 -1
package/codex_langfuse_notify.py
CHANGED
|
@@ -61,6 +61,7 @@ LOG_FILE = STATE_DIR / "codex_langfuse_notify.log"
|
|
|
61
61
|
|
|
62
62
|
DEBUG = os.environ.get("CODEX_LANGFUSE_DEBUG", "").lower() == "true"
|
|
63
63
|
MAX_CHARS = int(os.environ.get("CODEX_LANGFUSE_MAX_CHARS", "20000"))
|
|
64
|
+
MAX_SKILL_SCAN_CHARS = int(os.environ.get("CODEX_LANGFUSE_SKILL_SCAN_MAX_CHARS", "200000"))
|
|
64
65
|
METRICS_SCHEMA_VERSION = "1.1"
|
|
65
66
|
AGENT_NAME = "codex"
|
|
66
67
|
|
|
@@ -460,6 +461,7 @@ def build_interaction_metadata(
|
|
|
460
461
|
def discover_known_skills(extra_roots: Optional[List[Path]] = None) -> set:
|
|
461
462
|
roots = [
|
|
462
463
|
CODEX_DIR / "skills",
|
|
464
|
+
CODEX_DIR / "plugins" / "cache",
|
|
463
465
|
Path.home() / ".claude" / "skills",
|
|
464
466
|
Path.home() / ".config" / "opencode" / "skill",
|
|
465
467
|
]
|
|
@@ -490,6 +492,71 @@ def _skill_id_segment(name: str) -> str:
|
|
|
490
492
|
return (segment or "unknown")[:96]
|
|
491
493
|
|
|
492
494
|
|
|
495
|
+
def _skill_usage(name: str, detected_by: str, skill_call_id: str = "") -> Dict[str, str]:
|
|
496
|
+
clean = str(name or "").strip()
|
|
497
|
+
return {
|
|
498
|
+
"name": clean,
|
|
499
|
+
"skill_namespace": _skill_namespace(clean),
|
|
500
|
+
"detected_by": detected_by,
|
|
501
|
+
"skill_call_id": str(skill_call_id or "").strip(),
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _accept_skill_candidate(name: Any, known_skills: set, trusted: bool = False) -> str:
|
|
506
|
+
clean = str(name or "").strip()
|
|
507
|
+
if not clean:
|
|
508
|
+
return ""
|
|
509
|
+
if trusted or not known_skills or clean in known_skills:
|
|
510
|
+
return clean
|
|
511
|
+
return ""
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _collect_strings_limited(value: Any, out: List[str], remaining: List[int]) -> None:
|
|
515
|
+
if remaining[0] <= 0 or value is None:
|
|
516
|
+
return
|
|
517
|
+
if isinstance(value, str):
|
|
518
|
+
text = value[: remaining[0]]
|
|
519
|
+
if text:
|
|
520
|
+
out.append(text)
|
|
521
|
+
remaining[0] -= len(text)
|
|
522
|
+
return
|
|
523
|
+
if isinstance(value, (int, float, bool)):
|
|
524
|
+
return
|
|
525
|
+
if isinstance(value, list):
|
|
526
|
+
for item in value:
|
|
527
|
+
_collect_strings_limited(item, out, remaining)
|
|
528
|
+
if remaining[0] <= 0:
|
|
529
|
+
break
|
|
530
|
+
return
|
|
531
|
+
if isinstance(value, dict):
|
|
532
|
+
for item in value.values():
|
|
533
|
+
_collect_strings_limited(item, out, remaining)
|
|
534
|
+
if remaining[0] <= 0:
|
|
535
|
+
break
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _detect_skill_usages_from_text(text: str, known_skills: set) -> List[Dict[str, str]]:
|
|
539
|
+
found: List[Dict[str, str]] = []
|
|
540
|
+
if not text:
|
|
541
|
+
return found
|
|
542
|
+
seen: set = set()
|
|
543
|
+
for match in re.finditer(r"([A-Za-z]:)?[^\"'\n\r]*[\\/]+([^\\/\"'\n\r]+)[\\/]+SKILL\.md", text, re.IGNORECASE):
|
|
544
|
+
name = _accept_skill_candidate(match.group(2), known_skills)
|
|
545
|
+
if name and name not in seen:
|
|
546
|
+
seen.add(name)
|
|
547
|
+
found.append(_skill_usage(name, "skill_file_path"))
|
|
548
|
+
for match in re.finditer(r"Base directory for this skill:\s*([^\r\n]+)", text, re.IGNORECASE):
|
|
549
|
+
path_text = match.group(1)
|
|
550
|
+
path_match = re.search(r"[\\/](?:skills|skill)[\\/]([^\\/\"'\r\n]+)", path_text, re.IGNORECASE)
|
|
551
|
+
if not path_match:
|
|
552
|
+
continue
|
|
553
|
+
name = _accept_skill_candidate(path_match.group(1), known_skills)
|
|
554
|
+
if name and name not in seen:
|
|
555
|
+
seen.add(name)
|
|
556
|
+
found.append(_skill_usage(name, "skill_file_path"))
|
|
557
|
+
return found
|
|
558
|
+
|
|
559
|
+
|
|
493
560
|
def detect_skill_usages(tool_calls: List[Dict[str, Any]], known_skills: set) -> List[Dict[str, str]]:
|
|
494
561
|
found: List[Dict[str, str]] = []
|
|
495
562
|
seen_call_ids: set = set()
|
|
@@ -507,19 +574,59 @@ def detect_skill_usages(tool_calls: List[Dict[str, Any]], known_skills: set) ->
|
|
|
507
574
|
if dedupe_key in seen_call_ids:
|
|
508
575
|
break
|
|
509
576
|
seen_call_ids.add(dedupe_key)
|
|
510
|
-
found.append(
|
|
577
|
+
found.append(_skill_usage(name, "tool_call", call_id))
|
|
511
578
|
break
|
|
512
579
|
try:
|
|
513
580
|
text = json.dumps(input_obj, ensure_ascii=False)
|
|
514
581
|
except Exception:
|
|
515
582
|
text = str(input_obj)
|
|
516
|
-
|
|
517
|
-
candidate = match.group(2)
|
|
518
|
-
if candidate and (candidate in known_skills or not known_skills):
|
|
519
|
-
found.append({"name": candidate, "skill_namespace": _skill_namespace(candidate), "detected_by": "skill_file_path"})
|
|
583
|
+
found.extend(_detect_skill_usages_from_text(text, known_skills))
|
|
520
584
|
return found
|
|
521
585
|
|
|
522
586
|
|
|
587
|
+
def _dedupe_turn_skill_usages(usages: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
|
588
|
+
out: List[Dict[str, str]] = []
|
|
589
|
+
seen_call_ids: set = set()
|
|
590
|
+
seen_detected: set = set()
|
|
591
|
+
for usage in usages or []:
|
|
592
|
+
name = str(usage.get("name") or "").strip()
|
|
593
|
+
if not name:
|
|
594
|
+
continue
|
|
595
|
+
call_id = str(usage.get("skill_call_id") or "").strip()
|
|
596
|
+
if call_id:
|
|
597
|
+
key = f"call:{call_id}"
|
|
598
|
+
if key in seen_call_ids:
|
|
599
|
+
continue
|
|
600
|
+
seen_call_ids.add(key)
|
|
601
|
+
out.append(usage)
|
|
602
|
+
continue
|
|
603
|
+
detected_by = str(usage.get("detected_by") or "")
|
|
604
|
+
if detected_by == "skill_file_path":
|
|
605
|
+
key = f"{name}:{detected_by}"
|
|
606
|
+
if key in seen_detected:
|
|
607
|
+
continue
|
|
608
|
+
seen_detected.add(key)
|
|
609
|
+
out.append(usage)
|
|
610
|
+
return out
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def detect_turn_skill_usages(material: Dict[str, Any], known_skills: set) -> List[Dict[str, str]]:
|
|
614
|
+
found = list(detect_skill_usages(material.get("tool_calls") or [], known_skills))
|
|
615
|
+
sources = [
|
|
616
|
+
material.get("user_text"),
|
|
617
|
+
material.get("assistant_text"),
|
|
618
|
+
material.get("skill_detection_sources"),
|
|
619
|
+
]
|
|
620
|
+
strings: List[str] = []
|
|
621
|
+
remaining = [max(0, MAX_SKILL_SCAN_CHARS)]
|
|
622
|
+
for source in sources:
|
|
623
|
+
_collect_strings_limited(source, strings, remaining)
|
|
624
|
+
if remaining[0] <= 0:
|
|
625
|
+
break
|
|
626
|
+
found.extend(_detect_skill_usages_from_text("\n".join(strings), known_skills))
|
|
627
|
+
return _dedupe_turn_skill_usages(found)
|
|
628
|
+
|
|
629
|
+
|
|
523
630
|
def build_skill_use_events(interaction_id: str, skill_usages: List[Dict[str, str]]) -> List[Dict[str, Any]]:
|
|
524
631
|
events: List[Dict[str, Any]] = []
|
|
525
632
|
deduped: List[Dict[str, str]] = []
|
|
@@ -615,14 +722,16 @@ def usage_details_from_codex(usage: Dict[str, Any]) -> Dict[str, int]:
|
|
|
615
722
|
|
|
616
723
|
|
|
617
724
|
def collect_turn_material(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
618
|
-
user_texts: List[str] = []
|
|
619
|
-
assistant_texts: List[str] = []
|
|
620
|
-
tool_calls: List[Dict[str, Any]] = []
|
|
621
|
-
tool_results: List[Dict[str, Any]] = []
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
725
|
+
user_texts: List[str] = []
|
|
726
|
+
assistant_texts: List[str] = []
|
|
727
|
+
tool_calls: List[Dict[str, Any]] = []
|
|
728
|
+
tool_results: List[Dict[str, Any]] = []
|
|
729
|
+
skill_detection_sources: List[Any] = []
|
|
730
|
+
|
|
731
|
+
for row in rows:
|
|
732
|
+
row_type = row.get("type")
|
|
733
|
+
payload = get_payload(row)
|
|
734
|
+
skill_detection_sources.append(payload or row)
|
|
626
735
|
|
|
627
736
|
if row_type == "response_item":
|
|
628
737
|
item_type = payload.get("type")
|
|
@@ -664,10 +773,11 @@ def collect_turn_material(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
|
664
773
|
|
|
665
774
|
return {
|
|
666
775
|
"user_text": "\n\n".join(user_texts[-3:]),
|
|
667
|
-
"assistant_text": "\n\n".join(assistant_texts),
|
|
668
|
-
"tool_calls": tool_calls,
|
|
669
|
-
"tool_results": tool_results,
|
|
670
|
-
|
|
776
|
+
"assistant_text": "\n\n".join(assistant_texts),
|
|
777
|
+
"tool_calls": tool_calls,
|
|
778
|
+
"tool_results": tool_results,
|
|
779
|
+
"skill_detection_sources": skill_detection_sources,
|
|
780
|
+
}
|
|
671
781
|
|
|
672
782
|
|
|
673
783
|
def emit_codex_turn(
|
|
@@ -686,7 +796,7 @@ def emit_codex_turn(
|
|
|
686
796
|
model = first_string(meta.get("model"), meta.get("model_provider")) or "codex"
|
|
687
797
|
tool_calls = material.get("tool_calls") or []
|
|
688
798
|
tool_results = material.get("tool_results") or []
|
|
689
|
-
skill_usages =
|
|
799
|
+
skill_usages = detect_turn_skill_usages(material, discover_known_skills())
|
|
690
800
|
interaction_id = build_interaction_id("codex", session_id, turn_num)
|
|
691
801
|
skill_use_events = build_skill_use_events(interaction_id, skill_usages)
|
|
692
802
|
interaction_meta = build_interaction_metadata(
|