trodo-python 2.23.0__py3-none-any.whl → 2.23.2__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.
@@ -438,7 +438,44 @@ def _instrument(module_name: str, *preferred: str) -> None:
438
438
  )
439
439
  if cls is None:
440
440
  raise ImportError(f"no Instrumentor class exported by {module_name}")
441
- cls().instrument()
441
+
442
+ instr = cls()
443
+
444
+ # An instrumentor whose target library is the wrong VERSION does not raise:
445
+ # OpenTelemetry logs a DependencyConflict and returns without patching
446
+ # anything. We would then append it to `active` and report auto-capture that
447
+ # is not happening -- the same lie as listing a framework whose
448
+ # instrumentation cannot patch the installed SDK. Ask first, and let the
449
+ # caller report it honestly.
450
+ conflict = _dependency_conflict(instr)
451
+ if conflict is not None:
452
+ raise _IncompatibleInstrumentation(str(conflict))
453
+
454
+ instr.instrument()
455
+
456
+
457
+ class _IncompatibleInstrumentation(ImportError):
458
+ """The instrumentor loaded, but its target library is an unsupported version."""
459
+
460
+
461
+ def _dependency_conflict(instr: Any) -> Any:
462
+ """The version conflict OpenTelemetry would hit, or None.
463
+
464
+ Returns None when we cannot tell -- an older OTel without the helper, or an
465
+ instrumentor that declares nothing. Falsely reporting a working setup as
466
+ broken is worse than the silence this replaces.
467
+ """
468
+ try:
469
+ from opentelemetry.instrumentation.dependencies import ( # type: ignore
470
+ get_dependency_conflicts,
471
+ )
472
+
473
+ deps = instr.instrumentation_dependencies()
474
+ if not deps:
475
+ return None
476
+ return get_dependency_conflicts(deps)
477
+ except Exception: # noqa: BLE001
478
+ return None
442
479
 
443
480
 
444
481
  _INSTRUMENTORS: List[tuple[str, Callable[[], Any]]] = []
