trodo-python 2.10.9__tar.gz → 2.10.11__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.9 → trodo_python-2.10.11}/PKG-INFO +1 -1
- {trodo_python-2.10.9 → trodo_python-2.10.11}/pyproject.toml +1 -1
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_auto_instrument_fixes.py +68 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/__init__.py +1 -1
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/auto_instrument.py +97 -20
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo_python.egg-info/PKG-INFO +1 -1
- {trodo_python-2.10.9 → trodo_python-2.10.11}/README.md +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/setup.cfg +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_anon_distinct_id 2.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_anon_distinct_id.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_cross_process_session.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_end_run.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_error_enrichment.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_llm_usage_cost.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_processor_methods.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_register_otel.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_start_run.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/tests/test_wrap_agent_unchanged.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/api/__init__.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/api/async_client.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/api/endpoints.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/api/http_client.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/auto/__init__.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/auto/auto_event_manager.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/client.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/managers/__init__.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/managers/group_manager.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/managers/people_manager.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/__init__.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/context.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/helpers.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/processor.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/register.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/transport.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/otel/wrap_agent.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/queue/__init__.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/queue/batch_flusher.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/queue/event_queue.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/session/__init__.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/session/server_session.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/session/session_manager.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/types.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo/user_context.py +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo_python.egg-info/SOURCES.txt +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo_python.egg-info/dependency_links.txt +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo_python.egg-info/requires.txt +0 -0
- {trodo_python-2.10.9 → trodo_python-2.10.11}/trodo_python.egg-info/top_level.txt +0 -0
|
@@ -159,3 +159,71 @@ 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_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
|
+
|
|
223
|
+
def test_langfuse_traceloop_single(self):
|
|
224
|
+
s = otel_span_to_trodo_span(self._span({
|
|
225
|
+
"langfuse.observation.input": "user asked X",
|
|
226
|
+
"traceloop.entity.output": '{"result":"Y"}',
|
|
227
|
+
}))
|
|
228
|
+
assert s.input == "user asked X"
|
|
229
|
+
assert "Y" in s.output
|
|
@@ -67,6 +67,100 @@ 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", "gen_ai.tool.call.arguments",
|
|
86
|
+
"ai.toolCall.args", "input", "prompt",
|
|
87
|
+
]
|
|
88
|
+
_OUTPUT_SINGLE_KEYS = [
|
|
89
|
+
"gen_ai.output.messages", "ai.response.text", "ai.response.object", "ai.response.toolCalls",
|
|
90
|
+
"gen_ai.completion", "output.value", "traceloop.entity.output", "langfuse.observation.output",
|
|
91
|
+
"mlflow.spanOutputs", "llm.output_messages", "llm.completions", "gen_ai.tool.output",
|
|
92
|
+
"gen_ai.tool.call.result", "ai.toolCall.result", "output", "completion",
|
|
93
|
+
]
|
|
94
|
+
_INPUT_INDEXED_BASES = ["gen_ai.prompt", "llm.input_messages"]
|
|
95
|
+
_OUTPUT_INDEXED_BASES = ["gen_ai.completion", "llm.output_messages"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _first_single(attrs: Dict[str, Any], keys) -> Optional[Any]:
|
|
99
|
+
for k in keys:
|
|
100
|
+
v = attrs.get(k)
|
|
101
|
+
if v is None:
|
|
102
|
+
continue
|
|
103
|
+
if isinstance(v, str):
|
|
104
|
+
if v:
|
|
105
|
+
return v
|
|
106
|
+
continue
|
|
107
|
+
return v # dict/list — keep structure
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _set_nested(obj: Dict[str, Any], path: str, val: Any) -> None:
|
|
112
|
+
parts = path.split(".")
|
|
113
|
+
cur = obj
|
|
114
|
+
for p in parts[:-1]:
|
|
115
|
+
nxt = cur.get(p)
|
|
116
|
+
if not isinstance(nxt, dict):
|
|
117
|
+
nxt = {}
|
|
118
|
+
cur[p] = nxt
|
|
119
|
+
cur = nxt
|
|
120
|
+
cur[parts[-1]] = val
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _reconstruct_indexed(attrs: Dict[str, Any], bases) -> Optional[str]:
|
|
124
|
+
import json as _json
|
|
125
|
+
for base in bases:
|
|
126
|
+
prefix = base + "."
|
|
127
|
+
by_index: Dict[int, Dict[str, Any]] = {}
|
|
128
|
+
for key, val in attrs.items():
|
|
129
|
+
if not key.startswith(prefix):
|
|
130
|
+
continue
|
|
131
|
+
rest = key[len(prefix):]
|
|
132
|
+
dot = rest.find(".")
|
|
133
|
+
if dot < 0:
|
|
134
|
+
continue
|
|
135
|
+
idx_str = rest[:dot]
|
|
136
|
+
if not idx_str.isdigit():
|
|
137
|
+
continue
|
|
138
|
+
idx = int(idx_str)
|
|
139
|
+
by_index.setdefault(idx, {})
|
|
140
|
+
_set_nested(by_index[idx], rest[dot + 1:], val)
|
|
141
|
+
if by_index:
|
|
142
|
+
arr = [by_index[i] for i in sorted(by_index)]
|
|
143
|
+
return _json.dumps(arr)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _extract_input(attrs: Dict[str, Any]) -> Optional[Any]:
|
|
148
|
+
if not isinstance(attrs, dict):
|
|
149
|
+
return None
|
|
150
|
+
single = _first_single(attrs, _INPUT_SINGLE_KEYS)
|
|
151
|
+
if single is not None:
|
|
152
|
+
return single
|
|
153
|
+
return _reconstruct_indexed(attrs, _INPUT_INDEXED_BASES)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _extract_output(attrs: Dict[str, Any]) -> Optional[Any]:
|
|
157
|
+
if not isinstance(attrs, dict):
|
|
158
|
+
return None
|
|
159
|
+
single = _first_single(attrs, _OUTPUT_SINGLE_KEYS)
|
|
160
|
+
if single is not None:
|
|
161
|
+
return single
|
|
162
|
+
return _reconstruct_indexed(attrs, _OUTPUT_INDEXED_BASES)
|
|
163
|
+
|
|
70
164
|
|
|
71
165
|
def _span_id_to_uuid(span_id: Optional[str]) -> Optional[str]:
|
|
72
166
|
"""Convert an OTel hex span id into UUID format. Mirrors trodo-node's
|
|
@@ -206,26 +300,9 @@ def otel_span_to_trodo_span(otel_span: Any) -> Optional[TrodoSpan]:
|
|
|
206
300
|
or attrs.get("ai.response.model")
|
|
207
301
|
)
|
|
208
302
|
provider = attrs.get("gen_ai.system") or attrs.get("llm.vendor") or attrs.get("ai.model.provider")
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
)
|
|
303
|
+
# Convention-agnostic — see _extract_input/_extract_output above.
|
|
304
|
+
prompt = _extract_input(attrs)
|
|
305
|
+
completion = _extract_output(attrs)
|
|
229
306
|
tool_name = (
|
|
230
307
|
attrs.get("gen_ai.tool.name")
|
|
231
308
|
or attrs.get("ai.toolCall.name")
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|