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.
Files changed (109) hide show
  1. {tracecast-0.2.2/tracecast.egg-info → tracecast-0.2.5}/PKG-INFO +1 -1
  2. {tracecast-0.2.2 → tracecast-0.2.5}/pyproject.toml +1 -1
  3. tracecast-0.2.5/tests/test_aggregator_graph.py +228 -0
  4. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_dashboard_pushdown.py +14 -10
  5. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/tracer.py +10 -0
  6. tracecast-0.2.5/tracecast/dashboard/aggregator.py +642 -0
  7. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/reader.py +3 -2
  8. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/router.py +5 -1
  9. tracecast-0.2.5/tracecast/dashboard/static/assets/index-B07hiBI2.js +153 -0
  10. tracecast-0.2.5/tracecast/dashboard/static/assets/index-CPXZquql.js +153 -0
  11. tracecast-0.2.5/tracecast/dashboard/static/assets/index-CY5YOA8F.js +151 -0
  12. tracecast-0.2.5/tracecast/dashboard/static/assets/index-Dt0awpJJ.js +153 -0
  13. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/static/index.html +1 -1
  14. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/decorators.py +20 -7
  15. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/mongo.py +7 -3
  16. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/integrations/langchain.py +33 -36
  17. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/integrations/llm.py +75 -1
  18. {tracecast-0.2.2 → tracecast-0.2.5/tracecast.egg-info}/PKG-INFO +1 -1
  19. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/SOURCES.txt +5 -1
  20. tracecast-0.2.2/tracecast/dashboard/aggregator.py +0 -341
  21. tracecast-0.2.2/tracecast/dashboard/static/assets/index-CBra-omk.js +0 -151
  22. {tracecast-0.2.2 → tracecast-0.2.5}/LICENSE +0 -0
  23. {tracecast-0.2.2 → tracecast-0.2.5}/README.md +0 -0
  24. {tracecast-0.2.2 → tracecast-0.2.5}/setup.cfg +0 -0
  25. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_context_propagation.py +0 -0
  26. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_cost_calculator.py +0 -0
  27. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_dashboard_mount.py +0 -0
  28. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_dashboard_sessions.py +0 -0
  29. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_decorators.py +0 -0
  30. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_e2e_langgraph.py +0 -0
  31. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_exporters_db.py +0 -0
  32. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_integration_frameworks.py +0 -0
  33. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_integration_langchain.py +0 -0
  34. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_json_exporter.py +0 -0
  35. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_langchain_callback.py +0 -0
  36. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_llm_wrappers.py +0 -0
  37. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_logging.py +0 -0
  38. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_mongo_exporter.py +0 -0
  39. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_prompt_dashboard.py +0 -0
  40. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_prompts.py +0 -0
  41. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_roundtrip.py +0 -0
  42. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_schema_v2.py +0 -0
  43. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_score.py +0 -0
  44. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_score_dashboard.py +0 -0
  45. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_scoring_api.py +0 -0
  46. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_serve.py +0 -0
  47. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_span.py +0 -0
  48. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_streaming.py +0 -0
  49. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_token_counter.py +0 -0
  50. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_trace.py +0 -0
  51. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_trace_span.py +0 -0
  52. {tracecast-0.2.2 → tracecast-0.2.5}/tests/test_tracer.py +0 -0
  53. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/__init__.py +0 -0
  54. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/__init__.py +0 -0
  55. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/cost_calculator.py +0 -0
  56. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/logger.py +0 -0
  57. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/scoring.py +0 -0
  58. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/core/token_counter.py +0 -0
  59. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/__init__.py +0 -0
  60. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/asgi_middleware.py +0 -0
  61. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/blueprint.py +0 -0
  62. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/eval_reader.py +0 -0
  63. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/prompt_reader.py +0 -0
  64. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/score_reader.py +0 -0
  65. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/standalone.py +0 -0
  66. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/dashboard/static/assets/index-DllJJzmE.css +0 -0
  67. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/__init__.py +0 -0
  68. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/cli.py +0 -0
  69. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/compare.py +0 -0
  70. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/dataset.py +0 -0
  71. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/decorator.py +0 -0
  72. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/judge.py +0 -0
  73. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/metrics.py +0 -0
  74. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/models.py +0 -0
  75. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/online.py +0 -0
  76. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/runner.py +0 -0
  77. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/eval/scorers.py +0 -0
  78. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/__init__.py +0 -0
  79. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/_eval_store.py +0 -0
  80. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/base.py +0 -0
  81. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/dict_exporter.py +0 -0
  82. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/json_file.py +0 -0
  83. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/postgres.py +0 -0
  84. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/exporters/query.py +0 -0
  85. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrument.py +0 -0
  86. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/__init__.py +0 -0
  87. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/_llamaindex_handler.py +0 -0
  88. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/_streaming.py +0 -0
  89. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/anthropic_inst.py +0 -0
  90. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/base.py +0 -0
  91. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/crewai_inst.py +0 -0
  92. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/gemini_inst.py +0 -0
  93. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/langchain_inst.py +0 -0
  94. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/llamaindex_inst.py +0 -0
  95. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/instrumentors/openai_inst.py +0 -0
  96. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/integrations/__init__.py +0 -0
  97. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/middleware.py +0 -0
  98. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/__init__.py +0 -0
  99. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/score.py +0 -0
  100. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/span.py +0 -0
  101. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/models/trace.py +0 -0
  102. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/prompts/__init__.py +0 -0
  103. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/prompts/client.py +0 -0
  104. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/prompts/models.py +0 -0
  105. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast/serve.py +0 -0
  106. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/dependency_links.txt +0 -0
  107. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/entry_points.txt +0 -0
  108. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/requires.txt +0 -0
  109. {tracecast-0.2.2 → tracecast-0.2.5}/tracecast.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tracecast
3
- Version: 0.2.2
3
+ Version: 0.2.5
4
4
  Summary: LLM observability SDK — framework-agnostic tracing for AI applications
5
5
  Author: Pedro Castanheira Costa
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tracecast"
7
- version = "0.2.2"
7
+ version = "0.2.5"
8
8
  description = "LLM observability SDK — framework-agnostic tracing for AI applications"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -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 project_id and d["project_id"] != project_id:
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", "t1-llm"}
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
- llm_node = next(n for n in g["nodes"] if n["id"] == "t1-llm")
91
- assert llm_node["parent_span_id"] == "t1-root"
92
- assert llm_node["total_tokens"] == 15
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