trodo-python 2.10.11__tar.gz → 2.11.0__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.11 → trodo_python-2.11.0}/PKG-INFO +27 -2
  2. {trodo_python-2.10.11 → trodo_python-2.11.0}/README.md +26 -1
  3. {trodo_python-2.10.11 → trodo_python-2.11.0}/pyproject.toml +1 -1
  4. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_wrap_agent_unchanged.py +22 -0
  5. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/__init__.py +3 -2
  6. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/helpers.py +13 -1
  7. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/wrap_agent.py +16 -5
  8. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo_python.egg-info/PKG-INFO +27 -2
  9. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo_python.egg-info/SOURCES.txt +0 -1
  10. trodo_python-2.10.11/tests/test_anon_distinct_id 2.py +0 -100
  11. {trodo_python-2.10.11 → trodo_python-2.11.0}/setup.cfg +0 -0
  12. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_anon_distinct_id.py +0 -0
  13. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_auto_instrument_fixes.py +0 -0
  14. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_cross_process_session.py +0 -0
  15. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_end_run.py +0 -0
  16. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_error_enrichment.py +0 -0
  17. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_llm_usage_cost.py +0 -0
  18. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_processor_methods.py +0 -0
  19. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_register_otel.py +0 -0
  20. {trodo_python-2.10.11 → trodo_python-2.11.0}/tests/test_start_run.py +0 -0
  21. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/api/__init__.py +0 -0
  22. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/api/async_client.py +0 -0
  23. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/api/endpoints.py +0 -0
  24. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/api/http_client.py +0 -0
  25. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/auto/__init__.py +0 -0
  26. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/auto/auto_event_manager.py +0 -0
  27. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/client.py +0 -0
  28. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/managers/__init__.py +0 -0
  29. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/managers/group_manager.py +0 -0
  30. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/managers/people_manager.py +0 -0
  31. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/__init__.py +0 -0
  32. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/auto_instrument.py +0 -0
  33. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/context.py +0 -0
  34. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/processor.py +0 -0
  35. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/register.py +0 -0
  36. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/otel/transport.py +0 -0
  37. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/queue/__init__.py +0 -0
  38. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/queue/batch_flusher.py +0 -0
  39. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/queue/event_queue.py +0 -0
  40. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/session/__init__.py +0 -0
  41. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/session/server_session.py +0 -0
  42. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/session/session_manager.py +0 -0
  43. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/types.py +0 -0
  44. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo/user_context.py +0 -0
  45. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo_python.egg-info/dependency_links.txt +0 -0
  46. {trodo_python-2.10.11 → trodo_python-2.11.0}/trodo_python.egg-info/requires.txt +0 -0
  47. {trodo_python-2.10.11 → trodo_python-2.11.0}/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.11
3
+ Version: 2.11.0
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -250,6 +250,30 @@ search = trodo.retrieval('vector_search', vector_search)
250
250
  docs = search(query)
251
251
  ```
252
252
 
253
+ ### LLM span input: the chat-message list
254
+
255
+ For LLM spans, set the input to the **same messages you send to the model** —
256
+ a chat-message list, not one blob:
257
+
258
+ ```python
259
+ span.set_input([
260
+ {"role": "system", "content": system_prompt}, # rules & role
261
+ {"role": "context", "content": retrieved_docs}, # RAG docs (Trodo extension)
262
+ {"role": "user", "content": "Where is my order?"},
263
+ {"role": "assistant", "content": None, "tool_calls": [...]},
264
+ {"role": "tool", "content": '{"status": "shipped"}', "tool_call_id": "c1"},
265
+ {"role": "user", "content": "When will it arrive?"}, # multiple turns are fine
266
+ ])
267
+ ```
268
+
269
+ Roles: the standard `system` / `user` / `assistant` / `tool` plus `context` —
270
+ a Trodo extension for RAG / retrieved documents. Any order, any number per
271
+ role; aliases `developer` / `model` / `function` normalise automatically.
272
+ Trodo embeds the input as a whole **and each role separately**, which powers
273
+ the AI-score detectors (system → rule adherence; user → trajectory/echo;
274
+ context else tool+assistant → grounding, contradiction, factual retention).
275
+ A plain string still works and is embedded as one vector.
276
+
253
277
  ### Raw-HTTP escape hatches
254
278
 
255
279
  If your LLM client isn't OTel-instrumented and you can't wrap it as a
@@ -261,7 +285,8 @@ trodo.track_llm_call(
261
285
  model='gemini-2.5-flash', provider='google',
262
286
  input_tokens=resp['usageMetadata']['promptTokenCount'],
263
287
  output_tokens=resp['usageMetadata']['candidatesTokenCount'],
264
- prompt=body, completion=resp,
288
+ prompt=body['messages'], # the chat-message list sent to the model
289
+ completion=resp,
265
290
  )
266
291
  ```
