tracecast 0.2.3__tar.gz → 0.2.6__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.3/tracecast.egg-info → tracecast-0.2.6}/PKG-INFO +1 -1
- {tracecast-0.2.3 → tracecast-0.2.6}/pyproject.toml +1 -1
- tracecast-0.2.6/tests/test_aggregator_graph.py +228 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_dashboard_pushdown.py +60 -10
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/core/tracer.py +10 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/aggregator.py +273 -38
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/reader.py +25 -8
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/router.py +6 -2
- tracecast-0.2.6/tracecast/dashboard/static/assets/index-B07hiBI2.js +153 -0
- tracecast-0.2.6/tracecast/dashboard/static/assets/index-BMIlEmLS.js +153 -0
- tracecast-0.2.3/tracecast/dashboard/static/assets/index-BbNHE9WD.js → tracecast-0.2.6/tracecast/dashboard/static/assets/index-CY5YOA8F.js +48 -48
- tracecast-0.2.6/tracecast/dashboard/static/assets/index-Dt0awpJJ.js +153 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/static/index.html +1 -1
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/decorators.py +20 -7
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/mongo.py +7 -3
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/integrations/langchain.py +35 -55
- {tracecast-0.2.3 → tracecast-0.2.6/tracecast.egg-info}/PKG-INFO +1 -1
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast.egg-info/SOURCES.txt +5 -1
- {tracecast-0.2.3 → tracecast-0.2.6}/LICENSE +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/README.md +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/setup.cfg +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_context_propagation.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_cost_calculator.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_dashboard_mount.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_dashboard_sessions.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_decorators.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_e2e_langgraph.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_exporters_db.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_integration_frameworks.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_integration_langchain.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_json_exporter.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_langchain_callback.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_llm_wrappers.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_logging.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_mongo_exporter.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_prompt_dashboard.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_prompts.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_roundtrip.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_schema_v2.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_score.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_score_dashboard.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_scoring_api.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_serve.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_span.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_streaming.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_token_counter.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_trace.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_trace_span.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tests/test_tracer.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/core/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/core/cost_calculator.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/core/logger.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/core/scoring.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/core/token_counter.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/asgi_middleware.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/blueprint.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/eval_reader.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/prompt_reader.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/score_reader.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/standalone.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/dashboard/static/assets/index-DllJJzmE.css +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/cli.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/compare.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/dataset.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/decorator.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/judge.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/metrics.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/models.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/online.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/runner.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/eval/scorers.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/_eval_store.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/base.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/dict_exporter.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/json_file.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/postgres.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/exporters/query.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrument.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/_llamaindex_handler.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/_streaming.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/anthropic_inst.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/base.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/crewai_inst.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/gemini_inst.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/langchain_inst.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/llamaindex_inst.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/instrumentors/openai_inst.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/integrations/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/integrations/llm.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/middleware.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/models/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/models/score.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/models/span.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/models/trace.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/prompts/__init__.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/prompts/client.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/prompts/models.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast/serve.py +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast.egg-info/dependency_links.txt +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast.egg-info/entry_points.txt +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/tracecast.egg-info/requires.txt +0 -0
- {tracecast-0.2.3 → tracecast-0.2.6}/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():
|
|
@@ -104,3 +108,49 @@ def test_index_injects_prefix():
|
|
|
104
108
|
resp = client.get("/tc/")
|
|
105
109
|
assert resp.status_code == 200
|
|
106
110
|
assert 'window.__TC_PREFIX__ = "/tc"' in resp.text
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_get_traces_for_metrics_bypasses_max_traces_cap():
|
|
114
|
+
exporter = FakeReadableExporter([_trace(f"t{i}", "p1", i) for i in range(5)])
|
|
115
|
+
reader = TraceReader([exporter], max_traces=2)
|
|
116
|
+
assert len(reader.get_traces_for_metrics()) == 5
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_api_metrics_bypasses_max_traces_cap():
|
|
120
|
+
exporter = FakeReadableExporter([_trace(f"t{i}", "p1", i) for i in range(5)])
|
|
121
|
+
tracer = Tracer(exporters=[exporter])
|
|
122
|
+
app = FastAPI()
|
|
123
|
+
tracer.mount(app, prefix="/tc", max_traces=2)
|
|
124
|
+
client = TestClient(app)
|
|
125
|
+
resp = client.get("/tc/api/metrics", params={"from": "2020-01-01T00:00:00Z", "to": "2030-01-01T00:00:00Z"})
|
|
126
|
+
assert resp.status_code == 200
|
|
127
|
+
assert resp.json()["total_traces"] == 5
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _trace_named_project(trace_id, project_name, offset_min=0):
|
|
131
|
+
base = datetime(2026, 6, 1, tzinfo=timezone.utc) + timedelta(minutes=offset_min)
|
|
132
|
+
t = Trace(trace_id=trace_id, name="req", started_at=base, project_name=project_name)
|
|
133
|
+
t.finished_at = base + timedelta(seconds=1)
|
|
134
|
+
t._finalize()
|
|
135
|
+
return t
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_get_filter_options_bypasses_max_traces_cap():
|
|
139
|
+
# "rare-project" is the oldest trace; a small cap sorted by recency would drop it.
|
|
140
|
+
exporter = FakeReadableExporter([
|
|
141
|
+
_trace_named_project("old", "rare-project", offset_min=0),
|
|
142
|
+
*[_trace_named_project(f"t{i}", "common-project", offset_min=10 + i) for i in range(5)],
|
|
143
|
+
])
|
|
144
|
+
reader = TraceReader([exporter], max_traces=3)
|
|
145
|
+
opts = reader.get_filter_options()
|
|
146
|
+
assert "rare-project" in opts["project_names"]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_get_projects_bypasses_max_traces_cap():
|
|
150
|
+
exporter = FakeReadableExporter([
|
|
151
|
+
_trace_named_project("old", "rare-project", offset_min=0),
|
|
152
|
+
*[_trace_named_project(f"t{i}", "common-project", offset_min=10 + i) for i in range(5)],
|
|
153
|
+
])
|
|
154
|
+
reader = TraceReader([exporter], max_traces=3)
|
|
155
|
+
names = {p["project_name"] for p in reader.get_projects()}
|
|
156
|
+
assert "rare-project" in names
|
|
@@ -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
|
|