tracecast 0.2.2__tar.gz → 0.2.5__tar.gz
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.
- {tracecast-0.2.2/tracecast.egg-info → tracecast-0.2.5}/PKG-INFO +1 -1
- {tracecast-0.2.2 → tracecast-0.2.5}/pyproject.toml +1 -1
- tracecast-0.2.5/tests/test_aggregator_graph.py +228 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_dashboard_pushdown.py +14 -10
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/tracer.py +10 -0
- tracecast-0.2.5/tracecast/dashboard/aggregator.py +642 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/reader.py +3 -2
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/router.py +5 -1
- tracecast-0.2.5/tracecast/dashboard/static/assets/index-B07hiBI2.js +153 -0
- tracecast-0.2.5/tracecast/dashboard/static/assets/index-CPXZquql.js +153 -0
- tracecast-0.2.5/tracecast/dashboard/static/assets/index-CY5YOA8F.js +151 -0
- tracecast-0.2.5/tracecast/dashboard/static/assets/index-Dt0awpJJ.js +153 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/static/index.html +1 -1
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/decorators.py +20 -7
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/mongo.py +7 -3
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/integrations/langchain.py +33 -36
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/integrations/llm.py +75 -1
- {tracecast-0.2.2 → tracecast-0.2.5/tracecast.egg-info}/PKG-INFO +1 -1
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/SOURCES.txt +5 -1
- tracecast-0.2.2/tracecast/dashboard/aggregator.py +0 -341
- tracecast-0.2.2/tracecast/dashboard/static/assets/index-CBra-omk.js +0 -151
- {tracecast-0.2.2 → tracecast-0.2.5}/LICENSE +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/README.md +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/setup.cfg +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_context_propagation.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_cost_calculator.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_dashboard_mount.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_dashboard_sessions.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_decorators.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_e2e_langgraph.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_exporters_db.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_integration_frameworks.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_integration_langchain.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_json_exporter.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_langchain_callback.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_llm_wrappers.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_logging.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_mongo_exporter.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_prompt_dashboard.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_prompts.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_roundtrip.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_schema_v2.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_score.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_score_dashboard.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_scoring_api.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_serve.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_span.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_streaming.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_token_counter.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_trace.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_trace_span.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_tracer.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/cost_calculator.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/logger.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/scoring.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/token_counter.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/asgi_middleware.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/blueprint.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/eval_reader.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/prompt_reader.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/score_reader.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/standalone.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/static/assets/index-DllJJzmE.css +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/cli.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/compare.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/dataset.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/decorator.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/judge.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/metrics.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/models.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/online.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/runner.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/scorers.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/_eval_store.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/base.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/dict_exporter.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/json_file.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/postgres.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/query.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrument.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/_llamaindex_handler.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/_streaming.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/anthropic_inst.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/base.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/crewai_inst.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/gemini_inst.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/langchain_inst.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/llamaindex_inst.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/openai_inst.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/integrations/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/middleware.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/score.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/span.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/trace.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/prompts/__init__.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/prompts/client.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/prompts/models.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/serve.py +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/dependency_links.txt +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/entry_points.txt +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/requires.txt +0 -0
- {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from datetime import datetime, timezone, timedelta
|
|
2
|
+
|
|
3
|
+
from tracecast.models.trace import Trace
|
|
4
|
+
from tracecast.models.span import Span, SpanType
|
|
5
|
+
from tracecast.dashboard.aggregator import build_graph
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_T0 = datetime(2024, 1, 1, tzinfo=timezone.utc)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _span(span_id, name, type_, pid, ti=0, to=0, model=None, input=None, order=0):
|
|
12
|
+
started = _T0 + timedelta(seconds=order)
|
|
13
|
+
return Span(
|
|
14
|
+
span_id=span_id,
|
|
15
|
+
type=type_,
|
|
16
|
+
name=name,
|
|
17
|
+
started_at=started,
|
|
18
|
+
finished_at=started + timedelta(milliseconds=100),
|
|
19
|
+
parent_span_id=pid,
|
|
20
|
+
model=model,
|
|
21
|
+
tokens_in=ti,
|
|
22
|
+
tokens_out=to,
|
|
23
|
+
cost_usd=round((ti + to) * 0.000001, 6),
|
|
24
|
+
input=input,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _bia_like_trace():
|
|
29
|
+
spans = [
|
|
30
|
+
_span("root", "chain:LangGraph", SpanType.AGENT, None, order=0),
|
|
31
|
+
_span("router_node", "chain:router_node", SpanType.AGENT, "root", order=1),
|
|
32
|
+
_span("router_llm", "llm:gpt-4.1", SpanType.LLM, "router_node", ti=1867, to=18, model="gpt-4.1", order=2),
|
|
33
|
+
_span("route", "chain:route_by_intent", SpanType.AGENT, "router_node", order=3),
|
|
34
|
+
_span("service_node", "chain:service_node", SpanType.AGENT, "root", order=4),
|
|
35
|
+
_span("inner_lg", "chain:LangGraph", SpanType.AGENT, "service_node", order=5),
|
|
36
|
+
_span("agent", "chain:agent", SpanType.AGENT, "inner_lg", order=6),
|
|
37
|
+
_span("runseq", "chain:RunnableSequence", SpanType.AGENT, "agent", order=7),
|
|
38
|
+
_span("svc_llm1", "llm:gpt-4.1", SpanType.LLM, "runseq", ti=17155, to=76, model="gpt-4.1", order=8),
|
|
39
|
+
_span("svc_llm2", "llm:gpt-4.1", SpanType.LLM, "runseq", ti=17755, to=157, model="gpt-4.1", order=9),
|
|
40
|
+
_span("tools", "chain:tools", SpanType.AGENT, "inner_lg", order=10),
|
|
41
|
+
_span("tool1", "search_knowledge_base", SpanType.TOOL, "tools",
|
|
42
|
+
input="{'query': 'tecnologia tecnólogo graduação EAD', 'tipo': 'info_curso'}", order=11),
|
|
43
|
+
_span("rerank1", "llm:gpt-4.1-nano", SpanType.LLM, "tool1", ti=1800, to=27, model="gpt-4.1-nano", order=12),
|
|
44
|
+
_span("tool2", "search_knowledge_base", SpanType.TOOL, "tools",
|
|
45
|
+
input="{'query': 'engenharia software computação bacharelado EAD', 'tipo': 'info_curso'}", order=13),
|
|
46
|
+
_span("rerank2", "llm:gpt-4.1-nano", SpanType.LLM, "tool2", ti=840, to=17, model="gpt-4.1-nano", order=14),
|
|
47
|
+
_span("final", "chain:final_response", SpanType.AGENT, "root", order=15),
|
|
48
|
+
_span("orphan", "llm:gpt-4.1-nano", SpanType.LLM, None, ti=0, to=0, model="gpt-4.1-nano", order=16),
|
|
49
|
+
]
|
|
50
|
+
return Trace(trace_id="t1", name="bia_graph", started_at=_T0, spans=spans)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _nodes_by_id(graph):
|
|
54
|
+
return {n["id"]: n for n in graph["nodes"]}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_orphan_llm_dropped():
|
|
58
|
+
g = build_graph(_bia_like_trace())
|
|
59
|
+
assert "orphan" not in _nodes_by_id(g)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_attribution_targets():
|
|
63
|
+
n = _nodes_by_id(build_graph(_bia_like_trace()))
|
|
64
|
+
assert n["router_node"]["own_total_tokens"] == 1885
|
|
65
|
+
assert n["service_node"]["own_total_tokens"] == 35143
|
|
66
|
+
assert n["route"]["own_total_tokens"] == 0
|
|
67
|
+
assert n["final"]["own_total_tokens"] == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_reranker_attributes_to_tool():
|
|
71
|
+
n = _nodes_by_id(build_graph(_bia_like_trace()))
|
|
72
|
+
assert n["tool1"]["own_total_tokens"] == 1827
|
|
73
|
+
assert n["tool2"]["own_total_tokens"] == 857
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_no_double_count():
|
|
77
|
+
g = build_graph(_bia_like_trace())
|
|
78
|
+
total_own = sum(node["own_total_tokens"] for node in g["nodes"])
|
|
79
|
+
llm_total = 1885 + 17231 + 17912 + 1827 + 857
|
|
80
|
+
assert total_own == llm_total
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_llm_span_itself_not_a_node():
|
|
84
|
+
n = _nodes_by_id(build_graph(_bia_like_trace()))
|
|
85
|
+
assert "router_llm" not in n
|
|
86
|
+
assert "svc_llm1" not in n
|
|
87
|
+
assert "rerank1" not in n
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_llm_calls_per_node():
|
|
91
|
+
n = _nodes_by_id(build_graph(_bia_like_trace()))
|
|
92
|
+
assert len(n["service_node"]["llm_calls"]) == 2
|
|
93
|
+
assert len(n["tool1"]["llm_calls"]) == 1
|
|
94
|
+
assert n["route"]["llm_calls"] == []
|
|
95
|
+
call = n["tool1"]["llm_calls"][0]
|
|
96
|
+
assert call["model"] == "gpt-4.1-nano"
|
|
97
|
+
assert call["tokens_in"] == 1800
|
|
98
|
+
assert call["tokens_out"] == 27
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_primary_model_largest_call():
|
|
102
|
+
n = _nodes_by_id(build_graph(_bia_like_trace()))
|
|
103
|
+
assert n["service_node"]["primary_model"] == "gpt-4.1"
|
|
104
|
+
assert n["tool1"]["primary_model"] == "gpt-4.1-nano"
|
|
105
|
+
assert n["route"]["primary_model"] is None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_trace_totals():
|
|
109
|
+
g = build_graph(_bia_like_trace())
|
|
110
|
+
assert g["total_tokens"] == 1885 + 17231 + 17912 + 1827 + 857
|
|
111
|
+
assert g["total_tokens_in"] == 1867 + 17155 + 17755 + 1800 + 840
|
|
112
|
+
assert g["total_tokens_out"] == 18 + 76 + 157 + 27 + 17
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_tool_params_pyrepr():
|
|
116
|
+
n = _nodes_by_id(build_graph(_bia_like_trace()))
|
|
117
|
+
assert n["tool1"]["tool_params"] == {
|
|
118
|
+
"query": "tecnologia tecnólogo graduação EAD",
|
|
119
|
+
"tipo": "info_curso",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_tool_params_json():
|
|
124
|
+
spans = [
|
|
125
|
+
_span("root", "chain:LangGraph", SpanType.AGENT, None, order=0),
|
|
126
|
+
_span("tj", "search_knowledge_base", SpanType.TOOL, "root",
|
|
127
|
+
input='{"query": "x", "tipo": "y"}', order=1),
|
|
128
|
+
]
|
|
129
|
+
n = _nodes_by_id(build_graph(Trace(trace_id="t", name="n", started_at=_T0, spans=spans)))
|
|
130
|
+
assert n["tj"]["tool_params"] == {"query": "x", "tipo": "y"}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_tool_params_malformed_is_none():
|
|
134
|
+
spans = [
|
|
135
|
+
_span("root", "chain:LangGraph", SpanType.AGENT, None, order=0),
|
|
136
|
+
_span("tm", "search_knowledge_base", SpanType.TOOL, "root", input="not a dict", order=1),
|
|
137
|
+
]
|
|
138
|
+
n = _nodes_by_id(build_graph(Trace(trace_id="t", name="n", started_at=_T0, spans=spans)))
|
|
139
|
+
assert n["tm"]["tool_params"] is None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _cspan(span_id, name, type_, ti, to, model, start_s, end_s, display=False, order=None, input=None, pid=None):
|
|
143
|
+
s = Span(
|
|
144
|
+
span_id=span_id,
|
|
145
|
+
type=type_,
|
|
146
|
+
name=name,
|
|
147
|
+
started_at=_T0 + timedelta(seconds=start_s),
|
|
148
|
+
finished_at=_T0 + timedelta(seconds=end_s),
|
|
149
|
+
parent_span_id=pid,
|
|
150
|
+
model=model,
|
|
151
|
+
tokens_in=ti,
|
|
152
|
+
tokens_out=to,
|
|
153
|
+
cost_usd=round((ti + to) * 0.000001, 6),
|
|
154
|
+
input=input,
|
|
155
|
+
)
|
|
156
|
+
if display:
|
|
157
|
+
s.metadata["tc_display"] = True
|
|
158
|
+
if order is not None:
|
|
159
|
+
s.metadata["tc_order"] = order
|
|
160
|
+
return s
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _curated_trace():
|
|
164
|
+
spans = [
|
|
165
|
+
_cspan("ltm", "ltm_summarizer", SpanType.AGENT, 0, 0, None, 0, 2, display=True),
|
|
166
|
+
_cspan("ltm_llm", "llm:gpt-4.1-nano", SpanType.LLM, 600, 10, "gpt-4.1-nano", 0.5, 1.5),
|
|
167
|
+
_cspan("router", "router", SpanType.AGENT, 0, 0, None, 2, 4, display=True),
|
|
168
|
+
_cspan("router_llm", "llm:gpt-4.1", SpanType.LLM, 1800, 18, "gpt-4.1", 2.5, 3.5),
|
|
169
|
+
_cspan("chain_noise", "chain:RunnableSequence", SpanType.AGENT, 0, 0, None, 2.4, 3.6),
|
|
170
|
+
_cspan("service", "service", SpanType.AGENT, 0, 0, None, 4, 9, display=True),
|
|
171
|
+
_cspan("svc_llm1", "llm:gpt-4.1", SpanType.LLM, 17000, 76, "gpt-4.1", 4.5, 5.5),
|
|
172
|
+
_cspan("tool", "search_knowledge_base", SpanType.TOOL, 0, 0, None, 6, 8, display=True,
|
|
173
|
+
input="{'query': 'saúde graduação EAD', 'tipo': 'info_curso'}"),
|
|
174
|
+
_cspan("rerank_llm", "llm:gpt-4.1-nano", SpanType.LLM, 600, 11, "gpt-4.1-nano", 6.5, 7.5),
|
|
175
|
+
_cspan("svc_llm2", "llm:gpt-4.1", SpanType.LLM, 17500, 97, "gpt-4.1", 8.2, 8.9),
|
|
176
|
+
_cspan("final", "final_response", SpanType.AGENT, 0, 0, None, 9.1, 9.2, display=True),
|
|
177
|
+
]
|
|
178
|
+
return Trace(trace_id="c1", name="bia_graph", started_at=_T0, spans=spans)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_curated_nodes_only():
|
|
182
|
+
n = _nodes_by_id(build_graph(_curated_trace()))
|
|
183
|
+
assert set(n) == {"ltm", "router", "service", "tool", "final"}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_curated_attribution_by_containment():
|
|
187
|
+
n = _nodes_by_id(build_graph(_curated_trace()))
|
|
188
|
+
assert n["ltm"]["own_total_tokens"] == 610
|
|
189
|
+
assert n["router"]["own_total_tokens"] == 1818
|
|
190
|
+
assert n["service"]["own_total_tokens"] == 34673
|
|
191
|
+
assert n["tool"]["own_total_tokens"] == 611
|
|
192
|
+
assert n["final"]["own_total_tokens"] == 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_curated_reranker_folds_into_tool():
|
|
196
|
+
n = _nodes_by_id(build_graph(_curated_trace()))
|
|
197
|
+
assert n["tool"]["primary_model"] == "gpt-4.1-nano"
|
|
198
|
+
assert len(n["tool"]["llm_calls"]) == 1
|
|
199
|
+
assert n["service"]["primary_model"] == "gpt-4.1"
|
|
200
|
+
assert len(n["service"]["llm_calls"]) == 2
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_curated_sum_equals_total():
|
|
204
|
+
g = build_graph(_curated_trace())
|
|
205
|
+
total_own = sum(node["own_total_tokens"] for node in g["nodes"])
|
|
206
|
+
assert total_own == g["total_tokens"]
|
|
207
|
+
assert g["total_tokens"] == 610 + 1818 + 17076 + 611 + 17597
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_curated_order_by_timestamp():
|
|
211
|
+
g = build_graph(_curated_trace())
|
|
212
|
+
assert [node["id"] for node in g["nodes"]] == ["ltm", "router", "service", "tool", "final"]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_curated_explicit_order_overrides():
|
|
216
|
+
t = _curated_trace()
|
|
217
|
+
by = {s.span_id: s for s in t.spans}
|
|
218
|
+
for sid, o in [("final", 1), ("service", 2), ("tool", 2.5), ("router", 3), ("ltm", 4)]:
|
|
219
|
+
by[sid].metadata["tc_order"] = o
|
|
220
|
+
g = build_graph(t)
|
|
221
|
+
ids = [node["id"] for node in g["nodes"]]
|
|
222
|
+
assert ids == ["final", "service", "tool", "router", "ltm"]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_curated_tool_nesting_edge():
|
|
226
|
+
g = build_graph(_curated_trace())
|
|
227
|
+
pairs = {(e["from"], e["to"]) for e in g["edges"]}
|
|
228
|
+
assert ("service", "tool") in pairs
|
|
@@ -34,19 +34,21 @@ class FakeReadableExporter:
|
|
|
34
34
|
def export(self, trace):
|
|
35
35
|
self._docs.append(trace.to_dict())
|
|
36
36
|
|
|
37
|
-
def _match(self, d, project_id, from_dt, to_dt):
|
|
38
|
-
if
|
|
37
|
+
def _match(self, d, project_name, project_id, from_dt, to_dt):
|
|
38
|
+
if project_name and d.get("project_name") != project_name:
|
|
39
|
+
return False
|
|
40
|
+
if project_id and d.get("project_id") != project_id:
|
|
39
41
|
return False
|
|
40
42
|
return True
|
|
41
43
|
|
|
42
|
-
def query(self, *, project_id=None, user_id=None, session_id=None,
|
|
44
|
+
def query(self, *, project_name=None, project_id=None, user_id=None, session_id=None,
|
|
43
45
|
from_dt=None, to_dt=None, limit=50, offset=0, sort_by="date", order="desc"):
|
|
44
|
-
rows = [d for d in self._docs if self._match(d, project_id, from_dt, to_dt)]
|
|
46
|
+
rows = [d for d in self._docs if self._match(d, project_name, project_id, from_dt, to_dt)]
|
|
45
47
|
rows.sort(key=lambda d: d["started_at"], reverse=(order == "desc"))
|
|
46
48
|
return rows[offset:offset + limit]
|
|
47
49
|
|
|
48
|
-
def count(self, *, project_id=None, user_id=None, session_id=None, from_dt=None, to_dt=None):
|
|
49
|
-
return len([d for d in self._docs if self._match(d, project_id, from_dt, to_dt)])
|
|
50
|
+
def count(self, *, project_name=None, project_id=None, user_id=None, session_id=None, from_dt=None, to_dt=None):
|
|
51
|
+
return len([d for d in self._docs if self._match(d, project_name, project_id, from_dt, to_dt)])
|
|
50
52
|
|
|
51
53
|
def get(self, trace_id):
|
|
52
54
|
return next((d for d in self._docs if d["trace_id"] == trace_id), None)
|
|
@@ -85,11 +87,13 @@ def test_api_graph_endpoint():
|
|
|
85
87
|
resp = client.get("/tc/api/traces/t1/graph")
|
|
86
88
|
assert resp.status_code == 200
|
|
87
89
|
g = resp.json()
|
|
88
|
-
assert {n["id"] for n in g["nodes"]} == {"t1-root"
|
|
90
|
+
assert {n["id"] for n in g["nodes"]} == {"t1-root"}
|
|
89
91
|
assert g["edges"] == [] or all("from" in e for e in g["edges"])
|
|
90
|
-
|
|
91
|
-
assert
|
|
92
|
-
assert
|
|
92
|
+
root_node = next(n for n in g["nodes"] if n["id"] == "t1-root")
|
|
93
|
+
assert root_node["own_total_tokens"] == 15
|
|
94
|
+
assert root_node["primary_model"] == "gpt-4o"
|
|
95
|
+
assert len(root_node["llm_calls"]) == 1
|
|
96
|
+
assert g["total_tokens"] == 15
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
def test_api_graph_404():
|
|
@@ -23,6 +23,16 @@ def activate_span(span: Span):
|
|
|
23
23
|
_current_span.reset(token)
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def push_span(span: Span):
|
|
27
|
+
"""Set span as current without a context manager. Returns reset token."""
|
|
28
|
+
return _current_span.set(span)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def pop_span(token) -> None:
|
|
32
|
+
"""Restore previous current span using token from push_span."""
|
|
33
|
+
_current_span.reset(token)
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
def bind_context(fn: Callable, *args: Any, **kwargs: Any) -> Callable[[], Any]:
|
|
27
37
|
ctx = contextvars.copy_context()
|
|
28
38
|
|