267
292
 
@@ -219,6 +219,30 @@ search = trodo.retrieval('vector_search', vector_search)
219
219
  docs = search(query)
220
220
  ```
221
221
 
222
+ ### LLM span input: the chat-message list
223
+
224
+ For LLM spans, set the input to the **same messages you send to the model** —
225
+ a chat-message list, not one blob:
226
+
227
+ ```python
228
+ span.set_input([
229
+ {"role": "system", "content": system_prompt}, # rules & role
230
+ {"role": "context", "content": retrieved_docs}, # RAG docs (Trodo extension)
231
+ {"role": "user", "content": "Where is my order?"},
232
+ {"role": "assistant", "content": None, "tool_calls": [...]},
233
+ {"role": "tool", "content": '{"status": "shipped"}', "tool_call_id": "c1"},
234
+ {"role": "user", "content": "When will it arrive?"}, # multiple turns are fine
235
+ ])
236
+ ```
237
+
238
+ Roles: the standard `system` / `user` / `assistant` / `tool` plus `context` —
239
+ a Trodo extension for RAG / retrieved documents. Any order, any number per
240
+ role; aliases `developer` / `model` / `function` normalise automatically.
241
+ Trodo embeds the input as a whole **and each role separately**, which powers
242
+ the AI-score detectors (system → rule adherence; user → trajectory/echo;
243
+ context else tool+assistant → grounding, contradiction, factual retention).
244
+ A plain string still works and is embedded as one vector.
245
+
222
246
  ### Raw-HTTP escape hatches
223
247
 
224
248
  If your LLM client isn't OTel-instrumented and you can't wrap it as a
@@ -230,7 +254,8 @@ trodo.track_llm_call(
230
254
  model='gemini-2.5-flash', provider='google',
231
255
  input_tokens=resp['usageMetadata']['promptTokenCount'],
232
256
  output_tokens=resp['usageMetadata']['candidatesTokenCount'],
233
- prompt=body, completion=resp,
257
+ prompt=body['messages'], # the chat-message list sent to the model
258
+ completion=resp,
234
259
  )
235
260
  ```
236
261
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "trodo-python"
7
- version = "2.10.11"
7
+ version = "2.11.0"
8
8
  description = "Trodo Analytics SDK for Python — server-side event tracking"
9
9
  readme = "README.md"
10
10
  license = { text = "ISC" }
@@ -47,3 +47,25 @@ def test_wrap_agent_records_error_on_exception(processor, http):
47
47
  run_payload = http.run_ingest[0]["run"]
48
48
  assert run_payload["status"] == "error"
49
49
  assert "kaboom" in run_payload["error_summary"]
50
+
51
+
52
+ def test_set_input_passes_chat_message_list_through(processor, http):
53
+ messages = [
54
+ {"role": "system", "content": "be terse"},
55
+ {"role": "context", "content": ["doc chunk"]},
56
+ {"role": "user", "content": "q1"},
57
+ {"role": "assistant", "content": None, "tool_calls": [{"id": "c1", "function": {"name": "f", "arguments": "{}"}}]},
58
+ {"role": "tool", "content": '{"ok": true}', "tool_call_id": "c1"},
59
+ {"role": "user", "content": "q2"},
60
+ ]
61
+ with wrap_agent(
62
+ processor=processor,
63
+ team_site_id="site-x",
64
+ agent_name="chat",
65
+ ) as run:
66
+ run.set_input(messages)
67
+ run.set_output("done")
68
+
69
+ run_payload = http.run_ingest[0]["run"]
70
+ # The list must arrive as-is — no JSON stringification, no envelope.
71
+ assert run_payload["input"] == messages
@@ -19,7 +19,8 @@ Raw-HTTP LLM caller (no OTel integration for your client):
19
19
  model='gemini-2.5-flash', provider='google',
