trodo-python 2.10.9__tar.gz → 2.10.10__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.
Files changed (47) hide show
  1. {trodo_python-2.10.9 → trodo_python-2.10.10}/PKG-INFO +1 -1
  2. {trodo_python-2.10.9 → trodo_python-2.10.10}/pyproject.toml +1 -1
  3. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_auto_instrument_fixes.py +56 -0
  4. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/__init__.py +1 -1
  5. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/auto_instrument.py +96 -20
  6. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo_python.egg-info/PKG-INFO +1 -1
  7. {trodo_python-2.10.9 → trodo_python-2.10.10}/README.md +0 -0
  8. {trodo_python-2.10.9 → trodo_python-2.10.10}/setup.cfg +0 -0
  9. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_anon_distinct_id 2.py +0 -0
  10. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_anon_distinct_id.py +0 -0
  11. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_cross_process_session.py +0 -0
  12. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_end_run.py +0 -0
  13. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_error_enrichment.py +0 -0
  14. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_llm_usage_cost.py +0 -0
  15. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_processor_methods.py +0 -0
  16. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_register_otel.py +0 -0
  17. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_start_run.py +0 -0
  18. {trodo_python-2.10.9 → trodo_python-2.10.10}/tests/test_wrap_agent_unchanged.py +0 -0
  19. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/api/__init__.py +0 -0
  20. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/api/async_client.py +0 -0
  21. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/api/endpoints.py +0 -0
  22. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/api/http_client.py +0 -0
  23. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/auto/__init__.py +0 -0
  24. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/auto/auto_event_manager.py +0 -0
  25. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/client.py +0 -0
  26. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/managers/__init__.py +0 -0
  27. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/managers/group_manager.py +0 -0
  28. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/managers/people_manager.py +0 -0
  29. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/__init__.py +0 -0
  30. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/context.py +0 -0
  31. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/helpers.py +0 -0
  32. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/processor.py +0 -0
  33. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/register.py +0 -0
  34. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/transport.py +0 -0
  35. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/otel/wrap_agent.py +0 -0
  36. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/queue/__init__.py +0 -0
  37. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/queue/batch_flusher.py +0 -0
  38. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/queue/event_queue.py +0 -0
  39. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/session/__init__.py +0 -0
  40. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/session/server_session.py +0 -0
  41. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/session/session_manager.py +0 -0
  42. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/types.py +0 -0
  43. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo/user_context.py +0 -0
  44. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo_python.egg-info/SOURCES.txt +0 -0
  45. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo_python.egg-info/dependency_links.txt +0 -0
  46. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo_python.egg-info/requires.txt +0 -0
  47. {trodo_python-2.10.9 → trodo_python-2.10.10}/trodo_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.10.9
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trodo-python"
7
- version = "2.10.9"
7
+ version = "2.10.10"
8
8
  description = "Trodo Analytics SDK for Python — server-side event tracking"
9
9
  readme = "README.md"
10
10
  license = { text = "ISC" }
@@ -159,3 +159,59 @@ class TestVercelAiContentMapping:
159
159
  assert span.tool_name == "create_object"
160
160
  assert span.input == '{"isValid":true}'
161
161
  assert span.output == '{"ok":1}'
