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
+13 -5
View File
@@ -78,7 +78,10 @@ async def test_finalize_sends_user_trace(lf_server, django_user_model, monkeypat
from common.opencode_service import opencode_service
async def _msgs(_sid):
return [{"info": {"role": "assistant", "tokens": {"input": 3, "output": 4, "reasoning": 1}, "time": {"created": 1000, "completed": 1500}}, "parts": [{"type": "text", "text": ""}]}]
tool = {"type": "tool", "tool": "sap-icf_get_program_source", "state": {"status": "completed", "input": {"program_name": "ZTEST"}, "output": "REPORT ztest.", "title": "ZTEST", "time": {"start": 1100, "end": 1300}}}
bad = {"type": "tool", "tool": "read", "state": {"status": "error", "input": {"filePath": "x"}, "error": "no such file", "time": {"start": 1300, "end": 1310}}}
return [{"info": {"role": "assistant"}, "parts": [tool, bad]},
{"info": {"role": "assistant", "tokens": {"input": 3, "output": 4, "reasoning": 1}, "time": {"created": 1000, "completed": 1500}}, "parts": [{"type": "text", "text": ""}]}]
async def _sess(_sid):
return {"title": "MARA 조회"}
@@ -92,10 +95,15 @@ async def test_finalize_sends_user_trace(lf_server, django_user_model, monkeypat
await asyncio.gather(*list(langfuse._pending))
assert len(lf_server) == 1
spans = lf_server[0]["body"]["resourceSpans"][0]["scopeSpans"][0]["spans"]
t, g = _attrs(spans[0]), _attrs(spans[1])
assert [sp["name"] for sp in spans] == ["chat", "sap-icf_get_program_source", "read"]
t = _attrs(spans[0])
assert t["langfuse.user.id"] == "u@x.com" and t["langfuse.session.id"] == "ses_1"
assert t["langfuse.trace.input"] == "질문" and t["langfuse.trace.output"] == ""
assert spans[1]["parentSpanId"] == spans[0]["spanId"]
assert json.loads(g["langfuse.observation.usage_details"]) == {"input": 3, "output": 5, "total": 8}
assert g["langfuse.observation.level"] == "DEFAULT" and spans[1]["status"]["code"] == 1
assert t["langfuse.observation.type"] == "generation" # 루트가 곧 답변 — 토큰은 여기
assert json.loads(t["langfuse.observation.usage_details"]) == {"input": 3, "output": 5, "total": 8}
a1, a2 = _attrs(spans[1]), _attrs(spans[2])
assert spans[1]["parentSpanId"] == spans[0]["spanId"] and a1["langfuse.observation.type"] == "tool"
assert json.loads(a1["langfuse.observation.input"]) == {"program_name": "ZTEST"} and a1["langfuse.observation.output"] == "REPORT ztest."
assert spans[1]["startTimeUnixNano"] == "1100000000" and spans[1]["endTimeUnixNano"] == "1300000000"
assert spans[2]["status"] == {"code": 2, "message": "no such file"} and a2["langfuse.observation.level"] == "ERROR"
assert stream_mod is not None