20
20
  input_tokens=resp['usageMetadata']['promptTokenCount'],
21
21
  output_tokens=resp['usageMetadata']['candidatesTokenCount'],
22
- prompt=body, completion=resp,
22
+ prompt=body['messages'], # the chat-message list sent to the model
23
+ completion=resp,
23
24
  )
24
25
 
25
26
  Custom tool:
@@ -40,7 +41,7 @@ Downstream microservice (join the caller's run instead of making a new one):
40
41
 
41
42
  from __future__ import annotations
42
43
 
43
- __version__ = "2.10.11"
44
+ __version__ = "2.11.0"
44
45
 
45
46
  from typing import Any, Callable, Dict, List, Optional, Union
46
47
 
@@ -297,6 +297,12 @@ def llm(
297
297
  fall back to scalar-only extraction, or ``extract_usage_map=lambda r: {..}``
298
298
  to build the map yourself.
299
299
 
300
+ The wrapped function's arguments become the span input. For the AI-score
301
+ detectors to see per-role content, pass the model call's chat-message list
302
+ (``[{"role", "content"}, ...]`` — roles system/user/assistant/tool +
303
+ ``context`` for RAG docs) so the span input is exactly what the model
304
+ receives.
305
+
300
306
  Usage::
301
307
 
302
308
  answer = trodo.llm(
@@ -470,12 +476,18 @@ def track_llm_call(
470
476
  ``usage_details`` map, or a raw provider ``usage`` object to auto-extract
471
477
  from (e.g. ``resp['usage']`` or ``resp['usageMetadata']``).
472
478
 
479
+ ``prompt`` — prefer the chat-message list you sent to the model
480
+ (``[{"role", "content"}, ...]``, roles system/user/assistant/tool +
481
+ ``context`` for RAG docs, any order, multiple per role). The backend
482
+ embeds each role separately, which powers the AI-score detectors. A plain
483
+ string or dict is stored as one opaque input.
484
+
473
485
  Usage:
474
486
  resp = httpx.post(url, json=body).json()
475
487
  trodo.track_llm_call(
476
488
  model='claude-sonnet-4', provider='anthropic',
477
489
  usage=resp['usage'], # cache fields captured automatically
478
- prompt=body, completion=resp,
490
+ prompt=body['messages'], completion=resp,
479
491
  )
480
492
  """
481
493
  if get_active_context() is None:
@@ -29,7 +29,7 @@ import time
29
29
  import traceback
30
30
  import uuid
31
31
  from datetime import datetime, timezone
32
- from typing import Any, Callable, Dict, Optional, Union
32
+ from typing import Any, Callable, Dict, List, Optional, Union
33
33
 
34
34
  from .context import ActiveSpanContext, get_active_context, run_with_context
35
35
  from .processor import TrodoSpanProcessor, TrodoRun, TrodoSpan
@@ -149,16 +149,19 @@ def _resolve_error(handle, exc_type, exc, tb) -> Dict[str, Optional[str]]:
149
149
  "status_code": None, "stack_trace": None, "level": None}
150
150
 
151
151
 
152
- def _prepare_value(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[Union[str, Dict[str, Any]]]:
152
+ def _prepare_value(value: Any, max_len: int = _MAX_VALUE_LEN) -> Optional[Union[str, Dict[str, Any], List[Any]]]:
153
153
  """Prepare a value for storage in the JSONB input/output column.
154
154
 
155
- Dicts/lists pass through as-is (stored as JSONB objects/arrays).
156
- Strings are truncated at max_len.
155
+ Dicts/lists pass through as-is (stored as JSONB objects/arrays). For LLM
156
+ span inputs, prefer a chat-message list ``[{"role", "content"}, ...]``
157
+ (roles system/user/assistant/tool + ``context`` for RAG docs, any order,
158
+ multiple per role) — the backend embeds each role separately and the
159
+ AI-score detectors key on them. Strings are truncated at max_len.
157
160
  Everything else is JSON-serialised then truncated.
158
161
  """
159
162
  if value is None:
160
163
  return None
161
- if isinstance(value, dict):
164
+ if isinstance(value, (dict, list)):
162
165
  return value
163
166
  if isinstance(value, str):
164
167
  return value[:max_len] if len(value) > max_len else value
@@ -248,6 +251,9 @@ class RunHandle:
248
251
  self.error_type: Optional[str] = None
249
252
 
250
253
  def set_input(self, value: Any) -> None:
254
+ """Set the run input. Prefer a chat-message list
255
+ ``[{"role": "user", "content": ...}, ...]`` — the backend embeds the
256
+ user messages as the run's semantic input."""
251
257
  self.input = _prepare_value(value)
252
258
 
253
259
  def set_output(self, value: Any) -> None:
@@ -337,6 +343,11 @@ class SpanHandle:
337
343
  return self.error_message is not None or self.error_type is not None
338
344
 
339
345
  def set_input(self, value: Any) -> None:
346
+ """Set the span input. For LLM spans prefer a chat-message list
347
+ ``[{"role", "content"}, ...]`` (roles system/user/assistant/tool +
348
+ ``context`` for RAG docs, any order, multiple per role) — each role is
349
+ embedded separately and powers the AI-score detectors. Anything else
350
+ is stored as one opaque input."""
340
351
  self.input = _prepare_value(value)
341
352
 
342
353
  def set_output(self, value: Any) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.10.11
3
+ Version: 2.11.0
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -250,6 +250,30 @@ search = trodo.retrieval('vector_search', vector_search)
250
250
  docs = search(query)
251
251
  ```
252
252
 
253
+ ### LLM span input: the chat-message list
254
+
255
+ For LLM spans, set the input to the **same messages you send to the model** —
256
+ a chat-message list, not one blob:
257
+
258
+ ```python
259
+ span.set_input([
260
+ {"role": "system", "content": system_prompt}, # rules & role
261
+ {"role": "context", "content": retrieved_docs}, # RAG docs (Trodo extension)
262
+ {"role": "user", "content": "Where is my order?"},
263
+ {"role": "assistant", "content": None, "tool_calls": [...]},
264
+ {"role": "tool", "content": '{"status": "shipped"}', "tool_call_id": "c1"},
265
+ {"role": "user", "content": "When will it arrive?"}, # multiple turns are fine
266
+ ])
267
+ ```
268
+
269
+ Roles: the standard `system` / `user` / `assistant` / `tool` plus `context` —
270
+ a Trodo extension for RAG / retrieved documents. Any order, any number per
271
+ role; aliases `developer` / `model` / `function` normalise automatically.
272
+ Trodo embeds the input as a whole **and each role separately**, which powers
273
+ the AI-score detectors (system → rule adherence; user → trajectory/echo;
274
+ context else tool+assistant → grounding, contradiction, factual retention).
275
+ A plain string still works and is embedded as one vector.
276
+
253
277
  ### Raw-HTTP escape hatches
254
278
 
255
279
  If your LLM client isn't OTel-instrumented and you can't wrap it as a
@@ -261,7 +285,8 @@ trodo.track_llm_call(
261
285
  model='gemini-2.5-flash', provider='google',
262
286
  input_tokens=resp['usageMetadata']['promptTokenCount'],
263
287
  output_tokens=resp['usageMetadata']['candidatesTokenCount'],
264
- prompt=body, completion=resp,
288
+ prompt=body['messages'], # the chat-message list sent to the model
289
+ completion=resp,
265
290
  )
266
291
  ```
267
292
 
@@ -1,6 +1,5 @@
1
1
  README.md
2
2
  pyproject.toml
3
- tests/test_anon_distinct_id 2.py
4
3
  tests/test_anon_distinct_id.py
5
4
  tests/test_auto_instrument_fixes.py
6
5
  tests/test_cross_process_session.py
@@ -1,100 +0,0 @@
1
- """Anonymous distinct_id minting on agent surfaces.
2
-
3
- When the caller doesn't pass ``distinct_id``, the SDK mints an
4
- ``anon_<ts>_python_<uuid>_<rand>`` id so:
5
-
6
- * the agent_runs row always lands with a non-null distinct_id,
7
- * the RunHandle exposes the same id so callers can bind
8
- ``trodo.feedback(distinct_id=...)`` later,
9
- * older backends without identity-resolution still attribute the row.
10
- """
11
- from __future__ import annotations
12
-
13
- import re
14
-
15
- from trodo.otel.wrap_agent import _mint_anon_distinct_id, start_run, wrap_agent
16
-
17
-
18
- ANON_RE = re.compile(r"^anon_\d+_python_")
19
-
20
-
21
- def test_mint_anon_distinct_id_shape():
22
- a = _mint_anon_distinct_id()
23
- assert ANON_RE.match(a)
24
-
25
-
26
- def test_mint_anon_distinct_id_unique_under_load():
27
- ids = {_mint_anon_distinct_id() for _ in range(1000)}
28
- assert len(ids) == 1000
29
-
30
-
31
- def test_wrap_agent_mints_anon_when_distinct_id_omitted(processor, http):
32
- observed = {}
33
- with wrap_agent(
34
- processor=processor,
35
- team_site_id="site-x",
36
- agent_name="chat",
37
- ) as run:
38
- observed["distinct_id"] = run.distinct_id
39
- run.set_output("done")
40
-
41
- assert ANON_RE.match(observed["distinct_id"])
42
- assert len(http.run_ingest) == 1
43
- assert http.run_ingest[0]["run"]["distinct_id"] == observed["distinct_id"]
44
-
45
-
46
- def test_wrap_agent_respects_explicit_distinct_id(processor, http):
47
- with wrap_agent(
48
- processor=processor,
49
- team_site_id="site-x",
50
- agent_name="chat",
51
- distinct_id="user-42",
52
- ) as run:
53
- assert run.distinct_id == "user-42"
54
-
55
- assert http.run_ingest[0]["run"]["distinct_id"] == "user-42"
56
-
57
-
58
- def test_wrap_agent_mints_different_anon_ids_across_calls(processor, http):
59
- seen = []
60
- for _ in range(3):
61
- with wrap_agent(
62
- processor=processor,
63
- team_site_id="site-x",
64
- agent_name="chat",
65
- ) as run:
66
- seen.append(run.distinct_id)
67
- assert len(set(seen)) == 3
68
-
69
-
70
- def test_wrap_agent_still_mints_anon_on_error(processor, http):
71
- import pytest
72
- with pytest.raises(ValueError):
73
- with wrap_agent(
74
- processor=processor,
75
- team_site_id="site-x",
76
- agent_name="chat",
77
- ) as _run:
78
- raise ValueError("boom")
79
-
80
- assert len(http.run_ingest) == 1
81
- payload = http.run_ingest[0]["run"]
82
- assert payload["status"] == "error"
83
- assert ANON_RE.match(payload["distinct_id"])
84
-
85
-
86
- def test_start_run_mints_anon_when_distinct_id_omitted(processor, http):
87
- start_run(processor=processor, agent_name="external_session")
88
- assert len(http.run_start) == 1
89
- run = http.run_start[0]["run"]
90
- assert ANON_RE.match(run["distinct_id"])
91
-
92
-
93
- def test_start_run_respects_explicit_distinct_id(processor, http):
94
- start_run(
95
- processor=processor,
96
- agent_name="external_session",
97
- distinct_id="user-7",
98
- )
99
- run = http.run_start[0]["run"]
100
- assert run["distinct_id"] == "user-7"
File without changes