trodo-python 1.2.0__py3-none-any.whl → 2.2.0__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
@@ -1,35 +1,71 @@
1
1
  """
2
2
  trodo-python — Trodo Analytics SDK for Python
3
3
 
4
- Usage (module-level singleton):
4
+ Quick start (any stack):
5
5
  import trodo
6
- trodo.init(site_id='your-site-id')
7
- user = trodo.for_user('user-123')
8
- user.track('purchase_completed', {'amount': 99.99})
9
-
10
- Usage (class):
11
- from trodo import TrodoClient
12
- client = TrodoClient(site_id='your-site-id')
13
- user = client.for_user('user-123')
14
- user.track('purchase_completed', {'amount': 99.99})
6
+ trodo.init(site_id='your-site-id') # auto-instrument on by default
7
+
8
+ # Anthropic / OpenAI / LangChain / LlamaIndex — nothing else to do.
9
+ with trodo.wrap_agent('customer-support',
10
+ distinct_id=uid, conversation_id=conv) as run:
11
+ result = agent.run(query)
12
+ run.set_output(result)
13
+
14
+ trodo.feedback(run.run_id, satisfaction='positive', rating=5)
15
+
16
+ Raw-HTTP LLM caller (no OTel integration for your client):
17
+ resp = httpx.post(..., json=body).json()
18
+ trodo.track_llm_call(
19
+ model='gemini-2.5-flash', provider='google',
20
+ input_tokens=resp['usageMetadata']['promptTokenCount'],
21
+ output_tokens=resp['usageMetadata']['candidatesTokenCount'],
22
+ prompt=body, completion=resp,
23
+ )
24
+
25
+ Custom tool:
26
+ @trodo.tool()
27
+ def run_funnel_query(team_id, preset): ...
28
+
29
+ Downstream microservice (join the caller's run instead of making a new one):
30
+ # In FastAPI:
31
+ app.middleware('http')(trodo.fastapi_middleware())
32
+
33
+ # Or manually:
34
+ with trodo.join_run(
35
+ run_id=headers['X-Trodo-Run-Id'],
36
+ parent_span_id=headers['X-Trodo-Parent-Span-Id'],
37
+ ):
38
+ ...
15
39
  """
16
40
 
17
41
  from __future__ import annotations
18
42
 
19
- from typing import Any, Dict, List, Optional, Union
43
+ __version__ = "2.2.0"
44
+
45
+ from typing import Any, Callable, Dict, List, Optional, Union
20
46
 
21
47
  from .client import TrodoClient
22
48
  from .user_context import UserContext
23
49
  from .managers.group_manager import GroupProfile
50
+ from .otel.wrap_agent import (
51
+ wrap_agent as _wrap_agent_ctx,
52
+ span as _span_ctx,
53
+ join_run as _join_run_ctx,
54
+ RunHandle,
55
+ SpanHandle,
56
+ )
24
57
  from .types import (
25
58
  ApiResult, ResetResult, WalletAddressResult,
26
- AgentCallProps, ToolUseProps, AgentResponseProps, AgentErrorProps, FeedbackProps,
59
+ FeedbackProps,
27
60
  )
28
61
 
