trodo-python 2.10.2__tar.gz → 2.10.4__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.10.2 → trodo_python-2.10.4}/PKG-INFO +1 -1
- {trodo_python-2.10.2 → trodo_python-2.10.4}/pyproject.toml +1 -1
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/__init__.py +1 -1
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/client.py +9 -1
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/auto_instrument.py +22 -2
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/register.py +44 -2
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/wrap_agent.py +95 -4
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/PKG-INFO +1 -1
- {trodo_python-2.10.2 → trodo_python-2.10.4}/README.md +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/setup.cfg +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_anon_distinct_id 2.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_anon_distinct_id.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_auto_instrument_fixes.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_end_run.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_error_enrichment.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_llm_usage_cost.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_register_otel.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_start_run.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/tests/test_wrap_agent_unchanged.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/__init__.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/async_client.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/endpoints.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/api/http_client.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/context.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/helpers.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/processor.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/otel/transport.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/session/__init__.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/session/server_session.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/types.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo/user_context.py +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/SOURCES.txt +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/requires.txt +0 -0
- {trodo_python-2.10.2 → trodo_python-2.10.4}/trodo_python.egg-info/top_level.txt +0 -0
|
@@ -23,7 +23,11 @@ from .otel.wrap_agent import (
|
|
|
23
23
|
current_span_id as _current_span_id,
|
|
24
24
|
)
|
|
25
25
|
from .otel.auto_instrument import enable_auto_instrument
|
|
26
|
-
from .otel.register import
|
|
26
|
+
from .otel.register import (
|
|
27
|
+
register_otel as _register_otel,
|
|
28
|
+
flush_otlp_pipeline as _flush_otlp_pipeline,
|
|
29
|
+
shutdown_otlp_pipeline as _shutdown_otlp_pipeline,
|
|
30
|
+
)
|
|
27
31
|
from .otel.helpers import (
|
|
28
32
|
tool as tool_decorator,
|
|
29
33
|
track_llm_call as track_llm_call_fn,
|
|
@@ -233,6 +237,9 @@ class TrodoClient:
|
|
|
233
237
|
if self._batch_flusher:
|
|
234
238
|
self._batch_flusher.flush()
|
|
235
239
|
self._span_processor.force_flush()
|
|
240
|
+
# 'otlp' mode: drain the OTel BatchSpanProcessor too — it batches for
|
|
241
|
+
# up to 5s, so without this a short-lived process drops its spans.
|
|
242
|
+
_flush_otlp_pipeline()
|
|
236
243
|
|
|
237
244
|
def shutdown(self) -> None:
|
|
238
245
|
self._auto_event_manager.disable()
|
|
@@ -240,6 +247,7 @@ class TrodoClient:
|
|
|
240
247
|
self._batch_flusher.stop()
|
|
241
248
|
self._batch_flusher.flush()
|
|
242
249
|
self._span_processor.shutdown()
|
|
250
|
+
_shutdown_otlp_pipeline()
|
|
243
251
|
|
|
244
252
|
# --------------------------------------------------------------------------
|
|
245
253
|
# Agent Runs — Lemma-style seamless wrapping
|
|
@@ -9,12 +9,25 @@ lazily and skip anything that isn't installed.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import sys
|
|
12
13
|
from datetime import datetime, timezone
|
|
13
14
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
14
15
|
|
|
15
16
|
from .context import get_active_context
|
|
16
17
|
from .processor import TrodoSpan, TrodoSpanProcessor
|
|
17
18
|
|
|
19
|
+
# Always-on, one-shot warnings. Silent skips (missing opentelemetry-sdk) are the
|
|
20
|
+
# #1 cause of "I installed Trodo but see no LLM spans" — surface them even
|
|
21
|
+
# without debug, matching the Node SDK's warnOnce behaviour.
|
|
22
|
+
_WARNED_ONCE: set = set()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _warn_once(key: str, msg: str) -> None:
|
|
26
|
+
if key in _WARNED_ONCE:
|
|
27
|
+
return
|
|
28
|
+
_WARNED_ONCE.add(key)
|
|
29
|
+
sys.stderr.write(f"[trodo-python] {msg}\n")
|
|
30
|
+
|
|
18
31
|
|
|
19
32
|
def _hr_to_iso(nanos: Optional[int]) -> Optional[str]:
|
|
20
33
|
if nanos is None:
|
|
@@ -341,14 +354,21 @@ def enable_auto_instrument(
|
|
|
341
354
|
"""Register OTel auto-instrumentations for installed packages.
|
|
342
355
|
|
|
343
356
|
Returns the list of framework ids that were actually instrumented.
|
|
344
|
-
|
|
345
|
-
instrumentation
|
|
357
|
+
Emits a one-shot stderr warning if opentelemetry-sdk isn't installed (the
|
|
358
|
+
#1 silent-failure mode); per-framework instrumentation packages that aren't
|
|
359
|
+
installed are skipped silently.
|
|
346
360
|
"""
|
|
347
361
|
disabled = set(disable or [])
|
|
348
362
|
try:
|
|
349
363
|
from opentelemetry import trace # type: ignore
|
|
350
364
|
from opentelemetry.sdk.trace import TracerProvider # type: ignore
|
|
351
365
|
except Exception:
|
|
366
|
+
_warn_once(
|
|
367
|
+
"otel-sdk-missing",
|
|
368
|
+
"auto-instrument: opentelemetry-sdk is not installed — no LLM spans "
|
|
369
|
+
"will be auto-captured. Install: pip install opentelemetry-sdk "
|
|
370
|
+
"opentelemetry-api (plus opentelemetry-instrumentation-<provider> per framework).",
|
|
371
|
+
)
|
|
352
372
|
return []
|
|
353
373
|
|
|
354
374
|
provider = trace.get_tracer_provider()
|
|
@@ -36,6 +36,33 @@ class RegisterOtelResult:
|
|
|
36
36
|
|
|
37
37
|
_registered: Optional[RegisterOtelResult] = None
|
|
38
38
|
|
|
39
|
+
# Flush/shutdown handle for the 'otlp'-mode pipeline. Without this, the
|
|
40
|
+
# BatchSpanProcessor created in _setup_otlp_mode (default 5s schedule) is
|
|
41
|
+
# unreachable — trodo.flush()/shutdown() only drained the events batcher and
|
|
42
|
+
# TrodoSpanProcessor, so short-lived processes silently dropped every
|
|
43
|
+
# OTLP-exported span. TrodoClient.flush()/shutdown() call these.
|
|
44
|
+
_otlp_processor: Optional[Any] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def flush_otlp_pipeline() -> None:
|
|
48
|
+
"""Force-flush the 'otlp'-mode export pipeline. No-op in 'trodo' mode."""
|
|
49
|
+
if _otlp_processor is not None:
|
|
50
|
+
try:
|
|
51
|
+
_otlp_processor.force_flush()
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def shutdown_otlp_pipeline() -> None:
|
|
57
|
+
"""Shut down the 'otlp'-mode export pipeline (flushes first). No-op in 'trodo' mode."""
|
|
58
|
+
global _otlp_processor
|
|
59
|
+
if _otlp_processor is not None:
|
|
60
|
+
proc, _otlp_processor = _otlp_processor, None
|
|
61
|
+
try:
|
|
62
|
+
proc.shutdown()
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
39
66
|
|
|
40
67
|
def register_otel(
|
|
41
68
|
*,
|
|
@@ -86,8 +113,9 @@ def register_otel(
|
|
|
86
113
|
|
|
87
114
|
def _reset_for_tests() -> None:
|
|
88
115
|
"""Reset internal state — for tests only. Not exported from __init__.py."""
|
|
89
|
-
global _registered
|
|
116
|
+
global _registered, _otlp_processor
|
|
90
117
|
_registered = None
|
|
118
|
+
_otlp_processor = None
|
|
91
119
|
_reset_transport_for_tests()
|
|
92
120
|
|
|
93
121
|
|
|
@@ -131,6 +159,11 @@ def _setup_otlp_mode(
|
|
|
131
159
|
headers={"Authorization": f"Bearer {site_id}"},
|
|
132
160
|
)
|
|
133
161
|
batch_processor = BatchSpanProcessor(exporter)
|
|
162
|
+
# Expose flush/shutdown so trodo.flush()/shutdown() can drain the batch —
|
|
163
|
+
# BatchSpanProcessor holds spans for up to 5s by default, so without this
|
|
164
|
+
# a short-lived process exits before the exporter ever sends them.
|
|
165
|
+
global _otlp_processor
|
|
166
|
+
_otlp_processor = batch_processor
|
|
134
167
|
|
|
135
168
|
# Coexistence: if a non-default TracerProvider is already registered,
|
|
136
169
|
# attach our exporter to it instead of replacing it.
|
|
@@ -162,7 +195,16 @@ def _setup_otlp_mode(
|
|
|
162
195
|
active = _build_instrumentations(disable)
|
|
163
196
|
|
|
164
197
|
# Activate the OTel transport so wrap_agent / span dispatch via tracer.
|
|
165
|
-
|
|
198
|
+
# Pass the use_span/Status/StatusCode helpers (same as the coexistence
|
|
199
|
+
# branch above) — without use_span, wrap_agent's root span is never made
|
|
200
|
+
# current, so trodo.span() children and auto-instrumented spans can't
|
|
201
|
+
# join its trace, and error status can never be stamped.
|
|
202
|
+
set_otel_transport(
|
|
203
|
+
otel_trace.get_tracer("trodo-agent"),
|
|
204
|
+
use_span=use_span,
|
|
205
|
+
status_cls=Status,
|
|
206
|
+
status_code=StatusCode,
|
|
207
|
+
)
|
|
166
208
|
return active
|
|
167
209
|
|
|
168
210
|
|
|
@@ -253,8 +253,14 @@ class RunHandle:
|
|
|
253
253
|
def set_output(self, value: Any) -> None:
|
|
254
254
|
self.output = _prepare_value(value)
|
|
255
255
|
|
|
256
|
-
def set_metadata(self, **kwargs: Any) -> None:
|
|
257
|
-
|
|
256
|
+
def set_metadata(self, metadata: Optional[Dict[str, Any]] = None, **kwargs: Any) -> None:
|
|
257
|
+
# Accept a dict (Node parity: setMetadata({...})) and/or keyword args
|
|
258
|
+
# (set_metadata(key=value)). Previously kwargs-only, which broke code
|
|
259
|
+
# copied from the Node SDK.
|
|
260
|
+
if metadata:
|
|
261
|
+
self.metadata.update(metadata)
|
|
262
|
+
if kwargs:
|
|
263
|
+
self.metadata.update(kwargs)
|
|
258
264
|
|
|
259
265
|
def set_error_summary(self, summary: str, *, type: Optional[str] = None) -> None:
|
|
260
266
|
"""Mark the run as errored without raising — e.g. a soft failure the
|
|
@@ -636,10 +642,12 @@ class wrap_agent:
|
|
|
636
642
|
assert self._otel_span is not None
|
|
637
643
|
otel_span = self._otel_span
|
|
638
644
|
try:
|
|
645
|
+
# OTel attribute values must be primitives — dicts/lists are
|
|
646
|
+
# rejected (with a warning) and silently dropped. Serialize.
|
|
639
647
|
if self.handle.input is not None:
|
|
640
|
-
otel_span.set_attribute("ai.prompt", self.handle.input)
|
|
648
|
+
otel_span.set_attribute("ai.prompt", _serialize_attr(self.handle.input))
|
|
641
649
|
if self.handle.output is not None:
|
|
642
|
-
otel_span.set_attribute("ai.response.text", self.handle.output)
|
|
650
|
+
otel_span.set_attribute("ai.response.text", _serialize_attr(self.handle.output))
|
|
643
651
|
for k, v in self.handle.metadata.items():
|
|
644
652
|
otel_span.set_attribute(f"trodo.metadata.{k}", _serialize_attr(v))
|
|
645
653
|
if exc is not None:
|
|
@@ -787,8 +795,17 @@ class span:
|
|
|
787
795
|
self._active: Optional[ActiveSpanContext] = None
|
|
788
796
|
self._span_id: str = ""
|
|
789
797
|
self.handle: Optional[SpanHandle] = None
|
|
798
|
+
# 'otlp' transport state — populated only when register_otel(mode='otlp').
|
|
799
|
+
self._otel_span: Optional[Any] = None
|
|
800
|
+
self._otel_cm: Optional[Any] = None
|
|
790
801
|
|
|
791
802
|
def __enter__(self) -> SpanHandle:
|
|
803
|
+
# Dispatch: 'otlp' transport routes through the OTel tracer so this
|
|
804
|
+
# span joins the wrap_agent root's trace (mirrors Node withSpanOtel).
|
|
805
|
+
# Without this branch, otlp-mode children hit the "no active run"
|
|
806
|
+
# no-op below and were silently dropped.
|
|
807
|
+
if get_transport_mode() == "otlp" and get_otel_tracer() is not None:
|
|
808
|
+
return self._enter_otel()
|
|
792
809
|
self._active = get_active_context()
|
|
793
810
|
self._span_id = str(uuid.uuid4())
|
|
794
811
|
self.handle = SpanHandle(self._span_id, self._name)
|
|
@@ -813,6 +830,8 @@ class span:
|
|
|
813
830
|
return self.handle
|
|
814
831
|
|
|
815
832
|
def __exit__(self, exc_type, exc, tb) -> None:
|
|
833
|
+
if self._otel_span is not None:
|
|
834
|
+
return self._exit_otel(exc_type, exc, tb)
|
|
816
835
|
if self._active is None or self.handle is None:
|
|
817
836
|
return None
|
|
818
837
|
ended_iso = _now_iso()
|
|
@@ -854,3 +873,75 @@ class span:
|
|
|
854
873
|
if self._ctx_mgr is not None:
|
|
855
874
|
self._ctx_mgr.__exit__(exc_type, exc, tb)
|
|
856
875
|
return None
|
|
876
|
+
|
|
877
|
+
# ----------------------------------------------------------------------
|
|
878
|
+
# 'otlp' transport — OTel tracer path (mirrors Node withSpanOtel).
|
|
879
|
+
# ----------------------------------------------------------------------
|
|
880
|
+
|
|
881
|
+
def _enter_otel(self) -> SpanHandle:
|
|
882
|
+
tracer = get_otel_tracer()
|
|
883
|
+
# start_span parents to the CURRENT OTel context — the wrap_agent root
|
|
884
|
+
# (made current via use_span in _enter_otel of wrap_agent).
|
|
885
|
+
otel_span = tracer.start_span(self._name)
|
|
886
|
+
use_span_helper, _, _ = get_otel_helpers()
|
|
887
|
+
if use_span_helper is not None:
|
|
888
|
+
self._otel_cm = use_span_helper(otel_span, end_on_exit=False)
|
|
889
|
+
self._otel_cm.__enter__()
|
|
890
|
+
self._otel_span = otel_span
|
|
891
|
+
self._span_id = str(uuid.uuid4())
|
|
892
|
+
self.handle = SpanHandle(self._span_id, self._name)
|
|
893
|
+
if self._input is not None:
|
|
894
|
+
self.handle.input = self._input
|
|
895
|
+
if self._attributes:
|
|
896
|
+
self.handle.attributes.update(self._attributes)
|
|
897
|
+
return self.handle
|
|
898
|
+
|
|
899
|
+
def _exit_otel(self, exc_type, exc, tb) -> None:
|
|
900
|
+
otel_span = self._otel_span
|
|
901
|
+
h = self.handle
|
|
902
|
+
try:
|
|
903
|
+
# Stamp Trodo kind via gen_ai.* / retrieval.* convention so the
|
|
904
|
+
# backend's kind inference picks it up (same keys as trodo-node).
|
|
905
|
+
if self._kind == "tool":
|
|
906
|
+
otel_span.set_attribute("gen_ai.tool.name", h.tool_name or self._name)
|
|
907
|
+
if self._kind == "retrieval":
|
|
908
|
+
otel_span.set_attribute("retrieval.query", self._name)
|
|
909
|
+
if h.input is not None:
|
|
910
|
+
otel_span.set_attribute("input.value", _serialize_attr(h.input))
|
|
911
|
+
if h.output is not None:
|
|
912
|
+
otel_span.set_attribute("output.value", _serialize_attr(h.output))
|
|
913
|
+
if h.model:
|
|
914
|
+
otel_span.set_attribute("gen_ai.request.model", h.model)
|
|
915
|
+
if h.provider:
|
|
916
|
+
otel_span.set_attribute("gen_ai.system", h.provider)
|
|
917
|
+
if h.input_tokens is not None:
|
|
918
|
+
otel_span.set_attribute("gen_ai.usage.input_tokens", h.input_tokens)
|
|
919
|
+
if h.output_tokens is not None:
|
|
920
|
+
otel_span.set_attribute("gen_ai.usage.output_tokens", h.output_tokens)
|
|
921
|
+
if h.cost is not None:
|
|
922
|
+
otel_span.set_attribute("gen_ai.usage.cost", h.cost)
|
|
923
|
+
if h.temperature is not None:
|
|
924
|
+
otel_span.set_attribute("gen_ai.request.temperature", h.temperature)
|
|
925
|
+
if h.tool_name:
|
|
926
|
+
otel_span.set_attribute("gen_ai.tool.name", h.tool_name)
|
|
927
|
+
for k, v in (h.attributes or {}).items():
|
|
928
|
+
otel_span.set_attribute(f"trodo.span.{k}", _serialize_attr(v))
|
|
929
|
+
einfo = _resolve_error(h, exc_type, exc, tb)
|
|
930
|
+
if einfo["status"] == "error":
|
|
931
|
+
if exc is not None and hasattr(otel_span, "record_exception"):
|
|
932
|
+
otel_span.record_exception(exc)
|
|
933
|
+
_, status_cls, status_code = get_otel_helpers()
|
|
934
|
+
if status_cls is not None and status_code is not None:
|
|
935
|
+
otel_span.set_status(
|
|
936
|
+
status_cls(status_code.ERROR, einfo["error_message"] or "")
|
|
937
|
+
)
|
|
938
|
+
finally:
|
|
939
|
+
otel_span.end()
|
|
940
|
+
if self._otel_cm is not None:
|
|
941
|
+
try:
|
|
942
|
+
self._otel_cm.__exit__(exc_type, exc, tb)
|
|
943
|
+
except Exception:
|
|
944
|
+
pass
|
|
945
|
+
self._otel_span = None
|
|
946
|
+
self._otel_cm = None
|
|
947
|
+
return None
|
|
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
|