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.
Files changed (71) hide show
  1. prela/__init__.py +394 -0
  2. prela/_version.py +3 -0
  3. prela/contrib/CLI.md +431 -0
  4. prela/contrib/README.md +118 -0
  5. prela/contrib/__init__.py +5 -0
  6. prela/contrib/cli.py +1063 -0
  7. prela/contrib/explorer.py +571 -0
  8. prela/core/__init__.py +64 -0
  9. prela/core/clock.py +98 -0
  10. prela/core/context.py +228 -0
  11. prela/core/replay.py +403 -0
  12. prela/core/sampler.py +178 -0
  13. prela/core/span.py +295 -0
  14. prela/core/tracer.py +498 -0
  15. prela/evals/__init__.py +94 -0
  16. prela/evals/assertions/README.md +484 -0
  17. prela/evals/assertions/__init__.py +78 -0
  18. prela/evals/assertions/base.py +90 -0
  19. prela/evals/assertions/multi_agent.py +625 -0
  20. prela/evals/assertions/semantic.py +223 -0
  21. prela/evals/assertions/structural.py +443 -0
  22. prela/evals/assertions/tool.py +380 -0
  23. prela/evals/case.py +370 -0
  24. prela/evals/n8n/__init__.py +69 -0
  25. prela/evals/n8n/assertions.py +450 -0
  26. prela/evals/n8n/runner.py +497 -0
  27. prela/evals/reporters/README.md +184 -0
  28. prela/evals/reporters/__init__.py +32 -0
  29. prela/evals/reporters/console.py +251 -0
  30. prela/evals/reporters/json.py +176 -0
  31. prela/evals/reporters/junit.py +278 -0
  32. prela/evals/runner.py +525 -0
  33. prela/evals/suite.py +316 -0
  34. prela/exporters/__init__.py +27 -0
  35. prela/exporters/base.py +189 -0
  36. prela/exporters/console.py +443 -0
  37. prela/exporters/file.py +322 -0
  38. prela/exporters/http.py +394 -0
  39. prela/exporters/multi.py +154 -0
  40. prela/exporters/otlp.py +388 -0
  41. prela/instrumentation/ANTHROPIC.md +297 -0
  42. prela/instrumentation/LANGCHAIN.md +480 -0
  43. prela/instrumentation/OPENAI.md +59 -0
  44. prela/instrumentation/__init__.py +49 -0
  45. prela/instrumentation/anthropic.py +1436 -0
  46. prela/instrumentation/auto.py +129 -0
  47. prela/instrumentation/base.py +436 -0
  48. prela/instrumentation/langchain.py +959 -0
  49. prela/instrumentation/llamaindex.py +719 -0
  50. prela/instrumentation/multi_agent/__init__.py +48 -0
  51. prela/instrumentation/multi_agent/autogen.py +357 -0
  52. prela/instrumentation/multi_agent/crewai.py +404 -0
  53. prela/instrumentation/multi_agent/langgraph.py +299 -0
  54. prela/instrumentation/multi_agent/models.py +203 -0
  55. prela/instrumentation/multi_agent/swarm.py +231 -0
  56. prela/instrumentation/n8n/__init__.py +68 -0
  57. prela/instrumentation/n8n/code_node.py +534 -0
  58. prela/instrumentation/n8n/models.py +336 -0
  59. prela/instrumentation/n8n/webhook.py +489 -0
  60. prela/instrumentation/openai.py +1198 -0
  61. prela/license.py +245 -0
  62. prela/replay/__init__.py +31 -0
  63. prela/replay/comparison.py +390 -0
  64. prela/replay/engine.py +1227 -0
  65. prela/replay/loader.py +231 -0
  66. prela/replay/result.py +196 -0
  67. prela-0.1.0.dist-info/METADATA +399 -0
  68. prela-0.1.0.dist-info/RECORD +71 -0
  69. prela-0.1.0.dist-info/WHEEL +4 -0
  70. prela-0.1.0.dist-info/entry_points.txt +2 -0
  71. 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