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 +348 -47
- trodo/api/endpoints.py +5 -1
- trodo/api/http_client.py +30 -2
- trodo/client.py +239 -111
- trodo/otel/__init__.py +21 -0
- trodo/otel/auto_instrument.py +281 -0
- trodo/otel/context.py +44 -0
- trodo/otel/helpers.py +407 -0
- trodo/otel/processor.py +184 -0
- trodo/otel/wrap_agent.py +508 -0
- trodo/types.py +12 -68
- {trodo_python-1.2.0.dist-info → trodo_python-2.2.0.dist-info}/METADATA +183 -3
- {trodo_python-1.2.0.dist-info → trodo_python-2.2.0.dist-info}/RECORD +15 -9
- {trodo_python-1.2.0.dist-info → trodo_python-2.2.0.dist-info}/WHEEL +0 -0
- {trodo_python-1.2.0.dist-info → trodo_python-2.2.0.dist-info}/top_level.txt +0 -0
trodo/client.py
CHANGED
|
@@ -2,24 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import functools
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
6
7
|
|
|
7
8
|
from .api.http_client import HttpClient
|
|
8
9
|
from .session.session_manager import SessionManager
|
|
9
|
-
from .managers.
|
|
10
|
-
from .managers.group_manager import GroupManager, GroupProfile
|
|
10
|
+
from .managers.group_manager import GroupProfile
|
|
11
11
|
from .queue.event_queue import EventQueue
|
|
12
12
|
from .queue.batch_flusher import BatchFlusher
|
|
13
13
|
from .auto.auto_event_manager import AutoEventManager
|
|
14
14
|
from .user_context import UserContext
|
|
15
|
+
from .otel.processor import TrodoSpanProcessor
|
|
16
|
+
from .otel.wrap_agent import (
|
|
17
|
+
wrap_agent as wrap_agent_ctx,
|
|
18
|
+
span as span_ctx,
|
|
19
|
+
join_run as join_run_ctx,
|
|
20
|
+
start_run as start_run_fn,
|
|
21
|
+
end_run as end_run_fn,
|
|
22
|
+
current_run_id as _current_run_id,
|
|
23
|
+
current_span_id as _current_span_id,
|
|
24
|
+
)
|
|
25
|
+
from .otel.auto_instrument import enable_auto_instrument
|
|
26
|
+
from .otel.helpers import (
|
|
27
|
+
tool as tool_decorator,
|
|
28
|
+
track_llm_call as track_llm_call_fn,
|
|
29
|
+
fastapi_middleware as fastapi_middleware_fn,
|
|
30
|
+
propagation_headers as propagation_headers_fn,
|
|
31
|
+
)
|
|
15
32
|
from .types import (
|
|
16
33
|
ApiResult,
|
|
17
34
|
ResetResult,
|
|
18
35
|
WalletAddressResult,
|
|
19
|
-
AgentCallProps,
|
|
20
|
-
ToolUseProps,
|
|
21
|
-
AgentResponseProps,
|
|
22
|
-
AgentErrorProps,
|
|
23
36
|
FeedbackProps,
|
|
24
37
|
)
|
|
25
38
|
|
|
@@ -37,6 +50,7 @@ class TrodoClient:
|
|
|
37
50
|
auto_events: bool = False,
|
|
38
51
|
on_error: Optional[Any] = None,
|
|
39
52
|
debug: bool = False,
|
|
53
|
+
auto_instrument: bool = True,
|
|
40
54
|
) -> None:
|
|
41
55
|
if not site_id:
|
|
42
56
|
raise ValueError("trodo-python: site_id is required")
|
|
@@ -73,6 +87,10 @@ class TrodoClient:
|
|
|
73
87
|
|
|
74
88
|
self._user_context_cache: Dict[str, UserContext] = {}
|
|
75
89
|
|
|
90
|
+
self._span_processor = TrodoSpanProcessor(http_client=self._http)
|
|
91
|
+
if auto_instrument:
|
|
92
|
+
enable_auto_instrument(self._span_processor)
|
|
93
|
+
|
|
76
94
|
# --------------------------------------------------------------------------
|
|
77
95
|
# Primary pattern: for_user()
|
|
78
96
|
# --------------------------------------------------------------------------
|
|
@@ -195,124 +213,234 @@ class TrodoClient:
|
|
|
195
213
|
def flush(self) -> None:
|
|
196
214
|
if self._batch_flusher:
|
|
197
215
|
self._batch_flusher.flush()
|
|
216
|
+
self._span_processor.force_flush()
|
|
198
217
|
|
|
199
218
|
def shutdown(self) -> None:
|
|
200
219
|
self._auto_event_manager.disable()
|
|
201
220
|
if self._batch_flusher:
|
|
202
221
|
self._batch_flusher.stop()
|
|
203
222
|
self._batch_flusher.flush()
|
|
223
|
+
self._span_processor.shutdown()
|
|
204
224
|
|
|
205
225
|
# --------------------------------------------------------------------------
|
|
206
|
-
# Agent
|
|
226
|
+
# Agent Runs — Lemma-style seamless wrapping
|
|
207
227
|
# --------------------------------------------------------------------------
|
|
208
228
|
|
|
209
|
-
def
|
|
229
|
+
def wrap_agent(
|
|
210
230
|
self,
|
|
211
|
-
|
|
212
|
-
conversation_id: str,
|
|
213
|
-
message_id: str,
|
|
214
|
-
event_type: str,
|
|
231
|
+
agent_name: str,
|
|
215
232
|
distinct_id: Optional[str] = None,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
props.agent_id, props.conversation_id, props.message_id,
|
|
234
|
-
"agent_call", props.distinct_id, props.timestamp,
|
|
233
|
+
conversation_id: Optional[str] = None,
|
|
234
|
+
parent_run_id: Optional[str] = None,
|
|
235
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
236
|
+
) -> wrap_agent_ctx:
|
|
237
|
+
"""Context manager that captures the wrapped block as an agent run.
|
|
238
|
+
|
|
239
|
+
Every OTel-instrumented call (Anthropic, OpenAI, LangChain…) made
|
|
240
|
+
inside the ``with`` is auto-captured as a nested span.
|
|
241
|
+
"""
|
|
242
|
+
return wrap_agent_ctx(
|
|
243
|
+
processor=self._span_processor,
|
|
244
|
+
team_site_id=self.site_id,
|
|
245
|
+
agent_name=agent_name,
|
|
246
|
+
distinct_id=distinct_id,
|
|
247
|
+
conversation_id=conversation_id,
|
|
248
|
+
parent_run_id=parent_run_id,
|
|
249
|
+
metadata=metadata,
|
|
235
250
|
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
251
|
+
|
|
252
|
+
def agent(
|
|
253
|
+
self,
|
|
254
|
+
agent_name: str,
|
|
255
|
+
*,
|
|
256
|
+
distinct_id: Optional[str] = None,
|
|
257
|
+
conversation_id: Optional[str] = None,
|
|
258
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
259
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
260
|
+
"""Decorator form of wrap_agent."""
|
|
261
|
+
|
|
262
|
+
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
263
|
+
@functools.wraps(fn)
|
|
264
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
265
|
+
with self.wrap_agent(
|
|
266
|
+
agent_name,
|
|
267
|
+
distinct_id=distinct_id,
|
|
268
|
+
conversation_id=conversation_id,
|
|
269
|
+
metadata=metadata,
|
|
270
|
+
) as run:
|
|
271
|
+
result = fn(*args, **kwargs)
|
|
272
|
+
run.set_output(result)
|
|
273
|
+
return result
|
|
274
|
+
return wrapper
|
|
275
|
+
|
|
276
|
+
return decorator
|
|
277
|
+
|
|
278
|
+
def span(
|
|
279
|
+
self,
|
|
280
|
+
name: str,
|
|
281
|
+
*,
|
|
282
|
+
kind: str = "generic",
|
|
283
|
+
input: Any = None,
|
|
284
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
285
|
+
) -> span_ctx:
|
|
286
|
+
"""Create a nested span inside the current run."""
|
|
287
|
+
return span_ctx(name=name, kind=kind, input=input, attributes=attributes)
|
|
288
|
+
|
|
289
|
+
def start_run(
|
|
290
|
+
self,
|
|
291
|
+
agent_name: str,
|
|
292
|
+
*,
|
|
293
|
+
run_id: Optional[str] = None,
|
|
294
|
+
distinct_id: Optional[str] = None,
|
|
295
|
+
conversation_id: Optional[str] = None,
|
|
296
|
+
parent_run_id: Optional[str] = None,
|
|
297
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
298
|
+
input: Any = None,
|
|
299
|
+
) -> str:
|
|
300
|
+
"""Open a Run record outside a context manager. Returns the run_id.
|
|
301
|
+
|
|
302
|
+
Use ``end_run`` to finalise, ``join_run`` from any process to add spans.
|
|
303
|
+
"""
|
|
304
|
+
return start_run_fn(
|
|
305
|
+
processor=self._span_processor,
|
|
306
|
+
agent_name=agent_name,
|
|
307
|
+
run_id=run_id,
|
|
308
|
+
distinct_id=distinct_id,
|
|
309
|
+
conversation_id=conversation_id,
|
|
310
|
+
parent_run_id=parent_run_id,
|
|
311
|
+
metadata=metadata,
|
|
312
|
+
input=input,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def end_run(
|
|
316
|
+
self,
|
|
317
|
+
run_id: str,
|
|
318
|
+
*,
|
|
319
|
+
output: Any = None,
|
|
320
|
+
status: str = "ok",
|
|
321
|
+
error_summary: Optional[str] = None,
|
|
322
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Finalise a Run previously opened by :meth:`start_run`."""
|
|
325
|
+
end_run_fn(
|
|
326
|
+
run_id,
|
|
327
|
+
processor=self._span_processor,
|
|
328
|
+
output=output,
|
|
329
|
+
status=status,
|
|
330
|
+
error_summary=error_summary,
|
|
331
|
+
metadata=metadata,
|
|
255
332
|
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
333
|
+
|
|
334
|
+
def join_run(
|
|
335
|
+
self,
|
|
336
|
+
run_id: str,
|
|
337
|
+
parent_span_id: Optional[str] = None,
|
|
338
|
+
*,
|
|
339
|
+
name: str = "remote.handler",
|
|
340
|
+
kind: str = "agent",
|
|
341
|
+
input: Any = None,
|
|
342
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
343
|
+
) -> join_run_ctx:
|
|
344
|
+
"""Open a span on an existing run owned by a remote service.
|
|
345
|
+
|
|
346
|
+
Use this in downstream microservices that receive an inbound
|
|
347
|
+
request carrying ``X-Trodo-Run-Id`` / ``X-Trodo-Parent-Span-Id``.
|
|
348
|
+
Prefer :meth:`fastapi_middleware` to automate this.
|
|
349
|
+
"""
|
|
350
|
+
return join_run_ctx(
|
|
351
|
+
processor=self._span_processor,
|
|
352
|
+
team_site_id=self.site_id,
|
|
353
|
+
run_id=run_id,
|
|
354
|
+
parent_span_id=parent_span_id,
|
|
355
|
+
name=name,
|
|
356
|
+
kind=kind,
|
|
357
|
+
input=input,
|
|
358
|
+
attributes=attributes,
|
|
274
359
|
)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
360
|
+
|
|
361
|
+
def tool(
|
|
362
|
+
self,
|
|
363
|
+
name: Optional[str] = None,
|
|
364
|
+
*,
|
|
365
|
+
kind: str = "tool",
|
|
366
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
367
|
+
"""Decorator that wraps a function call as a tool span."""
|
|
368
|
+
return tool_decorator(name=name, kind=kind)
|
|
369
|
+
|
|
370
|
+
def track_llm_call(
|
|
371
|
+
self,
|
|
372
|
+
*,
|
|
373
|
+
model: Optional[str] = None,
|
|
374
|
+
provider: Optional[str] = None,
|
|
375
|
+
input_tokens: Optional[int] = None,
|
|
376
|
+
output_tokens: Optional[int] = None,
|
|
377
|
+
prompt: Any = None,
|
|
378
|
+
completion: Any = None,
|
|
379
|
+
temperature: Optional[float] = None,
|
|
380
|
+
cost: Optional[float] = None,
|
|
381
|
+
name: Optional[str] = None,
|
|
382
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Record an LLM span for a raw-HTTP caller (no OTel adapter)."""
|
|
385
|
+
track_llm_call_fn(
|
|
386
|
+
model=model,
|
|
387
|
+
provider=provider,
|
|
388
|
+
input_tokens=input_tokens,
|
|
389
|
+
output_tokens=output_tokens,
|
|
390
|
+
prompt=prompt,
|
|
391
|
+
completion=completion,
|
|
392
|
+
temperature=temperature,
|
|
393
|
+
cost=cost,
|
|
394
|
+
name=name,
|
|
395
|
+
metadata=metadata,
|
|
296
396
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
397
|
+
|
|
398
|
+
def fastapi_middleware(self) -> Callable:
|
|
399
|
+
"""Return a FastAPI/Starlette middleware that auto-joins runs."""
|
|
400
|
+
return fastapi_middleware_fn(self)
|
|
401
|
+
|
|
402
|
+
def propagation_headers(self) -> Dict[str, str]:
|
|
403
|
+
"""Return outbound HTTP headers carrying the current run/span id."""
|
|
404
|
+
return propagation_headers_fn()
|
|
405
|
+
|
|
406
|
+
def current_run_id(self) -> Optional[str]:
|
|
407
|
+
return _current_run_id()
|
|
408
|
+
|
|
409
|
+
def current_span_id(self) -> Optional[str]:
|
|
410
|
+
return _current_span_id()
|
|
411
|
+
|
|
412
|
+
def feedback(
|
|
413
|
+
self,
|
|
414
|
+
run_id: str,
|
|
415
|
+
satisfaction: Optional[str] = None,
|
|
416
|
+
rating: Optional[float] = None,
|
|
417
|
+
comment: Optional[str] = None,
|
|
418
|
+
feedback: Optional[str] = None,
|
|
419
|
+
distinct_id: Optional[str] = None,
|
|
420
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
421
|
+
) -> ApiResult:
|
|
422
|
+
"""Attach feedback to a completed run."""
|
|
423
|
+
if satisfaction is None and rating is None and not (comment or feedback):
|
|
424
|
+
raise ValueError(
|
|
425
|
+
"At least one of 'satisfaction', 'rating', or 'comment' is required"
|
|
426
|
+
)
|
|
427
|
+
payload: Dict[str, Any] = {
|
|
428
|
+
"satisfaction": satisfaction,
|
|
429
|
+
"rating": rating,
|
|
430
|
+
"comment": comment if comment is not None else feedback,
|
|
431
|
+
"distinct_id": distinct_id,
|
|
432
|
+
"attributes": metadata or {},
|
|
433
|
+
}
|
|
434
|
+
return self._http.post_run_feedback(run_id, payload)
|
|
435
|
+
|
|
436
|
+
def feedback_props(self, run_id: str, props: FeedbackProps) -> ApiResult:
|
|
437
|
+
"""Convenience — pass a FeedbackProps dataclass."""
|
|
438
|
+
return self.feedback(
|
|
439
|
+
run_id,
|
|
440
|
+
satisfaction=props.satisfaction,
|
|
441
|
+
rating=props.rating,
|
|
442
|
+
comment=props.comment,
|
|
443
|
+
feedback=props.feedback,
|
|
444
|
+
distinct_id=props.distinct_id,
|
|
445
|
+
metadata=props.metadata,
|
|
314
446
|
)
|
|
315
|
-
payload["feedback"] = props.feedback
|
|
316
|
-
if props.properties:
|
|
317
|
-
payload.update(props.properties)
|
|
318
|
-
self._http.post_agent_event(payload)
|
trodo/otel/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""OpenTelemetry-compatible run/span pipeline for trodo-python."""
|
|
2
|
+
|
|
3
|
+
from .context import ActiveSpanContext, get_active_context, run_with_context
|
|
4
|
+
from .processor import TrodoSpanProcessor, TrodoRun, TrodoSpan
|
|
5
|
+
from .wrap_agent import wrap_agent, span, SpanHandle, RunHandle
|
|
6
|
+
from .auto_instrument import enable_auto_instrument, otel_span_to_trodo_span
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ActiveSpanContext",
|
|
10
|
+
"get_active_context",
|
|
11
|
+
"run_with_context",
|
|
12
|
+
"TrodoSpanProcessor",
|
|
13
|
+
"TrodoRun",
|
|
14
|
+
"TrodoSpan",
|
|
15
|
+
"wrap_agent",
|
|
16
|
+
"span",
|
|
17
|
+
"SpanHandle",
|
|
18
|
+
"RunHandle",
|
|
19
|
+
"enable_auto_instrument",
|
|
20
|
+
"otel_span_to_trodo_span",
|
|
21
|
+
]
|