162
+
163
+
164
+ class TestCrossConventionContent:
165
+ """Convention-agnostic capture: OpenLLMetry (raw Anthropic/OpenAI/LangChain)
166
+ and OpenInference use indexed attrs; earlier only single-value keys were read
167
+ so those spans landed with empty input/output."""
168
+
169
+ def _span(self, attrs):
170
+ s = _fake_otel_span(start_ns=1_000_000_000, end_ns=2_000_000_000, attrs={"trodo.run_id": RUN_ID, **attrs})
171
+ s.name = "llm"
172
+ return s
173
+
174
+ def test_openllmetry_indexed(self):
175
+ s = otel_span_to_trodo_span(self._span({
176
+ "gen_ai.prompt.0.role": "system", "gen_ai.prompt.0.content": "be nice",
177
+ "gen_ai.prompt.1.role": "user", "gen_ai.prompt.1.content": "hi",
178
+ "gen_ai.completion.0.role": "assistant", "gen_ai.completion.0.content": "hello",
179
+ }))
180
+ assert "be nice" in s.input and "hi" in s.input
181
+ assert "hello" in s.output
182
+
183
+ def test_openinference_indexed(self):
184
+ s = otel_span_to_trodo_span(self._span({
185
+ "llm.input_messages.0.message.role": "user",
186
+ "llm.input_messages.0.message.content": "question",
187
+ "llm.output_messages.0.message.content": "reply",
188
+ }))
189
+ assert "question" in s.input
190
+ assert "reply" in s.output
191
+
192
+ def test_openinference_value_keys(self):
193
+ s = otel_span_to_trodo_span(self._span({"input.value": "the input", "output.value": "the output"}))
194
+ assert s.input == "the input"
195
+ assert s.output == "the output"
196
+
197
+ def test_otel_current_messages(self):
198
+ s = otel_span_to_trodo_span(self._span({
199
+ "gen_ai.input.messages": '[{"role":"user","parts":[{"content":"q"}]}]',
200
+ "gen_ai.output.messages": '[{"role":"assistant","parts":[{"content":"a"}]}]',
201
+ }))
202
+ assert '"q"' in s.input and '"a"' in s.output
203
+
204
+ def test_vercel_toolcalls_output(self):
205
+ s = otel_span_to_trodo_span(self._span({
206
+ "ai.prompt.messages": '[{"role":"user","content":"eval"}]',
207
+ "ai.response.toolCalls": '[{"toolName":"create_object"}]',
208
+ }))
209
+ assert "create_object" in s.output
210
+
211
+ def test_langfuse_traceloop_single(self):
212
+ s = otel_span_to_trodo_span(self._span({
213
+ "langfuse.observation.input": "user asked X",
214
+ "traceloop.entity.output": '{"result":"Y"}',
215
+ }))
216
+ assert s.input == "user asked X"
217
+ assert "Y" in s.output
@@ -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.9"
43
+ __version__ = "2.10.10"
44
44
 
45
45
  from typing import Any, Callable, Dict, List, Optional, Union
46
46
 
@@ -67,6 +67,99 @@ def _infer_kind(attrs: Dict[str, Any]) -> str:
67
67
  _ATTR_TRODO_RUN_ID = "trodo.run_id"
68
68
  _ATTR_TRODO_PARENT_SPAN_ID = "trodo.parent_span_id"
69
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
+
70
163
 
71
164
  def _span_id_to_uuid(span_id: Optional[str]) -> Optional[str]:
72
165
  """Convert an OTel hex span id into UUID format. Mirrors trodo-node's
@@ -206,26 +299,9 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
206
299
  or attrs.get("ai.response.model")
207
300
  )
208
301
  provider = attrs.get("gen_ai.system") or attrs.get("llm.vendor") or attrs.get("ai.model.provider")
209
- # Vercel AI SDK uses the `ai.*` convention, NOT gen_ai.prompt/completion:
210
- # ai.prompt.messages / ai.prompt (input), ai.response.text / ai.response.object
211
- # (output), ai.toolCall.args / ai.toolCall.result (tool spans). Omitting these
212
- # left input/output empty -> NULL in the DB -> nothing embedded or scored.
213
- prompt = (
214
- attrs.get("gen_ai.prompt")
215
- or attrs.get("llm.prompts")
216
- or attrs.get("ai.prompt")
217
- or attrs.get("ai.prompt.messages")
218
- or attrs.get("ai.toolCall.args")
219
- or attrs.get("input.value")
220
- )
221
- completion = (
222
- attrs.get("gen_ai.completion")
223
- or attrs.get("llm.completion")
224
- or attrs.get("ai.response.text")
225
- or attrs.get("ai.response.object")
226
- or attrs.get("ai.toolCall.result")
227
- or attrs.get("output.value")
228
- )
302
+ # Convention-agnostic see _extract_input/_extract_output above.
303
+ prompt = _extract_input(attrs)
304
+ completion = _extract_output(attrs)
229
305
  tool_name = (
230
306
  attrs.get("gen_ai.tool.name")
231
307
  or attrs.get("ai.toolCall.name")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.10.9
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
File without changes
File without changes