29
62
  __all__ = [
30
63
  "TrodoClient",
31
64
  "UserContext",
32
65
  "GroupProfile",
66
+ "RunHandle",
67
+ "SpanHandle",
68
+ "FeedbackProps",
33
69
  "init",
34
70
  "for_user",
35
71
  "track",
@@ -40,17 +76,25 @@ __all__ = [
40
76
  "disable_auto_events",
41
77
  "flush",
42
78
  "shutdown",
43
- # Agent analytics
44
- "AgentCallProps",
45
- "ToolUseProps",
46
- "AgentResponseProps",
47
- "AgentErrorProps",
48
- "FeedbackProps",
49
- "track_agent_call",
50
- "track_tool_use",
51
- "track_agent_response",
52
- "track_agent_error",
53
- "track_feedback",
79
+ # Agent runs
80
+ "wrap_agent",
81
+ "agent",
82
+ "span",
83
+ "tool",
84
+ "trace",
85
+ "llm",
86
+ "retrieval",
87
+ "join_run",
88
+ "start_run",
89
+ "end_run",
90
+ "track_llm_call",
91
+ "feedback",
92
+ "get_tracer",
93
+ # Cross-service propagation
94
+ "fastapi_middleware",
95
+ "propagation_headers",
96
+ "current_run_id",
97
+ "current_span_id",
54
98
  ]
55
99
 
56
100
  # ============================================================================
@@ -68,6 +112,10 @@ def _get_client() -> TrodoClient:
68
112
  return _client
69
113
 
70
114
 
115
+ def _maybe_client() -> Optional[TrodoClient]:
116
+ return _client
117
+
118
+
71
119
  def init(
72
120
  site_id: str,
73
121
  api_base: str = "https://sdkapi.trodo.ai",
@@ -79,8 +127,15 @@ def init(
79
127
  auto_events: bool = False,
80
128
  on_error: Optional[Any] = None,
81
129
  debug: bool = False,
130
+ auto_instrument: bool = True,
82
131
  ) -> TrodoClient:
83
- """Initialise the singleton SDK instance."""
132
+ """Initialise the singleton SDK instance.
133
+
134
+ ``auto_instrument=True`` (default) registers OTel adapters for
135
+ Anthropic, OpenAI, LangChain, LlamaIndex, Google Generative AI and
136
+ any other installed opentelemetry-instrumentation-* package, so
137
+ LLM calls emit token/cost spans with no further code.
138
+ """
84
139
  global _client
85
140
  _client = TrodoClient(
86
141
  site_id=site_id,
@@ -93,6 +148,7 @@ def init(
93
148
  auto_events=auto_events,
94
149
  on_error=on_error,
95
150
  debug=debug,
151
+ auto_instrument=auto_instrument,
96
152
  )
97
153
  return _client
98
154
 
@@ -101,7 +157,6 @@ def for_user(
101
157
  distinct_id: str,
102
158
  session_id: Optional[str] = None,
103
159
  ) -> UserContext:
104
- """Return a UserContext bound to the given distinctId."""
105
160
  return _get_client().for_user(distinct_id, session_id)
106
161
 
107
162
 
@@ -111,22 +166,18 @@ def track(
111
166
  properties: Optional[Dict[str, Any]] = None,
112
167
  category: str = "custom",
113
168
  ) -> None:
114
- """Track an event for a user (direct-call pattern)."""
115
169
  _get_client().track(distinct_id, event_name, properties, category)
116
170
 
117
171
 
118
172
  def identify(identify_id: str, session_id: Optional[str] = None) -> UserContext:
119
- """Create or retrieve a UserContext for an identified user. Fires identify API on first call."""
120
173
  return _get_client().identify(identify_id, session_id)
121
174
 
122
175
 
123
176
  def wallet_address(distinct_id: str, wallet_addr: str) -> WalletAddressResult:
124
- """Associate a wallet address with a user."""
125
177
  return _get_client().wallet_address(distinct_id, wallet_addr)
126
178
 
127
179
 
128
180
  def reset(distinct_id: str) -> ResetResult:
129
- """Reset a user's session."""
130
181
  return _get_client().reset(distinct_id)
131
182
 
132
183
 
@@ -139,39 +190,289 @@ def disable_auto_events() -> None:
139
190
 
140
191
 
141
192
  def flush() -> None:
142
- """Flush any queued batch events."""
143
193
  _get_client().flush()
144
194
 
145
195
 
146
196
  def shutdown() -> None:
147
- """Flush, stop timers, and disable auto events."""
148
197
  _get_client().shutdown()
149
198
 
150
199
 
151
200
  # ----------------------------------------------------------------------------
152
- # Agent Analytics — singleton wrappers
201
+ # Agent Runs
153
202
  # ----------------------------------------------------------------------------
154
203
 
155
- def track_agent_call(props: AgentCallProps) -> None:
156
- """Track an LLM invocation / inbound message."""
157
- _get_client().track_agent_call(props)
204
+ def wrap_agent(
205
+ agent_name: str,
206
+ *,
207
+ distinct_id: Optional[str] = None,
208
+ conversation_id: Optional[str] = None,
209
+ parent_run_id: Optional[str] = None,
210
+ metadata: Optional[Dict[str, Any]] = None,
211
+ ) -> _wrap_agent_ctx:
212
+ """Wrap a block as an agent run — returns a context manager yielding RunHandle."""
213
+ return _get_client().wrap_agent(
214
+ agent_name,
215
+ distinct_id=distinct_id,
216
+ conversation_id=conversation_id,
217
+ parent_run_id=parent_run_id,
218
+ metadata=metadata,
219
+ )
220
+
221
+
222
+ def agent(
223
+ agent_name: str,
224
+ *,
225
+ distinct_id: Optional[str] = None,
226
+ conversation_id: Optional[str] = None,
227
+ metadata: Optional[Dict[str, Any]] = None,
228
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
229
+ """Decorator form of wrap_agent."""
230
+ return _get_client().agent(
231
+ agent_name,
232
+ distinct_id=distinct_id,
233
+ conversation_id=conversation_id,
234
+ metadata=metadata,
235
+ )
236
+
237
+
238
+ def span(
239
+ name: str,
240
+ *,
241
+ kind: str = "generic",
242
+ input: Any = None,
243
+ attributes: Optional[Dict[str, Any]] = None,
244
+ ) -> _span_ctx:
245
+ """Nested span inside the current run."""
246
+ return _get_client().span(name, kind=kind, input=input, attributes=attributes)
247
+
248
+
249
+ def join_run(
250
+ run_id: str,
251
+ parent_span_id: Optional[str] = None,
252
+ *,
253
+ name: str = "remote.handler",
254
+ kind: str = "agent",
255
+ input: Any = None,
256
+ attributes: Optional[Dict[str, Any]] = None,
257
+ ) -> _join_run_ctx:
258
+ """Open a span on an existing run owned by a remote service."""
259
+ return _get_client().join_run(
260
+ run_id,
261
+ parent_span_id,
262
+ name=name,
263
+ kind=kind,
264
+ input=input,
265
+ attributes=attributes,
266
+ )
267
+
268
+
269
+ def start_run(
270
+ agent_name: str,
271
+ *,
272
+ run_id: Optional[str] = None,
273
+ distinct_id: Optional[str] = None,
274
+ conversation_id: Optional[str] = None,
275
+ parent_run_id: Optional[str] = None,
276
+ metadata: Optional[Dict[str, Any]] = None,
277
+ input: Any = None,
278
+ ) -> str:
279
+ """Open a Run record without a context manager.
280
+
281
+ Pairs with :func:`end_run` for sessions that span multiple processes or
282
+ HTTP requests. Returns the run_id (caller-supplied or freshly minted).
283
+ Between start_run and end_run any process can use ``join_run(run_id, ...)``
284
+ to add spans.
285
+ """
286
+ return _get_client().start_run(
287
+ agent_name,
288
+ run_id=run_id,
289
+ distinct_id=distinct_id,
290
+ conversation_id=conversation_id,
291
+ parent_run_id=parent_run_id,
292
+ metadata=metadata,
293
+ input=input,
294
+ )
295
+
296
+
297
+ def end_run(
298
+ run_id: str,
299
+ *,
300
+ output: Any = None,
301
+ status: str = "ok",
302
+ error_summary: Optional[str] = None,
303
+ metadata: Optional[Dict[str, Any]] = None,
304
+ ) -> None:
305
+ """Finalise a Run opened by :func:`start_run`."""
306
+ _get_client().end_run(
307
+ run_id,
308
+ output=output,
309
+ status=status,
310
+ error_summary=error_summary,
311
+ metadata=metadata,
312
+ )
313
+
314
+
315
+ def tool(
316
+ name: Any = None,
317
+ fn: Optional[Callable[..., Any]] = None,
318
+ *,
319
+ kind: str = "tool",
320
+ ) -> Any:
321
+ """Wrap a function as a tool span — dual-form helper and decorator.
322
+
323
+ Helper form::
324
+
325
+ run_funnel_query = trodo.tool('run_funnel_query', run_funnel_query)
326
+
327
+ Decorator form (backward-compatible)::
328
+
329
+ @trodo.tool()
330
+ def run_funnel_query(team_id, preset): ...
331
+
332
+ @trodo.tool(name='custom-name')
333
+ async def fetch(...): ...
334
+ """
335
+ # Deferred import: allow @trodo.tool() at import-time before init().
336
+ from .otel.helpers import tool as _tool_helper
337
+ return _tool_helper(name, fn, kind=kind)
338
+
339
+
340
+ def trace(
341
+ name: Any = None,
342
+ fn: Optional[Callable[..., Any]] = None,
343
+ ) -> Any:
344
+ """Wrap a function as a generic span — dual-form helper and decorator."""
345
+ from .otel.helpers import trace as _trace_helper
346
+ return _trace_helper(name, fn)
347
+
348
+
349
+ def llm(
350
+ name: Any = None,
351
+ fn: Optional[Callable[..., Any]] = None,
352
+ *,
353
+ model: Optional[str] = None,
354
+ provider: Optional[str] = None,
355
+ temperature: Optional[float] = None,
356
+ extract_usage: Optional[Callable[[Any], Any]] = None,
357
+ ) -> Any:
358
+ """Wrap an LLM call — auto-captures tokens from OpenAI/Anthropic/Gemini
359
+ response shapes. Dual-form helper and decorator."""
360
+ from .otel.helpers import llm as _llm_helper
361
+ return _llm_helper(
362
+ name,
363
+ fn,
364
+ model=model,
365
+ provider=provider,
366
+ temperature=temperature,
367
+ extract_usage=extract_usage,
368
+ )
369
+
370
+
371
+ def retrieval(
372
+ name: Any = None,
373
+ fn: Optional[Callable[..., Any]] = None,
374
+ ) -> Any:
375
+ """Wrap a retriever / vector search as a kind='retrieval' span."""
376
+ from .otel.helpers import retrieval as _retrieval_helper
377
+ return _retrieval_helper(name, fn)
378
+
379
+
380
+ def get_tracer(name: str = "trodo") -> Any:
381
+ """Return a raw OTel tracer — the Trodo processor is already subscribed.
382
+
383
+ Use for advanced cases where you want to emit spans with the raw OTel
384
+ API; they'll be captured and forwarded to Trodo like any other span.
385
+
386
+ tracer = trodo.get_tracer('my.module')
387
+ with tracer.start_as_current_span('custom') as sp:
388
+ sp.set_attribute('foo', 'bar')
389
+ """
390
+ try:
391
+ from opentelemetry import trace as _otel_trace # type: ignore
392
+ except Exception as e: # pragma: no cover
393
+ raise RuntimeError(
394
+ "trodo.get_tracer requires the opentelemetry-api package"
395
+ ) from e
396
+ return _otel_trace.get_tracer(name)
397
+
398
+
399
+ def track_llm_call(
400
+ *,
401
+ model: Optional[str] = None,
402
+ provider: Optional[str] = None,
403
+ input_tokens: Optional[int] = None,
404
+ output_tokens: Optional[int] = None,
405
+ prompt: Any = None,
406
+ completion: Any = None,
407
+ temperature: Optional[float] = None,
408
+ cost: Optional[float] = None,
409
+ name: Optional[str] = None,
410
+ metadata: Optional[Dict[str, Any]] = None,
411
+ ) -> None:
412
+ """Record a one-shot LLM span for a raw-HTTP caller."""
413
+ from .otel.helpers import track_llm_call as _fn
414
+ _fn(
415
+ model=model,
416
+ provider=provider,
417
+ input_tokens=input_tokens,
418
+ output_tokens=output_tokens,
419
+ prompt=prompt,
420
+ completion=completion,
421
+ temperature=temperature,
422
+ cost=cost,
423
+ name=name,
424
+ metadata=metadata,
425
+ )
426
+
427
+
428
+ def feedback(
429
+ run_id: str,
430
+ *,
431
+ satisfaction: Optional[str] = None,
432
+ rating: Optional[float] = None,
433
+ comment: Optional[str] = None,
434
+ feedback: Optional[str] = None,
435
+ distinct_id: Optional[str] = None,
436
+ metadata: Optional[Dict[str, Any]] = None,
437
+ ) -> ApiResult:
438
+ """Attach feedback to a completed run."""
439
+ return _get_client().feedback(
440
+ run_id,
441
+ satisfaction=satisfaction,
442
+ rating=rating,
443
+ comment=comment,
444
+ feedback=feedback,
445
+ distinct_id=distinct_id,
446
+ metadata=metadata,
447
+ )
448
+
449
+
450
+ # ----------------------------------------------------------------------------
451
+ # Cross-service propagation
452
+ # ----------------------------------------------------------------------------
158
453
 
454
+ def fastapi_middleware() -> Callable:
455
+ """Return a FastAPI/Starlette middleware that auto-joins inbound runs.
159
456
 
160
- def track_tool_use(props: ToolUseProps) -> None:
161
- """Track a tool invocation within an agent turn."""
162
- _get_client().track_tool_use(props)
457
+ Usage:
458
+ app = FastAPI()
459
+ trodo.init(site_id='...')
460
+ app.middleware('http')(trodo.fastapi_middleware())
461
+ """
462
+ return _get_client().fastapi_middleware()
163
463
 
164
464
 
165
- def track_agent_response(props: AgentResponseProps) -> None:
166
- """Track an LLM response / completion."""
167
- _get_client().track_agent_response(props)
465
+ def propagation_headers() -> Dict[str, str]:
466
+ """Return outbound HTTP headers carrying the current run/span id."""
467
+ from .otel.helpers import propagation_headers as _fn
468
+ return _fn()
168
469
 
169
470
 
170
- def track_agent_error(props: AgentErrorProps) -> None:
171
- """Track an error during an agent turn."""
172
- _get_client().track_agent_error(props)
471
+ def current_run_id() -> Optional[str]:
472
+ from .otel.wrap_agent import current_run_id as _fn
473
+ return _fn()
173
474
 
174
475
 
175
- def track_feedback(props: FeedbackProps) -> None:
176
- """Track a user feedback reaction on an agent response."""
177
- _get_client().track_feedback(props)
476
+ def current_span_id() -> Optional[str]:
477
+ from .otel.wrap_agent import current_span_id as _fn
478
+ return _fn()
trodo/api/endpoints.py CHANGED
@@ -1,5 +1,4 @@
1
1
  TRACK = "/api/sdk/track"
2
- TRACK_AGENT = "/api/sdk/track-agent"
3
2
  EVENTS = "/api/events"
4
3
  EVENTS_BULK = "/api/events/bulk"
5
4
  IDENTIFY = "/api/sdk/identify"
@@ -19,3 +18,8 @@ GROUPS_SET = "/api/sdk/groups/set_group"
19
18
  GROUPS_ADD = "/api/sdk/groups/add_group"
20
19
  GROUPS_REMOVE = "/api/sdk/groups/remove_group"
21
20
  GROUPS_PROFILE = "/api/sdk/groups/profile"
21
+ # Agent run tracking (Lemma-style, run-centric)
22
+ RUNS_INGEST = "/api/sdk/runs/ingest"
23
+ RUNS_START = "/api/sdk/runs/start"
24
+ RUNS_BASE = "/api/sdk/runs" # /runs/{run_id}/end, /spans, /feedback
25
+ OTLP_TRACES = "/api/sdk/otel/v1/traces"
trodo/api/http_client.py CHANGED
@@ -86,5 +86,33 @@ class HttpClient:
86
86
  def post_group(self, path: str, payload: Dict[str, Any]) -> ApiResult:
87
87
  return self._request(path, payload)
88
88
 
89
- def post_agent_event(self, payload: Dict[str, Any]) -> ApiResult:
90
- return self._request("/api/sdk/track-agent", payload)
89
+ # ------------------------------------------------------------------
90
+ # Agent Runs (Lemma-style)
91
+ # ------------------------------------------------------------------
92
+
93
+ def post_run_ingest(self, payload: Dict[str, Any]) -> ApiResult:
94
+ return self._request("/api/sdk/runs/ingest", payload)
95
+
96
+ def post_run_start(self, payload: Dict[str, Any]) -> ApiResult:
97
+ return self._request("/api/sdk/runs/start", payload)
98
+
99
+ def post_run_end(self, run_id: str, payload: Dict[str, Any]) -> ApiResult:
100
+ from urllib.parse import quote
101
+ return self._request(f"/api/sdk/runs/{quote(run_id, safe='')}/end", payload)
102
+
103
+ def post_spans_append(self, run_id: str, spans: list) -> ApiResult:
104
+ from urllib.parse import quote
105
+ return self._request(
106
+ f"/api/sdk/runs/{quote(run_id, safe='')}/spans",
107
+ {"spans": spans},
108
+ )
109
+
110
+ def post_run_feedback(self, run_id: str, payload: Dict[str, Any]) -> ApiResult:
111
+ from urllib.parse import quote
112
+ return self._request(
113
+ f"/api/sdk/runs/{quote(run_id, safe='')}/feedback",
114
+ payload,
115
+ )
116
+
117
+ def post_otlp_traces(self, payload: Dict[str, Any]) -> ApiResult:
118
+ return self._request("/api/sdk/otel/v1/traces", payload)