trodo-python 2.9.0__tar.gz → 2.10.2__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.
- {trodo_python-2.9.0 → trodo_python-2.10.2}/PKG-INFO +1 -1
- {trodo_python-2.9.0 → trodo_python-2.10.2}/pyproject.toml +1 -1
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_error_enrichment.py +23 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/__init__.py +1 -1
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/auto_instrument.py +17 -4
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/wrap_agent.py +99 -19
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo_python.egg-info/PKG-INFO +1 -1
- {trodo_python-2.9.0 → trodo_python-2.10.2}/README.md +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/setup.cfg +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_anon_distinct_id 2.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_anon_distinct_id.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_auto_instrument_fixes.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_end_run.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_llm_usage_cost.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_register_otel.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_start_run.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/tests/test_wrap_agent_unchanged.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/api/__init__.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/api/async_client.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/api/endpoints.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/api/http_client.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/client.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/context.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/helpers.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/processor.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/register.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/otel/transport.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/session/__init__.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/session/server_session.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/types.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo/user_context.py +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo_python.egg-info/SOURCES.txt +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo_python.egg-info/requires.txt +0 -0
- {trodo_python-2.9.0 → trodo_python-2.10.2}/trodo_python.egg-info/top_level.txt +0 -0
|
@@ -70,6 +70,29 @@ def test_errored_span_and_run_carry_rich_error_fields(processor, http):
|
|
|
70
70
|
assert "rate limit exceeded" in run_payload["error_summary"]
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def test_set_error_records_without_raising(processor, http):
|
|
74
|
+
with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat") as run:
|
|
75
|
+
with span("soft-step", kind="tool") as s:
|
|
76
|
+
s.set_error("bad input", type="ValidationError", status_code=422)
|
|
77
|
+
# no raise — the span still finalises as error
|
|
78
|
+
run.set_error_summary("partial failure", type="PartialFailure")
|
|
79
|
+
|
|
80
|
+
ingest = http.run_ingest[0]
|
|
81
|
+
run_payload = ingest["run"]
|
|
82
|
+
s = next(x for x in ingest.get("spans", []) if x["name"] == "soft-step")
|
|
83
|
+
|
|
84
|
+
assert s["status"] == "error"
|
|
85
|
+
assert s["error_type"] == "ValidationError"
|
|
86
|
+
assert s["error_message"] == "bad input"
|
|
87
|
+
assert s["status_code"] == "422"
|
|
88
|
+
assert s["level"] == "error"
|
|
89
|
+
|
|
90
|
+
assert run_payload["status"] == "error"
|
|
91
|
+
assert run_payload["error_summary"] == "partial failure"
|
|
92
|
+
assert run_payload["error_type"] == "PartialFailure"
|
|
93
|
+
assert run_payload["level"] == "error"
|
|
94
|
+
|
|
95
|
+
|
|
73
96
|
def test_healthy_span_omits_error_fields(processor, http):
|
|
74
97
|
with wrap_agent(processor=processor, team_site_id="site-x", agent_name="chat"):
|
|
75
98
|
with span("ok-step", kind="generic"):
|
|
@@ -263,11 +263,24 @@ def _register_instrumentors() -> None:
|
|
|
263
263
|
LlamaIndexInstrumentor().instrument()
|
|
264
264
|
|
|
265
265
|
def _google_generativeai() -> Any:
|
|
266
|
-
|
|
267
|
-
|
|
266
|
+
import opentelemetry.instrumentation.google_generativeai as _m # type: ignore
|
|
267
|
+
|
|
268
|
+
# The exported class name varies by version: GoogleGenerativeAiInstrumentor
|
|
269
|
+
# (current — note the lowercase 'i'), GoogleGenerativeAIInstrumentor, or
|
|
270
|
+
# GenAIInstrumentor. Importing the wrong casing silently disabled Gemini
|
|
271
|
+
# auto-instrumentation. (This instrumentation patches the new @google/genai
|
|
272
|
+
# SDK, i.e. the `google-genai` package, not legacy google-generativeai.)
|
|
273
|
+
instr = (
|
|
274
|
+
getattr(_m, "GoogleGenerativeAiInstrumentor", None)
|
|
275
|
+
or getattr(_m, "GoogleGenerativeAIInstrumentor", None)
|
|
276
|
+
or getattr(_m, "GenAIInstrumentor", None)
|
|
268
277
|
)
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
if instr is None:
|
|
279
|
+
raise ImportError(
|
|
280
|
+
"no GoogleGenerativeAi/GenAI Instrumentor in "
|
|
281
|
+
"opentelemetry.instrumentation.google_generativeai"
|
|
282
|
+
)
|
|
283
|
+
instr().instrument()
|
|
271
284
|
|
|
272
285
|
def _vertexai() -> Any:
|
|
273
286
|
from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor # type: ignore
|
|
@@ -128,6 +128,27 @@ def describe_error(exc_type, exc, tb=None) -> Dict[str, Optional[str]]:
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _resolve_error(handle, exc_type, exc, tb) -> Dict[str, Optional[str]]:
|
|
132
|
+
"""Resolve a span's final error state from either a raised exception (wins)
|
|
133
|
+
or a manual ``handle.set_error(...)``. Returns describe_error's keys plus a
|
|
134
|
+
``status`` of 'ok'|'error'."""
|
|
135
|
+
if exc is not None:
|
|
136
|
+
info = describe_error(exc_type, exc, tb)
|
|
137
|
+
info["status"] = "error"
|
|
138
|
+
return info
|
|
139
|
+
if handle is not None and getattr(handle, "has_error", False):
|
|
140
|
+
return {
|
|
141
|
+
"status": "error",
|
|
142
|
+
"error_type": handle.error_type,
|
|
143
|
+
"error_message": handle.error_message,
|
|
144
|
+
"status_code": handle.status_code,
|
|
145
|
+
"stack_trace": handle.stack_trace,
|
|
146
|
+
"level": "error",
|
|
147
|
+
}
|
|
148
|
+
return {"status": "ok", "error_type": None, "error_message": None,
|
|
149
|
+
"status_code": None, "stack_trace": None, "level": None}
|
|
150
|
+
|
|
151
|
+
|
|
131
152
|
def _prepare_value(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[Union[str, Dict[str, Any]]]:
|
|
132
153
|
"""Prepare a value for storage in the JSONB input/output column.
|
|
133
154
|
|
|
@@ -221,6 +242,10 @@ class RunHandle:
|
|
|
221
242
|
self.input: Optional[Union[str, Dict[str, Any]]] = None
|
|
222
243
|
self.output: Optional[Union[str, Dict[str, Any]]] = None
|
|
223
244
|
self.metadata: Dict[str, Any] = {}
|
|
245
|
+
# Manually-recorded run-level error (via set_error_summary). When set
|
|
246
|
+
# without a raised exception, the run finalises as ``error``.
|
|
247
|
+
self.error_summary: Optional[str] = None
|
|
248
|
+
self.error_type: Optional[str] = None
|
|
224
249
|
|
|
225
250
|
def set_input(self, value: Any) -> None:
|
|
226
251
|
self.input = _prepare_value(value)
|
|
@@ -231,6 +256,15 @@ class RunHandle:
|
|
|
231
256
|
def set_metadata(self, **kwargs: Any) -> None:
|
|
232
257
|
self.metadata.update(kwargs)
|
|
233
258
|
|
|
259
|
+
def set_error_summary(self, summary: str, *, type: Optional[str] = None) -> None:
|
|
260
|
+
"""Mark the run as errored without raising — e.g. a soft failure the
|
|
261
|
+
agent recovered from but you still want recorded. Sets ``status='error'``
|
|
262
|
+
and ``error_summary`` on the run at finalisation.
|
|
263
|
+
"""
|
|
264
|
+
self.error_summary = _truncate(summary, 4_000)
|
|
265
|
+
if type:
|
|
266
|
+
self.error_type = str(type)[:128]
|
|
267
|
+
|
|
234
268
|
|
|
235
269
|
class SpanHandle:
|
|
236
270
|
"""Handle returned by span context manager for setting output/attrs."""
|
|
@@ -258,11 +292,44 @@ class SpanHandle:
|
|
|
258
292
|
# 'default'). Set 'warning' for a recovered/retried step, 'debug' for
|
|
259
293
|
# verbose spans.
|
|
260
294
|
self.level: Optional[str] = None
|
|
295
|
+
# Manually-recorded error (via set_error) — applied at span close even
|
|
296
|
+
# when the body didn't raise. A raised exception still wins over these.
|
|
297
|
+
self.error_type: Optional[str] = None
|
|
298
|
+
self.error_message: Optional[str] = None
|
|
299
|
+
self.status_code: Optional[str] = None
|
|
300
|
+
self.stack_trace: Optional[str] = None
|
|
261
301
|
|
|
262
302
|
def set_level(self, level: str) -> None:
|
|
263
303
|
"""Mark this span's severity (does not change ok/error status)."""
|
|
264
304
|
self.level = level
|
|
265
305
|
|
|
306
|
+
def set_error(
|
|
307
|
+
self,
|
|
308
|
+
message: str,
|
|
309
|
+
*,
|
|
310
|
+
type: Optional[str] = None,
|
|
311
|
+
status_code: Optional[Any] = None,
|
|
312
|
+
stack_trace: Optional[str] = None,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Record an error on this span without raising — for when you catch an
|
|
315
|
+
exception to recover but still want it captured. Sets ``status='error'``
|
|
316
|
+
plus ``error_type`` / ``error_message`` (and optional ``status_code``),
|
|
317
|
+
and defaults the severity to ``error``.
|
|
318
|
+
"""
|
|
319
|
+
self.error_message = _truncate(message, 4_000)
|
|
320
|
+
self.error_type = str(type or "Error")[:128]
|
|
321
|
+
if status_code is not None and status_code != "":
|
|
322
|
+
self.status_code = str(status_code)[:32]
|
|
323
|
+
if stack_trace:
|
|
324
|
+
self.stack_trace = _truncate(stack_trace, 20_000)
|
|
325
|
+
if self.level is None:
|
|
326
|
+
self.level = "error"
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def has_error(self) -> bool:
|
|
330
|
+
"""True when set_error was called (span should finalise as error)."""
|
|
331
|
+
return self.error_message is not None or self.error_type is not None
|
|
332
|
+
|
|
266
333
|
def set_input(self, value: Any) -> None:
|
|
267
334
|
self.input = _prepare_value(value)
|
|
268
335
|
|
|
@@ -476,9 +543,22 @@ class wrap_agent:
|
|
|
476
543
|
assert self.handle is not None
|
|
477
544
|
ended_iso = _now_iso()
|
|
478
545
|
duration_ms = int(time.time() * 1000.0 - self._started_ms)
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
546
|
+
# Error precedence: a raised exception wins; otherwise a manual
|
|
547
|
+
# run.set_error_summary(...) marks the run errored without raising.
|
|
548
|
+
manual_run_error = self.handle.error_summary is not None
|
|
549
|
+
if exc is not None:
|
|
550
|
+
status = "error"
|
|
551
|
+
einfo = describe_error(exc_type, exc, tb)
|
|
552
|
+
error_summary = einfo["error_message"]
|
|
553
|
+
error_type = einfo["error_type"]
|
|
554
|
+
elif manual_run_error:
|
|
555
|
+
status = "error"
|
|
556
|
+
error_summary = self.handle.error_summary
|
|
557
|
+
error_type = self.handle.error_type
|
|
558
|
+
else:
|
|
559
|
+
status = "ok"
|
|
560
|
+
error_summary = None
|
|
561
|
+
error_type = None
|
|
482
562
|
|
|
483
563
|
pending = self._processor.get_pending(self.handle.run_id)
|
|
484
564
|
agg = _aggregate(pending)
|
|
@@ -490,14 +570,14 @@ class wrap_agent:
|
|
|
490
570
|
conversation_id=self._conversation_id,
|
|
491
571
|
parent_run_id=self._parent_run_id,
|
|
492
572
|
status=status,
|
|
493
|
-
level="error" if
|
|
573
|
+
level="error" if status == "error" else None,
|
|
494
574
|
input=self.handle.input,
|
|
495
575
|
output=self.handle.output,
|
|
496
576
|
started_at=self._started_iso,
|
|
497
577
|
ended_at=ended_iso,
|
|
498
578
|
duration_ms=duration_ms,
|
|
499
579
|
error_summary=error_summary,
|
|
500
|
-
error_type=
|
|
580
|
+
error_type=error_type,
|
|
501
581
|
metadata={**(self._metadata or {}), **self.handle.metadata} or None,
|
|
502
582
|
total_tokens_in=agg["total_tokens_in"],
|
|
503
583
|
total_tokens_out=agg["total_tokens_out"],
|
|
@@ -642,9 +722,9 @@ class join_run:
|
|
|
642
722
|
return None
|
|
643
723
|
ended_iso = _now_iso()
|
|
644
724
|
duration_ms = int(time.time() * 1000.0 - self._started_ms)
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
level = self.handle.level or
|
|
725
|
+
einfo = _resolve_error(self.handle, exc_type, exc, tb)
|
|
726
|
+
status = einfo["status"]
|
|
727
|
+
level = self.handle.level or einfo["level"]
|
|
648
728
|
|
|
649
729
|
trodo_span = TrodoSpan(
|
|
650
730
|
span_id=self._span_id,
|
|
@@ -659,10 +739,10 @@ class join_run:
|
|
|
659
739
|
duration_ms=duration_ms,
|
|
660
740
|
input=self.handle.input,
|
|
661
741
|
output=self.handle.output,
|
|
662
|
-
error_type=einfo["error_type"]
|
|
663
|
-
error_message=einfo["error_message"]
|
|
664
|
-
status_code=einfo["status_code"]
|
|
665
|
-
stack_trace=einfo["stack_trace"]
|
|
742
|
+
error_type=einfo["error_type"],
|
|
743
|
+
error_message=einfo["error_message"],
|
|
744
|
+
status_code=einfo["status_code"],
|
|
745
|
+
stack_trace=einfo["stack_trace"],
|
|
666
746
|
model=self.handle.model,
|
|
667
747
|
provider=self.handle.provider,
|
|
668
748
|
input_tokens=self.handle.input_tokens,
|
|
@@ -737,9 +817,9 @@ class span:
|
|
|
737
817
|
return None
|
|
738
818
|
ended_iso = _now_iso()
|
|
739
819
|
duration_ms = int(time.time() * 1000.0 - self._started_ms)
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
level = self.handle.level or
|
|
820
|
+
einfo = _resolve_error(self.handle, exc_type, exc, tb)
|
|
821
|
+
status = einfo["status"]
|
|
822
|
+
level = self.handle.level or einfo["level"]
|
|
743
823
|
|
|
744
824
|
trodo_span = TrodoSpan(
|
|
745
825
|
span_id=self._span_id,
|
|
@@ -754,10 +834,10 @@ class span:
|
|
|
754
834
|
duration_ms=duration_ms,
|
|
755
835
|
input=self.handle.input,
|
|
756
836
|
output=self.handle.output,
|
|
757
|
-
error_type=einfo["error_type"]
|
|
758
|
-
error_message=einfo["error_message"]
|
|
759
|
-
status_code=einfo["status_code"]
|
|
760
|
-
stack_trace=einfo["stack_trace"]
|
|
837
|
+
error_type=einfo["error_type"],
|
|
838
|
+
error_message=einfo["error_message"],
|
|
839
|
+
status_code=einfo["status_code"],
|
|
840
|
+
stack_trace=einfo["stack_trace"],
|
|
761
841
|
model=self.handle.model,
|
|
762
842
|
provider=self.handle.provider,
|
|
763
843
|
input_tokens=self.handle.input_tokens,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|