trodo-python 2.10.8__py3-none-any.whl → 2.10.10__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 CHANGED
@@ -40,7 +40,7 @@ Downstream microservice (join the caller's run instead of making a new one):
40
40
 
41
41
  from __future__ import annotations
42
42
 
43
- __version__ = "2.10.8"
43
+ __version__ = "2.10.10"
44
44
 
45
45
  from typing import Any, Callable, Dict, List, Optional, Union
46
46
 
@@ -45,9 +45,19 @@ def _trunc(s: Any, max_len: int) -> Optional[str]:
45
45
  def _infer_kind(attrs: Dict[str, Any]) -> str:
46
46
  if not attrs:
47
47
  return "generic"
48
- if attrs.get("gen_ai.tool.name"):
48
+ # Vercel AI SDK tool spans (`ai.toolCall`) carry ai.toolCall.name, not
49
+ # gen_ai.tool.name — without this they landed as 'generic'.
50
+ if attrs.get("gen_ai.tool.name") or attrs.get("ai.toolCall.name") or attrs.get("ai.functionCall.name"):
49
51
  return "tool"
50
- if attrs.get("gen_ai.operation.name") or attrs.get("gen_ai.request.model"):
52
+ if (
53
+ attrs.get("gen_ai.operation.name")
54
+ or attrs.get("gen_ai.request.model")
55
+ or attrs.get("gen_ai.response.model")
56
+ or attrs.get("ai.model.id")
57
+ or attrs.get("ai.response.model")
58
+ or attrs.get("gen_ai.usage.input_tokens") is not None
59
+ or attrs.get("ai.usage.promptTokens") is not None
60
+ ):
51
61
  return "llm"
52
62
  if attrs.get("db.system") or attrs.get("retrieval.query"):
53
63
  return "retrieval"
@@ -57,6 +67,99 @@ def _infer_kind(attrs: Dict[str, Any]) -> str:
57
67
  _ATTR_TRODO_RUN_ID = "trodo.run_id"
58
68
  _ATTR_TRODO_PARENT_SPAN_ID = "trodo.parent_span_id"
59
69
 
70
+ # ---------------------------------------------------------------------------
71
+ # Convention-agnostic input/output extraction — covers every common LLM
72
+ # instrumentor convention, not just Vercel AI. Mirrors the backend's
73
+ # services/ingest/spanContent.js.
74
+ # - Vercel AI ...... ai.prompt(.messages) / ai.response.text|object|toolCalls,
75
+ # ai.toolCall.args / ai.toolCall.result
76
+ # - OTel GenAI ..... gen_ai.input.messages / gen_ai.output.messages (current);
77
+ # gen_ai.prompt / gen_ai.completion (legacy single value)
78
+ # - OpenLLMetry .... INDEXED gen_ai.prompt.{i}.role/content, gen_ai.completion.{i}.*
79
+ # - OpenInference .. input.value / output.value + INDEXED llm.input_messages.{i}.message.*
80
+ # - Traceloop/Langfuse/MLflow, plus generic llm.prompts / input / prompt
81
+ # ---------------------------------------------------------------------------
82
+ _INPUT_SINGLE_KEYS = [
83
+ "gen_ai.input.messages", "ai.prompt.messages", "ai.prompt", "gen_ai.prompt",
84
+ "input.value", "traceloop.entity.input", "langfuse.observation.input", "mlflow.spanInputs",
85
+ "llm.input_messages", "llm.prompts", "gen_ai.tool.input", "ai.toolCall.args", "input", "prompt",
86
+ ]
87
+ _OUTPUT_SINGLE_KEYS = [
88
+ "gen_ai.output.messages", "ai.response.text", "ai.response.object", "ai.response.toolCalls",
89
+ "gen_ai.completion", "output.value", "traceloop.entity.output", "langfuse.observation.output",
90
+ "mlflow.spanOutputs", "llm.output_messages", "llm.completions", "gen_ai.tool.output",
91
+ "ai.toolCall.result", "output", "completion",
92
+ ]
93
+ _INPUT_INDEXED_BASES = ["gen_ai.prompt", "llm.input_messages"]
94
+ _OUTPUT_INDEXED_BASES = ["gen_ai.completion", "llm.output_messages"]
95
+
96
+
97
+ def _first_single(attrs: Dict[str, Any], keys) -> Optional[Any]:
98
+ for k in keys:
99
+ v = attrs.get(k)
100
+ if v is None:
101
+ continue
102
+ if isinstance(v, str):
103
+ if v:
104
+ return v
105
+ continue
106
+ return v # dict/list — keep structure
107
+ return None
108
+
109
+
110
+ def _set_nested(obj: Dict[str, Any], path: str, val: Any) -> None:
111
+ parts = path.split(".")
112
+ cur = obj
113
+ for p in parts[:-1]:
114
+ nxt = cur.get(p)
115
+ if not isinstance(nxt, dict):
116
+ nxt = {}
117
+ cur[p] = nxt
118
+ cur = nxt
119
+ cur[parts[-1]] = val
120
+
121
+
122
+ def _reconstruct_indexed(attrs: Dict[str, Any], bases) -> Optional[str]:
123
+ import json as _json
124
+ for base in bases:
125
+ prefix = base + "."
126
+ by_index: Dict[int, Dict[str, Any]] = {}
127
+ for key, val in attrs.items():
128
+ if not key.startswith(prefix):
129
+ continue
130
+ rest = key[len(prefix):]
131
+ dot = rest.find(".")
132
+ if dot < 0:
133
+ continue
134
+ idx_str = rest[:dot]
135
+ if not idx_str.isdigit():
136
+ continue
137
+ idx = int(idx_str)
138
+ by_index.setdefault(idx, {})
139
+ _set_nested(by_index[idx], rest[dot + 1:], val)
140
+ if by_index:
141
+ arr = [by_index[i] for i in sorted(by_index)]
142
+ return _json.dumps(arr)
143
+ return None
144
+
145
+
146
+ def _extract_input(attrs: Dict[str, Any]) -> Optional[Any]:
147
+ if not isinstance(attrs, dict):
148
+ return None
149
+ single = _first_single(attrs, _INPUT_SINGLE_KEYS)
150
+ if single is not None:
151
+ return single
152
+ return _reconstruct_indexed(attrs, _INPUT_INDEXED_BASES)
153
+
154
+
155
+ def _extract_output(attrs: Dict[str, Any]) -> Optional[Any]:
156
+ if not isinstance(attrs, dict):
157
+ return None
158
+ single = _first_single(attrs, _OUTPUT_SINGLE_KEYS)
159
+ if single is not None:
160
+ return single
161
+ return _reconstruct_indexed(attrs, _OUTPUT_INDEXED_BASES)
162
+
60
163
 
61
164
  def _span_id_to_uuid(span_id: Optional[str]) -> Optional[str]:
62
165
  """Convert an OTel hex span id into UUID format. Mirrors trodo-node's
@@ -178,20 +281,35 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
178
281
  attrs.get("gen_ai.usage.input_tokens")
179
282
  or attrs.get("gen_ai.usage.prompt_tokens")
180
283
  or attrs.get("llm.usage.prompt_tokens")
284
+ or attrs.get("ai.usage.promptTokens")
285
+ or attrs.get("ai.usage.inputTokens")
181
286
  )
182
287
  out_toks = (
183
288
  attrs.get("gen_ai.usage.output_tokens")
184
289
  or attrs.get("gen_ai.usage.completion_tokens")
185
290
  or attrs.get("llm.usage.completion_tokens")
291
+ or attrs.get("ai.usage.completionTokens")
292
+ or attrs.get("ai.usage.outputTokens")
186
293
  )
187
294
  model = (
188
295
  attrs.get("gen_ai.request.model")
189
296
  or attrs.get("gen_ai.response.model")
190
297
  or attrs.get("llm.request.model")
298
+ or attrs.get("ai.model.id")
299
+ or attrs.get("ai.response.model")
300
+ )
301
+ provider = attrs.get("gen_ai.system") or attrs.get("llm.vendor") or attrs.get("ai.model.provider")
302
+ # Convention-agnostic — see _extract_input/_extract_output above.
303
+ prompt = _extract_input(attrs)
304
+ completion = _extract_output(attrs)
305
+ tool_name = (
306
+ attrs.get("gen_ai.tool.name")
307
+ or attrs.get("ai.toolCall.name")
308
+ or attrs.get("ai.functionCall.name")
191
309
  )
192
- provider = attrs.get("gen_ai.system") or attrs.get("llm.vendor")
193
- prompt = attrs.get("gen_ai.prompt") or attrs.get("llm.prompts")
194
- completion = attrs.get("gen_ai.completion") or attrs.get("llm.completion")
310
+ temperature = attrs.get("gen_ai.request.temperature")
311
+ if temperature is None:
312
+ temperature = attrs.get("ai.settings.temperature")
195
313
 
196
314
  return TrodoSpan(
197
315
  span_id=span_id,
@@ -212,8 +330,8 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
212
330
  provider=provider,
213
331
  input_tokens=int(in_toks) if in_toks is not None else None,
214
332
  output_tokens=int(out_toks) if out_toks is not None else None,
215
- temperature=attrs.get("gen_ai.request.temperature"),
216
- tool_name=attrs.get("gen_ai.tool.name"),
333
+ temperature=temperature,
334
+ tool_name=tool_name,
217
335
  input=prompt,
218
336
  output=completion,
219
337
  attributes=attrs or None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.10.8
3
+ Version: 2.10.10
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -1,4 +1,4 @@
1
- trodo/__init__.py,sha256=tfTaRW3Cmc87r_zTYy1SbztE9qbnvpp7tBo--DNMd8w,18166
1
+ trodo/__init__.py,sha256=NdFjtAqmhBo4Ij9SAl7ByzTxUsVWSKbBGwGiaXKGioc,18167
2
2
  trodo/client.py,sha256=dhGiOJmxdWmXDDDabHN865_cAxttJs9LWPeDwEFbAwI,19107
3
3
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
4
4
  trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
@@ -12,7 +12,7 @@ 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=QhSqQF6KRbSbeRuG28mzyJZleeCyDIpDRdxCKcC7eps,16169
15
+ trodo/otel/auto_instrument.py,sha256=JvXXssoCbX3vE_3GrqDZPIqKHXqVrFbW2DEnmuYtFoE,20834
16
16
  trodo/otel/context.py,sha256=iJ1rE42-SbO8VZHAxhIl2ZJXgNwLIVps5xLg8GKgfFc,1165
17
17
  trodo/otel/helpers.py,sha256=4HsjMOrE-7zuvaRSiGXxV7ZyfXQ5gLxtR3HdpLut9sk,20054
18
18
  trodo/otel/processor.py,sha256=aqcTmzTw9cESgIp829pu_XCa5_dG_2MaeJNsqJZeqQU,7495
@@ -25,7 +25,7 @@ trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,87
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.10.8.dist-info/METADATA,sha256=Itp-gZCxGIWFFQo91wCT5VRdrDmtcw0C79yyKM4veIg,20483
29
- trodo_python-2.10.8.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
30
- trodo_python-2.10.8.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
31
- trodo_python-2.10.8.dist-info/RECORD,,
28
+ trodo_python-2.10.10.dist-info/METADATA,sha256=5QKvyxt9X3UnpLrhJy2f4BBqCumdmkZX2y9oahc-sdg,20484
29
+ trodo_python-2.10.10.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
30
+ trodo_python-2.10.10.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
31
+ trodo_python-2.10.10.dist-info/RECORD,,