trodo-python 2.8.0__py3-none-any.whl → 2.9.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.
- trodo/__init__.py +1 -1
- trodo/otel/auto_instrument.py +35 -0
- trodo/otel/helpers.py +1 -0
- trodo/otel/processor.py +12 -0
- trodo/otel/wrap_agent.py +76 -11
- {trodo_python-2.8.0.dist-info → trodo_python-2.9.0.dist-info}/METADATA +1 -1
- {trodo_python-2.8.0.dist-info → trodo_python-2.9.0.dist-info}/RECORD +9 -9
- {trodo_python-2.8.0.dist-info → trodo_python-2.9.0.dist-info}/WHEEL +0 -0
- {trodo_python-2.8.0.dist-info → trodo_python-2.9.0.dist-info}/top_level.txt +0 -0
trodo/__init__.py
CHANGED
trodo/otel/auto_instrument.py
CHANGED
|
@@ -22,6 +22,13 @@ def _hr_to_iso(nanos: Optional[int]) -> Optional[str]:
|
|
|
22
22
|
return datetime.fromtimestamp(nanos / 1e9, tz=timezone.utc).isoformat().replace("+00:00", "Z")
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _trunc(s: Any, max_len: int) -> Optional[str]:
|
|
26
|
+
if s is None:
|
|
27
|
+
return None
|
|
28
|
+
s = str(s)
|
|
29
|
+
return s[:max_len] if len(s) > max_len else s
|
|
30
|
+
|
|
31
|
+
|
|
25
32
|
def _infer_kind(attrs: Dict[str, Any]) -> str:
|
|
26
33
|
if not attrs:
|
|
27
34
|
return "generic"
|
|
@@ -111,6 +118,29 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
|
|
|
111
118
|
except Exception:
|
|
112
119
|
ok = "error" if (status_code and str(status_code).endswith("ERROR")) else "ok"
|
|
113
120
|
|
|
121
|
+
# Rich error detail. The real exception class + stacktrace live in the OTel
|
|
122
|
+
# `exception` event (record_exception — emitted by the Anthropic/OpenAI/
|
|
123
|
+
# LangChain instrumentors), NOT span.status. The old bridge read only the
|
|
124
|
+
# status code and dropped all of it.
|
|
125
|
+
status_desc = getattr(status, "description", None)
|
|
126
|
+
exc_attrs: Dict[str, Any] = {}
|
|
127
|
+
for ev in (getattr(otel_span, "events", None) or []):
|
|
128
|
+
if getattr(ev, "name", None) == "exception":
|
|
129
|
+
exc_attrs = dict(getattr(ev, "attributes", {}) or {})
|
|
130
|
+
break
|
|
131
|
+
err_type = exc_attrs.get("exception.type") or attrs.get("exception.type") or attrs.get("error.type")
|
|
132
|
+
err_msg = exc_attrs.get("exception.message") or attrs.get("exception.message") or status_desc
|
|
133
|
+
err_stack = exc_attrs.get("exception.stacktrace") or attrs.get("exception.stacktrace")
|
|
134
|
+
status_code_val = (
|
|
135
|
+
attrs.get("http.response.status_code")
|
|
136
|
+
or attrs.get("http.status_code")
|
|
137
|
+
or attrs.get("gen_ai.response.status_code")
|
|
138
|
+
or attrs.get("error.code")
|
|
139
|
+
)
|
|
140
|
+
has_error = ok == "error" or err_type is not None
|
|
141
|
+
ok = "error" if has_error else "ok"
|
|
142
|
+
level = "error" if has_error else "default"
|
|
143
|
+
|
|
114
144
|
# Accept both stable and experimental GenAI semconv keys.
|
|
115
145
|
in_toks = (
|
|
116
146
|
attrs.get("gen_ai.usage.input_tokens")
|
|
@@ -138,6 +168,11 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
|
|
|
138
168
|
kind=kind,
|
|
139
169
|
name=getattr(otel_span, "name", kind),
|
|
140
170
|
status=ok,
|
|
171
|
+
level=level,
|
|
172
|
+
error_type=_trunc(err_type, 128) if err_type else None,
|
|
173
|
+
error_message=_trunc(err_msg, 4_000) if err_msg else None,
|
|
174
|
+
status_code=_trunc(status_code_val, 32) if status_code_val is not None else None,
|
|
175
|
+
stack_trace=_trunc(err_stack, 20_000) if err_stack else None,
|
|
141
176
|
started_at=started_at,
|
|
142
177
|
ended_at=ended_at,
|
|
143
178
|
duration_ms=duration_ms,
|
trodo/otel/helpers.py
CHANGED
|
@@ -413,6 +413,7 @@ def track_mcp(
|
|
|
413
413
|
"kind": "tool",
|
|
414
414
|
"name": f"tool.{tool}",
|
|
415
415
|
"status": status,
|
|
416
|
+
"level": "error" if error else "default",
|
|
416
417
|
"input": _stringify({"tool": tool, "params": input}) if input is not None else None,
|
|
417
418
|
"output": _stringify(output_to_record),
|
|
418
419
|
"tool_name": tool,
|
trodo/otel/processor.py
CHANGED
|
@@ -20,12 +20,18 @@ class TrodoRun:
|
|
|
20
20
|
conversation_id: Optional[str] = None
|
|
21
21
|
parent_run_id: Optional[str] = None
|
|
22
22
|
status: str = "ok" # 'running' | 'ok' | 'error'
|
|
23
|
+
# Severity (Langfuse parity): 'debug' | 'default' | 'warning' | 'error'.
|
|
24
|
+
# Optional — backend derives it from status when omitted.
|
|
25
|
+
level: Optional[str] = None
|
|
23
26
|
input: Optional[Union[str, Dict[str, Any]]] = None
|
|
24
27
|
output: Optional[Union[str, Dict[str, Any]]] = None
|
|
25
28
|
started_at: Optional[str] = None
|
|
26
29
|
ended_at: Optional[str] = None
|
|
27
30
|
duration_ms: Optional[int] = None
|
|
28
31
|
error_summary: Optional[str] = None
|
|
32
|
+
# Exception class of the run-level failure (runs previously only had the
|
|
33
|
+
# free-text error_summary).
|
|
34
|
+
error_type: Optional[str] = None
|
|
29
35
|
metadata: Optional[Dict[str, Any]] = None
|
|
30
36
|
# Aggregates summed from child spans at finalisation.
|
|
31
37
|
total_tokens_in: Optional[int] = None
|
|
@@ -47,6 +53,8 @@ class TrodoSpan:
|
|
|
47
53
|
kind: str = "generic" # 'llm' | 'tool' | 'agent' | 'retrieval' | 'generic'
|
|
48
54
|
name: str = ""
|
|
49
55
|
status: str = "ok"
|
|
56
|
+
# Severity (Langfuse parity): 'debug' | 'default' | 'warning' | 'error'.
|
|
57
|
+
level: Optional[str] = None
|
|
50
58
|
started_at: Optional[str] = None
|
|
51
59
|
ended_at: Optional[str] = None
|
|
52
60
|
duration_ms: Optional[int] = None
|
|
@@ -54,6 +62,10 @@ class TrodoSpan:
|
|
|
54
62
|
output: Optional[Union[str, Dict[str, Any]]] = None
|
|
55
63
|
error_type: Optional[str] = None
|
|
56
64
|
error_message: Optional[str] = None
|
|
65
|
+
# HTTP/provider status code (e.g. '429', 'rate_limit_exceeded').
|
|
66
|
+
status_code: Optional[str] = None
|
|
67
|
+
# Truncated exception stacktrace.
|
|
68
|
+
stack_trace: Optional[str] = None
|
|
57
69
|
model: Optional[str] = None
|
|
58
70
|
provider: Optional[str] = None
|
|
59
71
|
input_tokens: Optional[int] = None
|
trodo/otel/wrap_agent.py
CHANGED
|
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
import json
|
|
28
28
|
import time
|
|
29
|
+
import traceback
|
|
29
30
|
import uuid
|
|
30
31
|
from datetime import datetime, timezone
|
|
31
32
|
from typing import Any, Callable, Dict, Optional, Union
|
|
@@ -79,6 +80,54 @@ def _truncate(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[str]:
|
|
|
79
80
|
return s[:max_len] if len(s) > max_len else s
|
|
80
81
|
|
|
81
82
|
|
|
83
|
+
def describe_error(exc_type, exc, tb=None) -> Dict[str, Optional[str]]:
|
|
84
|
+
"""Extract rich error detail from a caught exception so spans carry error
|
|
85
|
+
TYPE, HTTP/provider STATUS CODE, and STACK TRACE — not just a message.
|
|
86
|
+
|
|
87
|
+
Works generically across provider SDKs: OpenAI (``exc.status`` /
|
|
88
|
+
``exc.code``), Anthropic (``exc.status``), httpx/requests
|
|
89
|
+
(``exc.response.status_code``), and stdlib errors (``exc.errno``). Never
|
|
90
|
+
raises. Returns keys: error_type, error_message, status_code, stack_trace,
|
|
91
|
+
level.
|
|
92
|
+
"""
|
|
93
|
+
if exc is None:
|
|
94
|
+
return {"error_type": None, "error_message": None, "status_code": None,
|
|
95
|
+
"stack_trace": None, "level": "error"}
|
|
96
|
+
error_type = getattr(exc_type, "__name__", None) or type(exc).__name__
|
|
97
|
+
error_message = _truncate(str(exc), 4_000)
|
|
98
|
+
|
|
99
|
+
status_code: Optional[str] = None
|
|
100
|
+
# HTTP status first (numeric), then a provider/system error code.
|
|
101
|
+
for attr in ("status", "status_code"):
|
|
102
|
+
v = getattr(exc, attr, None)
|
|
103
|
+
if v is not None:
|
|
104
|
+
status_code = str(v)[:32]
|
|
105
|
+
break
|
|
106
|
+
if status_code is None:
|
|
107
|
+
resp = getattr(exc, "response", None)
|
|
108
|
+
rc = getattr(resp, "status_code", None) if resp is not None else None
|
|
109
|
+
if rc is not None:
|
|
110
|
+
status_code = str(rc)[:32]
|
|
111
|
+
if status_code is None:
|
|
112
|
+
code = getattr(exc, "code", None) or getattr(exc, "errno", None)
|
|
113
|
+
if code is not None:
|
|
114
|
+
status_code = str(code)[:32]
|
|
115
|
+
|
|
116
|
+
stack_trace: Optional[str] = None
|
|
117
|
+
try:
|
|
118
|
+
stack_trace = _truncate("".join(traceback.format_exception(exc_type, exc, tb)), 20_000)
|
|
119
|
+
except Exception:
|
|
120
|
+
stack_trace = None
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"error_type": (error_type or "Error")[:128],
|
|
124
|
+
"error_message": error_message,
|
|
125
|
+
"status_code": status_code,
|
|
126
|
+
"stack_trace": stack_trace,
|
|
127
|
+
"level": "error",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
82
131
|
def _prepare_value(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[Union[str, Dict[str, Any]]]:
|
|
83
132
|
"""Prepare a value for storage in the JSONB input/output column.
|
|
84
133
|
|
|
@@ -204,6 +253,15 @@ class SpanHandle:
|
|
|
204
253
|
self.cost_details: Optional[Dict[str, float]] = None
|
|
205
254
|
self.temperature: Optional[float] = None
|
|
206
255
|
self.tool_name: Optional[str] = None
|
|
256
|
+
# Severity for this span (Langfuse parity). Leave None for the default
|
|
257
|
+
# behaviour (backend derives 'error' on a thrown exception, else
|
|
258
|
+
# 'default'). Set 'warning' for a recovered/retried step, 'debug' for
|
|
259
|
+
# verbose spans.
|
|
260
|
+
self.level: Optional[str] = None
|
|
261
|
+
|
|
262
|
+
def set_level(self, level: str) -> None:
|
|
263
|
+
"""Mark this span's severity (does not change ok/error status)."""
|
|
264
|
+
self.level = level
|
|
207
265
|
|
|
208
266
|
def set_input(self, value: Any) -> None:
|
|
209
267
|
self.input = _prepare_value(value)
|
|
@@ -419,9 +477,8 @@ class wrap_agent:
|
|
|
419
477
|
ended_iso = _now_iso()
|
|
420
478
|
duration_ms = int(time.time() * 1000.0 - self._started_ms)
|
|
421
479
|
status = "error" if exc is not None else "ok"
|
|
422
|
-
|
|
423
|
-
if
|
|
424
|
-
error_summary = _truncate(str(exc), 4_000)
|
|
480
|
+
einfo = describe_error(exc_type, exc, tb) if exc is not None else None
|
|
481
|
+
error_summary = einfo["error_message"] if einfo else None
|
|
425
482
|
|
|
426
483
|
pending = self._processor.get_pending(self.handle.run_id)
|
|
427
484
|
agg = _aggregate(pending)
|
|
@@ -433,12 +490,14 @@ class wrap_agent:
|
|
|
433
490
|
conversation_id=self._conversation_id,
|
|
434
491
|
parent_run_id=self._parent_run_id,
|
|
435
492
|
status=status,
|
|
493
|
+
level="error" if exc is not None else None,
|
|
436
494
|
input=self.handle.input,
|
|
437
495
|
output=self.handle.output,
|
|
438
496
|
started_at=self._started_iso,
|
|
439
497
|
ended_at=ended_iso,
|
|
440
498
|
duration_ms=duration_ms,
|
|
441
499
|
error_summary=error_summary,
|
|
500
|
+
error_type=einfo["error_type"] if einfo else None,
|
|
442
501
|
metadata={**(self._metadata or {}), **self.handle.metadata} or None,
|
|
443
502
|
total_tokens_in=agg["total_tokens_in"],
|
|
444
503
|
total_tokens_out=agg["total_tokens_out"],
|
|
@@ -584,8 +643,8 @@ class join_run:
|
|
|
584
643
|
ended_iso = _now_iso()
|
|
585
644
|
duration_ms = int(time.time() * 1000.0 - self._started_ms)
|
|
586
645
|
status = "error" if exc is not None else "ok"
|
|
587
|
-
|
|
588
|
-
|
|
646
|
+
einfo = describe_error(exc_type, exc, tb) if exc is not None else None
|
|
647
|
+
level = self.handle.level or ("error" if exc is not None else None)
|
|
589
648
|
|
|
590
649
|
trodo_span = TrodoSpan(
|
|
591
650
|
span_id=self._span_id,
|
|
@@ -594,13 +653,16 @@ class join_run:
|
|
|
594
653
|
kind=self._kind,
|
|
595
654
|
name=self._name,
|
|
596
655
|
status=status,
|
|
656
|
+
level=level,
|
|
597
657
|
started_at=self._started_iso,
|
|
598
658
|
ended_at=ended_iso,
|
|
599
659
|
duration_ms=duration_ms,
|
|
600
660
|
input=self.handle.input,
|
|
601
661
|
output=self.handle.output,
|
|
602
|
-
error_type=error_type,
|
|
603
|
-
error_message=error_message,
|
|
662
|
+
error_type=einfo["error_type"] if einfo else None,
|
|
663
|
+
error_message=einfo["error_message"] if einfo else None,
|
|
664
|
+
status_code=einfo["status_code"] if einfo else None,
|
|
665
|
+
stack_trace=einfo["stack_trace"] if einfo else None,
|
|
604
666
|
model=self.handle.model,
|
|
605
667
|
provider=self.handle.provider,
|
|
606
668
|
input_tokens=self.handle.input_tokens,
|
|
@@ -676,8 +738,8 @@ class span:
|
|
|
676
738
|
ended_iso = _now_iso()
|
|
677
739
|
duration_ms = int(time.time() * 1000.0 - self._started_ms)
|
|
678
740
|
status = "error" if exc is not None else "ok"
|
|
679
|
-
|
|
680
|
-
|
|
741
|
+
einfo = describe_error(exc_type, exc, tb) if exc is not None else None
|
|
742
|
+
level = self.handle.level or ("error" if exc is not None else None)
|
|
681
743
|
|
|
682
744
|
trodo_span = TrodoSpan(
|
|
683
745
|
span_id=self._span_id,
|
|
@@ -686,13 +748,16 @@ class span:
|
|
|
686
748
|
kind=self._kind,
|
|
687
749
|
name=self._name,
|
|
688
750
|
status=status,
|
|
751
|
+
level=level,
|
|
689
752
|
started_at=self._started_iso,
|
|
690
753
|
ended_at=ended_iso,
|
|
691
754
|
duration_ms=duration_ms,
|
|
692
755
|
input=self.handle.input,
|
|
693
756
|
output=self.handle.output,
|
|
694
|
-
error_type=error_type,
|
|
695
|
-
error_message=error_message,
|
|
757
|
+
error_type=einfo["error_type"] if einfo else None,
|
|
758
|
+
error_message=einfo["error_message"] if einfo else None,
|
|
759
|
+
status_code=einfo["status_code"] if einfo else None,
|
|
760
|
+
stack_trace=einfo["stack_trace"] if einfo else None,
|
|
696
761
|
model=self.handle.model,
|
|
697
762
|
provider=self.handle.provider,
|
|
698
763
|
input_tokens=self.handle.input_tokens,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
trodo/__init__.py,sha256=
|
|
1
|
+
trodo/__init__.py,sha256=p9NGic9WzF53OYKtk-MSdQNcCTbXq8DNZeXYWl9OCUo,16678
|
|
2
2
|
trodo/client.py,sha256=8DsKoLh_eaNxj93qkHynfeee-QsdomB_kXfUQjGnWDk,18607
|
|
3
3
|
trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
|
|
4
4
|
trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
|
|
@@ -12,20 +12,20 @@ trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
12
12
|
trodo/managers/group_manager.py,sha256=ki3Se3qEoSZfREX63oeDeBmEfZF-ISHLE8azEtLg0tM,3542
|
|
13
13
|
trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8pK1E,2882
|
|
14
14
|
trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
|
|
15
|
-
trodo/otel/auto_instrument.py,sha256=
|
|
15
|
+
trodo/otel/auto_instrument.py,sha256=gym90cYD6NzVDVsNtUjKHdKANy17t4AnU4lYGEGHyo8,12866
|
|
16
16
|
trodo/otel/context.py,sha256=iJ1rE42-SbO8VZHAxhIl2ZJXgNwLIVps5xLg8GKgfFc,1165
|
|
17
|
-
trodo/otel/helpers.py,sha256=
|
|
18
|
-
trodo/otel/processor.py,sha256=
|
|
17
|
+
trodo/otel/helpers.py,sha256=4HsjMOrE-7zuvaRSiGXxV7ZyfXQ5gLxtR3HdpLut9sk,20054
|
|
18
|
+
trodo/otel/processor.py,sha256=aqcTmzTw9cESgIp829pu_XCa5_dG_2MaeJNsqJZeqQU,7495
|
|
19
19
|
trodo/otel/register.py,sha256=YV2EnkUoa-_54YAuChOe-Mg28UUKg8JO7-qhVP9G6u4,7644
|
|
20
20
|
trodo/otel/transport.py,sha256=hzZz8gwSMGJ8CxdijmLn1Ljt18owr9XTWy13DLbwYbw,2441
|
|
21
|
-
trodo/otel/wrap_agent.py,sha256=
|
|
21
|
+
trodo/otel/wrap_agent.py,sha256=Nvjj8CjIGHqfWNV_heHno9HKJbKU3lHqHInX1EyRhnw,30334
|
|
22
22
|
trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
trodo/queue/batch_flusher.py,sha256=4Lg6T3Urwi9U0Q4FpFGPmjDYKg4ZliCTR-ND8BJvWaY,1298
|
|
24
24
|
trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,872
|
|
25
25
|
trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrTjs,2136
|
|
27
27
|
trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
|
|
28
|
-
trodo_python-2.
|
|
29
|
-
trodo_python-2.
|
|
30
|
-
trodo_python-2.
|
|
31
|
-
trodo_python-2.
|
|
28
|
+
trodo_python-2.9.0.dist-info/METADATA,sha256=Ab8QIUgDMayOyTjdtrdFP_EMk5bCcLHtX4hZJaaKEac,20482
|
|
29
|
+
trodo_python-2.9.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
30
|
+
trodo_python-2.9.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
|
|
31
|
+
trodo_python-2.9.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|