prela 0.1.0__py3-none-any.whl
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.
- prela/__init__.py +394 -0
- prela/_version.py +3 -0
- prela/contrib/CLI.md +431 -0
- prela/contrib/README.md +118 -0
- prela/contrib/__init__.py +5 -0
- prela/contrib/cli.py +1063 -0
- prela/contrib/explorer.py +571 -0
- prela/core/__init__.py +64 -0
- prela/core/clock.py +98 -0
- prela/core/context.py +228 -0
- prela/core/replay.py +403 -0
- prela/core/sampler.py +178 -0
- prela/core/span.py +295 -0
- prela/core/tracer.py +498 -0
- prela/evals/__init__.py +94 -0
- prela/evals/assertions/README.md +484 -0
- prela/evals/assertions/__init__.py +78 -0
- prela/evals/assertions/base.py +90 -0
- prela/evals/assertions/multi_agent.py +625 -0
- prela/evals/assertions/semantic.py +223 -0
- prela/evals/assertions/structural.py +443 -0
- prela/evals/assertions/tool.py +380 -0
- prela/evals/case.py +370 -0
- prela/evals/n8n/__init__.py +69 -0
- prela/evals/n8n/assertions.py +450 -0
- prela/evals/n8n/runner.py +497 -0
- prela/evals/reporters/README.md +184 -0
- prela/evals/reporters/__init__.py +32 -0
- prela/evals/reporters/console.py +251 -0
- prela/evals/reporters/json.py +176 -0
- prela/evals/reporters/junit.py +278 -0
- prela/evals/runner.py +525 -0
- prela/evals/suite.py +316 -0
- prela/exporters/__init__.py +27 -0
- prela/exporters/base.py +189 -0
- prela/exporters/console.py +443 -0
- prela/exporters/file.py +322 -0
- prela/exporters/http.py +394 -0
- prela/exporters/multi.py +154 -0
- prela/exporters/otlp.py +388 -0
- prela/instrumentation/ANTHROPIC.md +297 -0
- prela/instrumentation/LANGCHAIN.md +480 -0
- prela/instrumentation/OPENAI.md +59 -0
- prela/instrumentation/__init__.py +49 -0
- prela/instrumentation/anthropic.py +1436 -0
- prela/instrumentation/auto.py +129 -0
- prela/instrumentation/base.py +436 -0
- prela/instrumentation/langchain.py +959 -0
- prela/instrumentation/llamaindex.py +719 -0
- prela/instrumentation/multi_agent/__init__.py +48 -0
- prela/instrumentation/multi_agent/autogen.py +357 -0
- prela/instrumentation/multi_agent/crewai.py +404 -0
- prela/instrumentation/multi_agent/langgraph.py +299 -0
- prela/instrumentation/multi_agent/models.py +203 -0
- prela/instrumentation/multi_agent/swarm.py +231 -0
- prela/instrumentation/n8n/__init__.py +68 -0
- prela/instrumentation/n8n/code_node.py +534 -0
- prela/instrumentation/n8n/models.py +336 -0
- prela/instrumentation/n8n/webhook.py +489 -0
- prela/instrumentation/openai.py +1198 -0
- prela/license.py +245 -0
- prela/replay/__init__.py +31 -0
- prela/replay/comparison.py +390 -0
- prela/replay/engine.py +1227 -0
- prela/replay/loader.py +231 -0
- prela/replay/result.py +196 -0
- prela-0.1.0.dist-info/METADATA +399 -0
- prela-0.1.0.dist-info/RECORD +71 -0
- prela-0.1.0.dist-info/WHEEL +4 -0
- prela-0.1.0.dist-info/entry_points.txt +2 -0
- prela-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""Console exporter for pretty-printing spans to stdout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from prela.core.span import Span, SpanStatus, SpanType
|
|
11
|
+
from prela.exporters.base import BaseExporter, ExportResult
|
|
12
|
+
|
|
13
|
+
# Try to import rich for colored output
|
|
14
|
+
try:
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.tree import Tree
|
|
17
|
+
|
|
18
|
+
RICH_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
RICH_AVAILABLE = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConsoleExporter(BaseExporter):
|
|
24
|
+
"""
|
|
25
|
+
Export spans to console with pretty-printed tree visualization.
|
|
26
|
+
|
|
27
|
+
Features:
|
|
28
|
+
- Tree structure showing parent-child relationships
|
|
29
|
+
- Color-coded output (when rich library is available)
|
|
30
|
+
- Multiple verbosity levels (minimal, normal, verbose)
|
|
31
|
+
- Duration and status indicators
|
|
32
|
+
- Key attribute display
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
```python
|
|
36
|
+
from prela.core.tracer import Tracer
|
|
37
|
+
from prela.exporters.console import ConsoleExporter
|
|
38
|
+
|
|
39
|
+
tracer = Tracer(
|
|
40
|
+
service_name="my-agent",
|
|
41
|
+
exporter=ConsoleExporter(
|
|
42
|
+
verbosity="normal",
|
|
43
|
+
color=True,
|
|
44
|
+
show_timestamps=True
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
with tracer.span("research_agent", span_type="agent") as span:
|
|
49
|
+
with tracer.span("gpt-4", span_type="llm") as llm_span:
|
|
50
|
+
llm_span.set_attribute("llm.model", "gpt-4")
|
|
51
|
+
llm_span.set_attribute("llm.input_tokens", 150)
|
|
52
|
+
llm_span.set_attribute("llm.output_tokens", 89)
|
|
53
|
+
# Output:
|
|
54
|
+
# ─ agent: research_agent (1.523s) ✓
|
|
55
|
+
# └─ llm: gpt-4 (823ms) ✓
|
|
56
|
+
# model: gpt-4 | tokens: 150 → 89
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
verbosity: str = "normal",
|
|
63
|
+
color: bool = True,
|
|
64
|
+
show_timestamps: bool = True,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
Initialize console exporter.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
verbosity: Output verbosity level:
|
|
71
|
+
- "minimal": name + duration + status only
|
|
72
|
+
- "normal": + key attributes (model, tokens, query)
|
|
73
|
+
- "verbose": + all attributes + events
|
|
74
|
+
color: Enable colored output (requires rich library)
|
|
75
|
+
show_timestamps: Show timestamps in output
|
|
76
|
+
"""
|
|
77
|
+
if verbosity not in ("minimal", "normal", "verbose"):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Invalid verbosity: {verbosity}. "
|
|
80
|
+
"Must be 'minimal', 'normal', or 'verbose'"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.verbosity = verbosity
|
|
84
|
+
self.color = color and RICH_AVAILABLE
|
|
85
|
+
self.show_timestamps = show_timestamps
|
|
86
|
+
|
|
87
|
+
if self.color:
|
|
88
|
+
self.console = Console(file=sys.stdout)
|
|
89
|
+
|
|
90
|
+
def export(self, spans: list[Span]) -> ExportResult:
|
|
91
|
+
"""
|
|
92
|
+
Export spans to console with tree visualization.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
spans: List of spans to export
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
ExportResult.SUCCESS (console export never fails)
|
|
99
|
+
"""
|
|
100
|
+
if not spans:
|
|
101
|
+
return ExportResult.SUCCESS
|
|
102
|
+
|
|
103
|
+
# Group spans by trace_id
|
|
104
|
+
traces = defaultdict(list)
|
|
105
|
+
for span in spans:
|
|
106
|
+
traces[span.trace_id].append(span)
|
|
107
|
+
|
|
108
|
+
# Print each trace
|
|
109
|
+
for trace_id, trace_spans in traces.items():
|
|
110
|
+
self._print_trace(trace_id, trace_spans)
|
|
111
|
+
|
|
112
|
+
return ExportResult.SUCCESS
|
|
113
|
+
|
|
114
|
+
def _print_trace(self, trace_id: str, spans: list[Span]) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Print a single trace as a tree structure.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
trace_id: The trace ID
|
|
120
|
+
spans: List of spans in this trace
|
|
121
|
+
"""
|
|
122
|
+
# Build parent → children mapping
|
|
123
|
+
children_map = defaultdict(list)
|
|
124
|
+
span_map = {span.span_id: span for span in spans}
|
|
125
|
+
|
|
126
|
+
for span in spans:
|
|
127
|
+
parent_id = span.parent_span_id or "root"
|
|
128
|
+
children_map[parent_id].append(span)
|
|
129
|
+
|
|
130
|
+
# Find root spans
|
|
131
|
+
root_spans = children_map["root"]
|
|
132
|
+
|
|
133
|
+
if not root_spans:
|
|
134
|
+
# No root spans found, treat first span as root
|
|
135
|
+
root_spans = [spans[0]]
|
|
136
|
+
|
|
137
|
+
# Print trace header
|
|
138
|
+
if self.show_timestamps:
|
|
139
|
+
timestamp = root_spans[0].started_at.strftime("%H:%M:%S.%f")[:-3]
|
|
140
|
+
header = f"Trace {trace_id[:8]} @ {timestamp}"
|
|
141
|
+
else:
|
|
142
|
+
header = f"Trace {trace_id[:8]}"
|
|
143
|
+
|
|
144
|
+
if self.color:
|
|
145
|
+
self.console.print(f"\n[bold cyan]{header}[/bold cyan]")
|
|
146
|
+
else:
|
|
147
|
+
print(f"\n{header}")
|
|
148
|
+
print("=" * len(header))
|
|
149
|
+
|
|
150
|
+
# Print tree
|
|
151
|
+
if self.color:
|
|
152
|
+
for root_span in root_spans:
|
|
153
|
+
tree = self._build_rich_tree(root_span, children_map)
|
|
154
|
+
self.console.print(tree)
|
|
155
|
+
else:
|
|
156
|
+
for root_span in root_spans:
|
|
157
|
+
self._print_plain_tree(root_span, children_map, prefix="", is_last=True)
|
|
158
|
+
|
|
159
|
+
def _build_rich_tree(
|
|
160
|
+
self, span: Span, children_map: dict[str, list[Span]]
|
|
161
|
+
) -> Tree:
|
|
162
|
+
"""
|
|
163
|
+
Build a rich Tree node for a span.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
span: The span to build a tree for
|
|
167
|
+
children_map: Mapping of parent span IDs to children
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Rich Tree node
|
|
171
|
+
"""
|
|
172
|
+
# Build label
|
|
173
|
+
label = self._format_span_label(span, use_color=True)
|
|
174
|
+
|
|
175
|
+
# Create tree node
|
|
176
|
+
tree = Tree(label)
|
|
177
|
+
|
|
178
|
+
# Add attributes
|
|
179
|
+
if self.verbosity in ("normal", "verbose"):
|
|
180
|
+
attr_str = self._format_attributes(span, use_color=True)
|
|
181
|
+
if attr_str:
|
|
182
|
+
tree.add(attr_str)
|
|
183
|
+
|
|
184
|
+
# Add events (verbose only)
|
|
185
|
+
if self.verbosity == "verbose" and span.events:
|
|
186
|
+
events_tree = tree.add("[dim]Events:[/dim]")
|
|
187
|
+
for event in span.events:
|
|
188
|
+
event_time = event.timestamp.strftime("%H:%M:%S.%f")[:-3]
|
|
189
|
+
event_label = f"[dim]{event_time}[/dim] {event.name}"
|
|
190
|
+
if event.attributes:
|
|
191
|
+
attr_list = [
|
|
192
|
+
f"{k}={v}" for k, v in sorted(event.attributes.items())
|
|
193
|
+
]
|
|
194
|
+
event_label += f" [dim]({', '.join(attr_list)})[/dim]"
|
|
195
|
+
events_tree.add(event_label)
|
|
196
|
+
|
|
197
|
+
# Add children (exclude self to prevent infinite recursion)
|
|
198
|
+
children = [
|
|
199
|
+
c for c in children_map.get(span.span_id, []) if c.span_id != span.span_id
|
|
200
|
+
]
|
|
201
|
+
for child in sorted(children, key=lambda s: s.started_at):
|
|
202
|
+
child_tree = self._build_rich_tree(child, children_map)
|
|
203
|
+
tree.add(child_tree)
|
|
204
|
+
|
|
205
|
+
return tree
|
|
206
|
+
|
|
207
|
+
def _print_plain_tree(
|
|
208
|
+
self,
|
|
209
|
+
span: Span,
|
|
210
|
+
children_map: dict[str, list[Span]],
|
|
211
|
+
prefix: str,
|
|
212
|
+
is_last: bool,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Print span tree in plain text format.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
span: The span to print
|
|
219
|
+
children_map: Mapping of parent span IDs to children
|
|
220
|
+
prefix: Prefix for tree indentation
|
|
221
|
+
is_last: Whether this is the last child
|
|
222
|
+
"""
|
|
223
|
+
# Build connector
|
|
224
|
+
connector = "└─ " if is_last else "├─ "
|
|
225
|
+
|
|
226
|
+
# Print span
|
|
227
|
+
label = self._format_span_label(span, use_color=False)
|
|
228
|
+
print(f"{prefix}{connector}{label}")
|
|
229
|
+
|
|
230
|
+
# Print attributes
|
|
231
|
+
if self.verbosity in ("normal", "verbose"):
|
|
232
|
+
attr_str = self._format_attributes(span, use_color=False)
|
|
233
|
+
if attr_str:
|
|
234
|
+
child_prefix = prefix + (" " if is_last else "│ ")
|
|
235
|
+
print(f"{child_prefix} {attr_str}")
|
|
236
|
+
|
|
237
|
+
# Print events (verbose only)
|
|
238
|
+
if self.verbosity == "verbose" and span.events:
|
|
239
|
+
child_prefix = prefix + (" " if is_last else "│ ")
|
|
240
|
+
print(f"{child_prefix} Events:")
|
|
241
|
+
for event in span.events:
|
|
242
|
+
event_time = event.timestamp.strftime("%H:%M:%S.%f")[:-3]
|
|
243
|
+
event_label = f"{event_time} {event.name}"
|
|
244
|
+
if event.attributes:
|
|
245
|
+
attr_list = [
|
|
246
|
+
f"{k}={v}" for k, v in sorted(event.attributes.items())
|
|
247
|
+
]
|
|
248
|
+
event_label += f" ({', '.join(attr_list)})"
|
|
249
|
+
print(f"{child_prefix} - {event_label}")
|
|
250
|
+
|
|
251
|
+
# Print children (exclude self to prevent infinite recursion)
|
|
252
|
+
children = [
|
|
253
|
+
c for c in children_map.get(span.span_id, []) if c.span_id != span.span_id
|
|
254
|
+
]
|
|
255
|
+
sorted_children = sorted(children, key=lambda s: s.started_at)
|
|
256
|
+
for i, child in enumerate(sorted_children):
|
|
257
|
+
child_is_last = i == len(sorted_children) - 1
|
|
258
|
+
child_prefix = prefix + (" " if is_last else "│ ")
|
|
259
|
+
self._print_plain_tree(child, children_map, child_prefix, child_is_last)
|
|
260
|
+
|
|
261
|
+
def _format_span_label(self, span: Span, use_color: bool) -> str:
|
|
262
|
+
"""
|
|
263
|
+
Format a span label with type, name, duration, and status.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
span: The span to format
|
|
267
|
+
use_color: Whether to use color codes
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Formatted label string
|
|
271
|
+
"""
|
|
272
|
+
# Span type
|
|
273
|
+
span_type = span.span_type.value
|
|
274
|
+
if use_color:
|
|
275
|
+
type_color = self._get_type_color(span.span_type)
|
|
276
|
+
type_str = f"[{type_color}]{span_type}[/{type_color}]"
|
|
277
|
+
else:
|
|
278
|
+
type_str = span_type
|
|
279
|
+
|
|
280
|
+
# Duration
|
|
281
|
+
duration = self._format_duration(span)
|
|
282
|
+
|
|
283
|
+
# Status indicator
|
|
284
|
+
if span.status == SpanStatus.SUCCESS:
|
|
285
|
+
status_indicator = "[green]✓[/green]" if use_color else "✓"
|
|
286
|
+
elif span.status == SpanStatus.ERROR:
|
|
287
|
+
status_indicator = "[red]✗[/red]" if use_color else "✗"
|
|
288
|
+
else:
|
|
289
|
+
status_indicator = "[yellow]⋯[/yellow]" if use_color else "⋯"
|
|
290
|
+
|
|
291
|
+
# Build label
|
|
292
|
+
label = f"{type_str}: {span.name} ({duration}) {status_indicator}"
|
|
293
|
+
|
|
294
|
+
return label
|
|
295
|
+
|
|
296
|
+
def _format_attributes(self, span: Span, use_color: bool) -> str:
|
|
297
|
+
"""
|
|
298
|
+
Format span attributes based on verbosity level.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
span: The span to format attributes for
|
|
302
|
+
use_color: Whether to use color codes
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Formatted attributes string
|
|
306
|
+
"""
|
|
307
|
+
if self.verbosity == "verbose":
|
|
308
|
+
# Show all attributes
|
|
309
|
+
if not span.attributes:
|
|
310
|
+
return ""
|
|
311
|
+
attr_list = []
|
|
312
|
+
for key, value in sorted(span.attributes.items()):
|
|
313
|
+
if use_color:
|
|
314
|
+
attr_list.append(f"[dim]{key}[/dim] = [cyan]{value!r}[/cyan]")
|
|
315
|
+
else:
|
|
316
|
+
attr_list.append(f"{key} = {value!r}")
|
|
317
|
+
return "\n".join(attr_list)
|
|
318
|
+
else:
|
|
319
|
+
# Show key attributes only (including error messages)
|
|
320
|
+
key_attrs = self._extract_key_attributes(span)
|
|
321
|
+
if not key_attrs:
|
|
322
|
+
return ""
|
|
323
|
+
|
|
324
|
+
parts = []
|
|
325
|
+
for key, value in key_attrs.items():
|
|
326
|
+
if use_color:
|
|
327
|
+
parts.append(f"[dim]{key}:[/dim] [cyan]{value}[/cyan]")
|
|
328
|
+
else:
|
|
329
|
+
parts.append(f"{key}: {value}")
|
|
330
|
+
|
|
331
|
+
return " | ".join(parts)
|
|
332
|
+
|
|
333
|
+
def _extract_key_attributes(self, span: Span) -> dict[str, Any]:
|
|
334
|
+
"""
|
|
335
|
+
Extract key attributes to display based on span type.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
span: The span to extract attributes from
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Dictionary of key attributes
|
|
342
|
+
"""
|
|
343
|
+
attrs = span.attributes
|
|
344
|
+
key_attrs = {}
|
|
345
|
+
|
|
346
|
+
if span.span_type == SpanType.LLM:
|
|
347
|
+
# LLM: model, tokens
|
|
348
|
+
if "llm.model" in attrs:
|
|
349
|
+
key_attrs["model"] = attrs["llm.model"]
|
|
350
|
+
if "llm.input_tokens" in attrs and "llm.output_tokens" in attrs:
|
|
351
|
+
key_attrs["tokens"] = (
|
|
352
|
+
f"{attrs['llm.input_tokens']} → {attrs['llm.output_tokens']}"
|
|
353
|
+
)
|
|
354
|
+
elif "llm.prompt_tokens" in attrs and "llm.completion_tokens" in attrs:
|
|
355
|
+
key_attrs["tokens"] = (
|
|
356
|
+
f"{attrs['llm.prompt_tokens']} → {attrs['llm.completion_tokens']}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
elif span.span_type == SpanType.TOOL:
|
|
360
|
+
# Tool: tool name, query/input
|
|
361
|
+
if "tool.name" in attrs:
|
|
362
|
+
key_attrs["tool"] = attrs["tool.name"]
|
|
363
|
+
if "tool.input" in attrs:
|
|
364
|
+
tool_input = str(attrs["tool.input"])
|
|
365
|
+
if len(tool_input) > 50:
|
|
366
|
+
tool_input = tool_input[:50] + "..."
|
|
367
|
+
key_attrs["input"] = tool_input
|
|
368
|
+
|
|
369
|
+
elif span.span_type == SpanType.RETRIEVAL:
|
|
370
|
+
# Retrieval: query, document count
|
|
371
|
+
if "retriever.query" in attrs:
|
|
372
|
+
query = str(attrs["retriever.query"])
|
|
373
|
+
if len(query) > 50:
|
|
374
|
+
query = query[:50] + "..."
|
|
375
|
+
key_attrs["query"] = query
|
|
376
|
+
if "retriever.document_count" in attrs:
|
|
377
|
+
key_attrs["docs"] = attrs["retriever.document_count"]
|
|
378
|
+
|
|
379
|
+
elif span.span_type == SpanType.EMBEDDING:
|
|
380
|
+
# Embedding: model, dimensions
|
|
381
|
+
if "embedding.model" in attrs:
|
|
382
|
+
key_attrs["model"] = attrs["embedding.model"]
|
|
383
|
+
if "embedding.dimensions" in attrs:
|
|
384
|
+
key_attrs["dims"] = attrs["embedding.dimensions"]
|
|
385
|
+
|
|
386
|
+
# Show error message if present
|
|
387
|
+
if span.status == SpanStatus.ERROR and span.status_message:
|
|
388
|
+
error_msg = span.status_message
|
|
389
|
+
if len(error_msg) > 100:
|
|
390
|
+
error_msg = error_msg[:100] + "..."
|
|
391
|
+
key_attrs["error"] = error_msg
|
|
392
|
+
|
|
393
|
+
return key_attrs
|
|
394
|
+
|
|
395
|
+
def _format_duration(self, span: Span) -> str:
|
|
396
|
+
"""
|
|
397
|
+
Format span duration in human-readable format.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
span: The span to format duration for
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Formatted duration string (e.g., "1.523s", "823ms")
|
|
404
|
+
"""
|
|
405
|
+
if not span.ended_at:
|
|
406
|
+
return "running"
|
|
407
|
+
|
|
408
|
+
duration = (span.ended_at - span.started_at).total_seconds()
|
|
409
|
+
|
|
410
|
+
if duration < 0.001:
|
|
411
|
+
return f"{duration * 1_000_000:.0f}µs"
|
|
412
|
+
elif duration < 1.0:
|
|
413
|
+
return f"{duration * 1000:.0f}ms"
|
|
414
|
+
else:
|
|
415
|
+
return f"{duration:.3f}s"
|
|
416
|
+
|
|
417
|
+
def _get_type_color(self, span_type: SpanType) -> str:
|
|
418
|
+
"""
|
|
419
|
+
Get color for span type.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
span_type: The span type
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Rich color name
|
|
426
|
+
"""
|
|
427
|
+
color_map = {
|
|
428
|
+
SpanType.AGENT: "yellow",
|
|
429
|
+
SpanType.LLM: "magenta",
|
|
430
|
+
SpanType.TOOL: "blue",
|
|
431
|
+
SpanType.RETRIEVAL: "green",
|
|
432
|
+
SpanType.EMBEDDING: "cyan",
|
|
433
|
+
SpanType.CUSTOM: "white",
|
|
434
|
+
}
|
|
435
|
+
return color_map.get(span_type, "white")
|
|
436
|
+
|
|
437
|
+
def shutdown(self) -> None:
|
|
438
|
+
"""
|
|
439
|
+
Shutdown the exporter.
|
|
440
|
+
|
|
441
|
+
No cleanup needed for console exporter.
|
|
442
|
+
"""
|
|
443
|
+
pass
|