@@ -556,6 +593,17 @@ def enable_auto_instrument(
556
593
  try:
557
594
  register()
558
595
  active.append(name)
596
+ except _IncompatibleInstrumentation as e:
597
+ # Loud, because this is the case where everything LOOKS installed:
598
+ # the package is present, the import worked, and no spans will ever
599
+ # appear. Silence here is what costs people an afternoon.
600
+ _warn_once(
601
+ f"version-{name}",
602
+ f"auto-instrument: {name} will NOT be auto-captured and is not "
603
+ f"reported as active -- {e}. Wrap the call with trodo.llm(...) "
604
+ f"to capture it, or pin the library to a supported version.",
605
+ )
606
+ continue
559
607
  except Exception:
560
608
  continue
561
609
  return active
@@ -25,7 +25,7 @@ PromptRef = Dict[str, Any]
25
25
 
26
26
  def new_prompt_state() -> Dict[str, Any]:
27
27
  """Per-run accumulator, shared by reference across the run's span tree."""
28
- return {"current": None, "all": []}
28
+ return {"current": None, "current_span_id": None, "all": []}
29
29
 
30
30
 
31
31
  # Compiled prompt (or its messages list) -> the version it came from.
@@ -93,6 +93,7 @@ def record_compiled_prompt(ref: PromptRef) -> None:
93
93
  state = getattr(active, "prompt_state", None) if active is not None else None
94
94
  if isinstance(state, dict):
95
95
  state["current"] = ref
96
+ state["current_span_id"] = getattr(active, "span_id", None)
96
97
  key = _ref_key(ref)
97
98
  used: List[PromptRef] = state["all"]
98
99
  if not any(_ref_key(r) == key for r in used):
@@ -116,7 +117,11 @@ def prompt_attributes(ref: Optional[PromptRef]) -> Dict[str, str]:
116
117
 
117
118
 
118
119
  def merge_prompt_attrs(
119
- active: Any, attrs: Optional[Dict[str, Any]], from_input: Optional[PromptRef] = None
120
+ active: Any,
121
+ attrs: Optional[Dict[str, Any]],
122
+ from_input: Optional[PromptRef] = None,
123
+ kind: Optional[str] = None,
124
+ span_id: Optional[str] = None,
120
125
  ) -> Optional[Dict[str, Any]]:
121
126
  """Merge the prompt this span used into its attributes.
122
127
 
@@ -134,7 +139,24 @@ def merge_prompt_attrs(
134
139
  remember_prompt(state, from_input)
135
140
  merged.update(prompt_attributes(from_input))
136
141
  elif isinstance(state, dict):
137
- merged.update(prompt_attributes(state.get("current")))
142
+ # The run's "most recently compiled" prompt is a FALLBACK, used only
143
+ # where it is defensible. Applying it to every span meant compiling a
144
+ # prompt anywhere in a run attributed it to every LATER span -- a tool,
145
+ # a retrieval, a bit of plain work all claiming a prompt they never ran.
146
+ # Prompts belong to SPANS: one run's spans routinely use different
147
+ # prompts, different versions, or none at all.
148
+ #
149
+ # Two cases survive: a MODEL call (kind 'llm'), the only thing that runs
150
+ # a prompt and the one shape whose input we cannot read when it comes
151
+ # from a provider instrumentation; and the very span the prompt was
152
+ # compiled INSIDE, which plainly used it whatever its kind.
153
+ compiled_here = (
154
+ state.get("current") is not None
155
+ and span_id is not None
156
+ and state.get("current_span_id") == span_id
157
+ )
158
+ if kind == "llm" or compiled_here:
159
+ merged.update(prompt_attributes(state.get("current")))
138
160
  return merged or None
139
161
 
140
162
 
trodo/otel/wrap_agent.py CHANGED
@@ -876,7 +876,8 @@ class join_run:
876
876
  temperature=self.handle.temperature,
877
877
  tool_name=self.handle.tool_name,
878
878
  attributes=merge_prompt_attrs(
879
- self._ctx, self.handle.attributes, self.handle.prompt_ref
879
+ self._ctx, self.handle.attributes, self.handle.prompt_ref,
880
+ self._kind, self._span_id,
880
881
  ),
881
882
  )
882
883
  try:
@@ -988,7 +989,8 @@ class span:
988
989
  temperature=self.handle.temperature,
989
990
  tool_name=self.handle.tool_name,
990
991
  attributes=merge_prompt_attrs(
991
- self._active, self.handle.attributes, self.handle.prompt_ref
992
+ self._active, self.handle.attributes, self.handle.prompt_ref,
993
+ self._kind, self._span_id,
992
994
  ),
993
995
  )
994
996
  processor: TrodoSpanProcessor = self._active.processor # type: ignore[assignment]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.23.0
3
+ Version: 2.23.2
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -16,14 +16,14 @@ trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8p
16
16
  trodo/managers/prompt_manager.py,sha256=qFVHgh7PW-SlgHp62ybirdYsJBg-fcQVMTTU3jPqFvo,13435
17
17
  trodo/managers/user_manager.py,sha256=faJYX3CHrD7ulYiShV7FhThSMX9aJ3kdOgU4Qtdy5FM,2844
18
18
  trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
19
- trodo/otel/auto_instrument.py,sha256=kcfajnmaVuX_giLYBr2P7GiWk-lzZt-iniY8rJEALYw,21819
19
+ trodo/otel/auto_instrument.py,sha256=dA7IZ9cTSNTFK-oVwkYL2OwfTH3wDKM4iKXtPvZoK-Y,23800
20
20
  trodo/otel/context.py,sha256=Jd0aTc0Q-1dM5kXXinhZD8YtnpdKgvneNiODyboVGKY,1418
21
21
  trodo/otel/helpers.py,sha256=XOMWcgZHaq5SQbkFxDaXPE4CDFjn01xjJmJ1vIxvwpw,20730
22
22
  trodo/otel/processor.py,sha256=LKlXxP3BeQ7DP8SzYAgXZZOeJ6-6b7e43i19gCUC_0Y,7939
23
- trodo/otel/prompt_trace.py,sha256=X-8AIfwMmegOZl9yMifUkpYu1rV4wE6-kuYYtts0wgA,6184
23
+ trodo/otel/prompt_trace.py,sha256=d2A6UhlWAgHZLIKtU0NxgGUpHk_z3F2Nm4u4QTAC3tA,7332
24
24
  trodo/otel/register.py,sha256=bV_ePTfUvPugig2GZnylhzxi2QfoPWs96A9mGLPMrSQ,9387
25
25
  trodo/otel/transport.py,sha256=hzZz8gwSMGJ8CxdijmLn1Ljt18owr9XTWy13DLbwYbw,2441
26
- trodo/otel/wrap_agent.py,sha256=Izty1jxF4ANVqS72L0CR9jmMuW372AApaEO_0LC5Thw,43860
26
+ trodo/otel/wrap_agent.py,sha256=PiluY8r_yvGtuST45sqYWDbtF0trmKFQRiN0vbZl8Rk,43948
27
27
  trodo/prompts/__init__.py,sha256=yunNc8WkTSfEEkRR-NZWXAt_ZB3Ee2INRh-4RE7uHj4,806
28
28
  trodo/prompts/compile.py,sha256=XoKDKz6Yaofhrk-87AVW-SnwbiXLvfp161AL2bAFzNY,8802
29
29
  trodo/prompts/template.py,sha256=eGO2ZVNOw6JK7Ffj1UCYDMJFYyX5qQW2POlmFJSQpAo,9975
@@ -36,7 +36,7 @@ trodo/session/server_session.py,sha256=McsudEiq33XDq3nqxgzBcUvIjQxCMscwEuAPnYXrT
36
36
  trodo/session/session_manager.py,sha256=7ht5LeeZX1HLLfPeNV_a8NkXGbHl3up0CRtCGr3EzjQ,2995
37
37
  trodo/util/__init__.py,sha256=Z9c4rPPdKg06Kk3byKheDqksSgR4WNvS475oQ5sNljc,54
38
38
  trodo/util/lru.py,sha256=QIsM7s6J_E9ZjiXatSyReZIEeGypTAcgrPWWy8YsRa4,2465
39
- trodo_python-2.23.0.dist-info/METADATA,sha256=HklaG2r4SZNydUnzdRUluF9KLh1d-IszAlS1Sr4UEFI,25308
40
- trodo_python-2.23.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
41
- trodo_python-2.23.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
42
- trodo_python-2.23.0.dist-info/RECORD,,
39
+ trodo_python-2.23.2.dist-info/METADATA,sha256=HWnV5YiFL2qLt9cMyNDi8Sjuq9FCh9HmfCmLVhRBvIY,25308
40
+ trodo_python-2.23.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
41
+ trodo_python-2.23.2.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
42
+ trodo_python-2.23.2.dist-info/RECORD,,