trodo-python 2.10.10__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.
- {trodo_python-2.10.10 → trodo_python-2.11.0}/PKG-INFO +27 -2
- {trodo_python-2.10.10 → trodo_python-2.11.0}/README.md +26 -1
- {trodo_python-2.10.10 → trodo_python-2.11.0}/pyproject.toml +1 -1
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_auto_instrument_fixes.py +12 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_wrap_agent_unchanged.py +22 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/__init__.py +3 -2
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/auto_instrument.py +3 -2
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/helpers.py +13 -1
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/wrap_agent.py +16 -5
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo_python.egg-info/PKG-INFO +27 -2
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo_python.egg-info/SOURCES.txt +0 -1
- trodo_python-2.10.10/tests/test_anon_distinct_id 2.py +0 -100
- {trodo_python-2.10.10 → trodo_python-2.11.0}/setup.cfg +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_anon_distinct_id.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_end_run.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_error_enrichment.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_llm_usage_cost.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_register_otel.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/tests/test_start_run.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/api/__init__.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/api/async_client.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/api/endpoints.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/api/http_client.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/client.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/context.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/processor.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/register.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/otel/transport.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/session/__init__.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/session/server_session.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/types.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo/user_context.py +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.10.10 → trodo_python-2.11.0}/trodo_python.egg-info/requires.txt +0 -0
- {trodo_python-2.10.10 → 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.
|
|
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,
|
|
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,
|
|
257
|
+
prompt=body['messages'], # the chat-message list sent to the model
|
|
258
|
+
completion=resp,
|
|
234
259
|
)
|
|
235
260
|
```
|
|
236
261
|
|
|
@@ -208,6 +208,18 @@ class TestCrossConventionContent:
|
|
|
208
208
|
}))
|
|
209
209
|
assert "create_object" in s.output
|
|
210
210
|
|
|
211
|
+
def test_vercel_v7_execute_tool(self):
|
|
212
|
+
s = otel_span_to_trodo_span(self._span({
|
|
213
|
+
"gen_ai.operation.name": "execute_tool",
|
|
214
|
+
"gen_ai.tool.name": "getWeather",
|
|
215
|
+
"gen_ai.tool.call.arguments": '{"city":"NYC"}',
|
|
216
|
+
"gen_ai.tool.call.result": '{"city":"NYC","tempC":22}',
|
|
217
|
+
}))
|
|
218
|
+
assert s.kind == "tool"
|
|
219
|
+
assert s.tool_name == "getWeather"
|
|
220
|
+
assert s.input == '{"city":"NYC"}'
|
|
221
|
+
assert "tempC" in s.output
|
|
222
|
+
|
|
211
223
|
def test_langfuse_traceloop_single(self):
|
|
212
224
|
s = otel_span_to_trodo_span(self._span({
|
|
213
225
|
"langfuse.observation.input": "user asked X",
|
|
@@ -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,
|
|
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.
|
|
44
|
+
__version__ = "2.11.0"
|
|
44
45
|
|
|
45
46
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
46
47
|
|
|
@@ -82,13 +82,14 @@ _ATTR_TRODO_PARENT_SPAN_ID = "trodo.parent_span_id"
|
|
|
82
82
|
_INPUT_SINGLE_KEYS = [
|
|
83
83
|
"gen_ai.input.messages", "ai.prompt.messages", "ai.prompt", "gen_ai.prompt",
|
|
84
84
|
"input.value", "traceloop.entity.input", "langfuse.observation.input", "mlflow.spanInputs",
|
|
85
|
-
"llm.input_messages", "llm.prompts", "gen_ai.tool.input", "
|
|
85
|
+
"llm.input_messages", "llm.prompts", "gen_ai.tool.input", "gen_ai.tool.call.arguments",
|
|
86
|
+
"ai.toolCall.args", "input", "prompt",
|
|
86
87
|
]
|
|
87
88
|
_OUTPUT_SINGLE_KEYS = [
|
|
88
89
|
"gen_ai.output.messages", "ai.response.text", "ai.response.object", "ai.response.toolCalls",
|
|
89
90
|
"gen_ai.completion", "output.value", "traceloop.entity.output", "langfuse.observation.output",
|
|
90
91
|
"mlflow.spanOutputs", "llm.output_messages", "llm.completions", "gen_ai.tool.output",
|
|
91
|
-
"ai.toolCall.result", "output", "completion",
|
|
92
|
+
"gen_ai.tool.call.result", "ai.toolCall.result", "output", "completion",
|
|
92
93
|
]
|
|
93
94
|
_INPUT_INDEXED_BASES = ["gen_ai.prompt", "llm.input_messages"]
|
|
94
95
|
_OUTPUT_INDEXED_BASES = ["gen_ai.completion", "llm.output_messages"]
|
|
@@ -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
|
-
|
|
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.
|
|
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,
|
|
288
|
+
prompt=body['messages'], # the chat-message list sent to the model
|
|
289
|
+
completion=resp,
|
|
265
290
|
)
|
|
266
291
|
```
|
|
267
292
|
|
|
@@ -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
|
|
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
|