feat(langfuse): 목록 한 줄 = 채팅(질문/답/토큰), 트리 = OpenCode 도구 호출(SAP 조회·위키 read·grep) 순서대로

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
lee-hyeon-cheol
2026-09-22 14:06:11 +09:00
co-authored by Claude Fable 5.1
parent 07d733ea30
commit c9cc6b7eeb
3 changed files with 71 additions and 24 deletions
+32 -4
View File
@@ -78,11 +78,17 @@ def _span(trace_id: str, name: str, attrs: dict, *, start: str | None, end: str
def trace(trace_id: str, name: str, *, userId: str = "", sessionId: str = "", input=None, output=None,
metadata: dict | None = None, tags: list[str] | None = None, start: str | None = None, end: str | None = None) -> dict:
"""루트 span. trace 속성(이름·사용자·세션·태그)은 여기 실림."""
metadata: dict | None = None, tags: list[str] | None = None, start: str | None = None, end: str | None = None,
usage: dict | None = None, model: str = "", error: str | None = None) -> dict:
"""루트 span. trace 속성(이름·사용자·세션·태그)은 여기 실림.
usage 를 주면 루트 자체가 generation — 목록 한 줄 = 질문/답/토큰, 밑에 도구 호출만 트리로."""
attrs = {
"langfuse.trace.name": name,
"langfuse.observation.type": "span",
"langfuse.observation.type": "generation" if usage else "span",
"langfuse.observation.model.name": model,
"langfuse.observation.usage_details": usage,
"langfuse.observation.level": "ERROR" if error else "DEFAULT",
"langfuse.observation.status_message": error or "",
"langfuse.user.id": userId,
"langfuse.session.id": sessionId,
"langfuse.trace.input": input,
@@ -92,7 +98,29 @@ def trace(trace_id: str, name: str, *, userId: str = "", sessionId: str = "", in
"langfuse.trace.tags": tags,
**{f"langfuse.trace.metadata.{k}": v for k, v in (metadata or {}).items()},
}
return _span(trace_id, name, attrs, start=start, end=end, parent=None, error=None)
return _span(trace_id, name, attrs, start=start, end=end, parent=None, error=error)
def tool_span(trace_id: str, name: str, *, parent: dict, input=None, output=None, title: str = "",
start: str | None = None, end: str | None = None, error: str | None = None) -> dict:
"""도구 호출 하나(SAP 조회·위키 read·grep…). 트리에서 루트 밑에 순서대로 보임."""
inherited = {a["key"]: a["value"].get("stringValue") for a in parent.get("attributes", [])
if a["key"] in ("langfuse.user.id", "langfuse.session.id", "langfuse.trace.name", "langfuse.trace.tags")}
attrs = {
**inherited,
"langfuse.observation.type": "tool",
"langfuse.observation.input": input,
"langfuse.observation.output": output,
"langfuse.observation.level": "ERROR" if error else "DEFAULT",
"langfuse.observation.status_message": error or "",
"langfuse.observation.metadata.title": title,
}
return _span(trace_id, name, attrs, start=start, end=end, parent=parent["spanId"], error=error)
def ms_iso(ms: int | float | None) -> str | None:
"""OpenCode 의 epoch ms → ISO. 없으면 None(=지금)."""
return datetime.fromtimestamp(ms / 1000, timezone.utc).isoformat() if ms else None
def generation(trace_id: str, name: str, *, model: str = "", input=None, output=None, usage: dict | None = None,