trodo-python 2.1.0__py3-none-any.whl → 2.3.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 +109 -1
- trodo/api/http_client.py +5 -0
- trodo/client.py +107 -0
- trodo/otel/helpers.py +90 -0
- trodo/otel/processor.py +19 -0
- trodo/otel/wrap_agent.py +70 -0
- {trodo_python-2.1.0.dist-info → trodo_python-2.3.0.dist-info}/METADATA +29 -1
- {trodo_python-2.1.0.dist-info → trodo_python-2.3.0.dist-info}/RECORD +10 -10
- {trodo_python-2.1.0.dist-info → trodo_python-2.3.0.dist-info}/WHEEL +0 -0
- {trodo_python-2.1.0.dist-info → trodo_python-2.3.0.dist-info}/top_level.txt +0 -0
trodo/__init__.py
CHANGED
|
@@ -40,7 +40,7 @@ Downstream microservice (join the caller's run instead of making a new one):
|
|
|
40
40
|
|
|
41
41
|
from __future__ import annotations
|
|
42
42
|
|
|
43
|
-
__version__ = "2.
|
|
43
|
+
__version__ = "2.3.0"
|
|
44
44
|
|
|
45
45
|
from typing import Any, Callable, Dict, List, Optional, Union
|
|
46
46
|
|
|
@@ -85,7 +85,10 @@ __all__ = [
|
|
|
85
85
|
"llm",
|
|
86
86
|
"retrieval",
|
|
87
87
|
"join_run",
|
|
88
|
+
"start_run",
|
|
89
|
+
"end_run",
|
|
88
90
|
"track_llm_call",
|
|
91
|
+
"track_mcp",
|
|
89
92
|
"feedback",
|
|
90
93
|
"get_tracer",
|
|
91
94
|
# Cross-service propagation
|
|
@@ -264,6 +267,52 @@ def join_run(
|
|
|
264
267
|
)
|
|
265
268
|
|
|
266
269
|
|
|
270
|
+
def start_run(
|
|
271
|
+
agent_name: str,
|
|
272
|
+
*,
|
|
273
|
+
run_id: Optional[str] = None,
|
|
274
|
+
distinct_id: Optional[str] = None,
|
|
275
|
+
conversation_id: Optional[str] = None,
|
|
276
|
+
parent_run_id: Optional[str] = None,
|
|
277
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
278
|
+
input: Any = None,
|
|
279
|
+
) -> str:
|
|
280
|
+
"""Open a Run record without a context manager.
|
|
281
|
+
|
|
282
|
+
Pairs with :func:`end_run` for sessions that span multiple processes or
|
|
283
|
+
HTTP requests. Returns the run_id (caller-supplied or freshly minted).
|
|
284
|
+
Between start_run and end_run any process can use ``join_run(run_id, ...)``
|
|
285
|
+
to add spans.
|
|
286
|
+
"""
|
|
287
|
+
return _get_client().start_run(
|
|
288
|
+
agent_name,
|
|
289
|
+
run_id=run_id,
|
|
290
|
+
distinct_id=distinct_id,
|
|
291
|
+
conversation_id=conversation_id,
|
|
292
|
+
parent_run_id=parent_run_id,
|
|
293
|
+
metadata=metadata,
|
|
294
|
+
input=input,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def end_run(
|
|
299
|
+
run_id: str,
|
|
300
|
+
*,
|
|
301
|
+
output: Any = None,
|
|
302
|
+
status: str = "ok",
|
|
303
|
+
error_summary: Optional[str] = None,
|
|
304
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Finalise a Run opened by :func:`start_run`."""
|
|
307
|
+
_get_client().end_run(
|
|
308
|
+
run_id,
|
|
309
|
+
output=output,
|
|
310
|
+
status=status,
|
|
311
|
+
error_summary=error_summary,
|
|
312
|
+
metadata=metadata,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
267
316
|
def tool(
|
|
268
317
|
name: Any = None,
|
|
269
318
|
fn: Optional[Callable[..., Any]] = None,
|
|
@@ -377,6 +426,65 @@ def track_llm_call(
|
|
|
377
426
|
)
|
|
378
427
|
|
|
379
428
|
|
|
429
|
+
def track_mcp(
|
|
430
|
+
*,
|
|
431
|
+
tool: str,
|
|
432
|
+
distinct_id: str,
|
|
433
|
+
input: Any = None,
|
|
434
|
+
output: Any = None,
|
|
435
|
+
error: Optional[str] = None,
|
|
436
|
+
duration_ms: Optional[int] = None,
|
|
437
|
+
session_id: Optional[str] = None,
|
|
438
|
+
client_label: Optional[str] = None,
|
|
439
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
440
|
+
agent_name: str = "MCP",
|
|
441
|
+
started_at: Optional[str] = None,
|
|
442
|
+
ended_at: Optional[str] = None,
|
|
443
|
+
) -> str:
|
|
444
|
+
"""Record one MCP tool call as a runless span (no parent run).
|
|
445
|
+
|
|
446
|
+
Use this from inside an MCP server's ``tools/call`` handler. The MCP
|
|
447
|
+
server proxies tool calls but never sees the user's prompt or the
|
|
448
|
+
LLM's final answer, so a parent run carries no analytical value.
|
|
449
|
+
Each call to ``track_mcp`` writes one self-contained span row tagged
|
|
450
|
+
with ``agent_name='MCP'`` (overridable), ``distinct_id`` (the user),
|
|
451
|
+
and ``conversation_id`` (the MCP-Session-Id, auto-uuid'd if omitted).
|
|
452
|
+
|
|
453
|
+
Required:
|
|
454
|
+
tool: the tool name. Becomes ``name='tool.<tool>'``.
|
|
455
|
+
distinct_id: end-user attribution (email or stable user id).
|
|
456
|
+
|
|
457
|
+
Optional:
|
|
458
|
+
input, output: full args + result. Truncated at 64KB by the SDK.
|
|
459
|
+
error: if set, marks ``status='error'``.
|
|
460
|
+
duration_ms: tool wall time. Defaults to 0 if omitted.
|
|
461
|
+
session_id: the ``Mcp-Session-Id`` from the client; auto-uuid'd
|
|
462
|
+
if omitted (each call becomes its own grouping bucket).
|
|
463
|
+
client_label: 'anthropic' / 'cursor' / 'chatgpt' / etc.; merged
|
|
464
|
+
into attributes as ``mcp_client_label``.
|
|
465
|
+
attributes: free-form extras.
|
|
466
|
+
agent_name: defaults to 'MCP'; override only for custom tags.
|
|
467
|
+
started_at / ended_at: ISO 8601; defaults derived from now() and
|
|
468
|
+
``duration_ms`` so callers usually skip them.
|
|
469
|
+
|
|
470
|
+
Returns the span_id (str) for caller-side correlation/logging.
|
|
471
|
+
"""
|
|
472
|
+
return _get_client().track_mcp(
|
|
473
|
+
tool=tool,
|
|
474
|
+
distinct_id=distinct_id,
|
|
475
|
+
input=input,
|
|
476
|
+
output=output,
|
|
477
|
+
error=error,
|
|
478
|
+
duration_ms=duration_ms,
|
|
479
|
+
session_id=session_id,
|
|
480
|
+
client_label=client_label,
|
|
481
|
+
attributes=attributes,
|
|
482
|
+
agent_name=agent_name,
|
|
483
|
+
started_at=started_at,
|
|
484
|
+
ended_at=ended_at,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
380
488
|
def feedback(
|
|
381
489
|
run_id: str,
|
|
382
490
|
*,
|
trodo/api/http_client.py
CHANGED
|
@@ -107,6 +107,11 @@ class HttpClient:
|
|
|
107
107
|
{"spans": spans},
|
|
108
108
|
)
|
|
109
109
|
|
|
110
|
+
def post_runless_spans(self, spans: list) -> ApiResult:
|
|
111
|
+
"""Ingest spans with no parent run (e.g. MCP tool calls). Each span
|
|
112
|
+
MUST carry distinct_id and agent_name; the server gates on that."""
|
|
113
|
+
return self._request("/api/sdk/spans/append", {"spans": spans})
|
|
114
|
+
|
|
110
115
|
def post_run_feedback(self, run_id: str, payload: Dict[str, Any]) -> ApiResult:
|
|
111
116
|
from urllib.parse import quote
|
|
112
117
|
return self._request(
|
trodo/client.py
CHANGED
|
@@ -17,6 +17,8 @@ from .otel.wrap_agent import (
|
|
|
17
17
|
wrap_agent as wrap_agent_ctx,
|
|
18
18
|
span as span_ctx,
|
|
19
19
|
join_run as join_run_ctx,
|
|
20
|
+
start_run as start_run_fn,
|
|
21
|
+
end_run as end_run_fn,
|
|
20
22
|
current_run_id as _current_run_id,
|
|
21
23
|
current_span_id as _current_span_id,
|
|
22
24
|
)
|
|
@@ -24,6 +26,7 @@ from .otel.auto_instrument import enable_auto_instrument
|
|
|
24
26
|
from .otel.helpers import (
|
|
25
27
|
tool as tool_decorator,
|
|
26
28
|
track_llm_call as track_llm_call_fn,
|
|
29
|
+
track_mcp as track_mcp_fn,
|
|
27
30
|
fastapi_middleware as fastapi_middleware_fn,
|
|
28
31
|
propagation_headers as propagation_headers_fn,
|
|
29
32
|
)
|
|
@@ -284,6 +287,51 @@ class TrodoClient:
|
|
|
284
287
|
"""Create a nested span inside the current run."""
|
|
285
288
|
return span_ctx(name=name, kind=kind, input=input, attributes=attributes)
|
|
286
289
|
|
|
290
|
+
def start_run(
|
|
291
|
+
self,
|
|
292
|
+
agent_name: str,
|
|
293
|
+
*,
|
|
294
|
+
run_id: Optional[str] = None,
|
|
295
|
+
distinct_id: Optional[str] = None,
|
|
296
|
+
conversation_id: Optional[str] = None,
|
|
297
|
+
parent_run_id: Optional[str] = None,
|
|
298
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
299
|
+
input: Any = None,
|
|
300
|
+
) -> str:
|
|
301
|
+
"""Open a Run record outside a context manager. Returns the run_id.
|
|
302
|
+
|
|
303
|
+
Use ``end_run`` to finalise, ``join_run`` from any process to add spans.
|
|
304
|
+
"""
|
|
305
|
+
return start_run_fn(
|
|
306
|
+
processor=self._span_processor,
|
|
307
|
+
agent_name=agent_name,
|
|
308
|
+
run_id=run_id,
|
|
309
|
+
distinct_id=distinct_id,
|
|
310
|
+
conversation_id=conversation_id,
|
|
311
|
+
parent_run_id=parent_run_id,
|
|
312
|
+
metadata=metadata,
|
|
313
|
+
input=input,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def end_run(
|
|
317
|
+
self,
|
|
318
|
+
run_id: str,
|
|
319
|
+
*,
|
|
320
|
+
output: Any = None,
|
|
321
|
+
status: str = "ok",
|
|
322
|
+
error_summary: Optional[str] = None,
|
|
323
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Finalise a Run previously opened by :meth:`start_run`."""
|
|
326
|
+
end_run_fn(
|
|
327
|
+
run_id,
|
|
328
|
+
processor=self._span_processor,
|
|
329
|
+
output=output,
|
|
330
|
+
status=status,
|
|
331
|
+
error_summary=error_summary,
|
|
332
|
+
metadata=metadata,
|
|
333
|
+
)
|
|
334
|
+
|
|
287
335
|
def join_run(
|
|
288
336
|
self,
|
|
289
337
|
run_id: str,
|
|
@@ -348,6 +396,65 @@ class TrodoClient:
|
|
|
348
396
|
metadata=metadata,
|
|
349
397
|
)
|
|
350
398
|
|
|
399
|
+
def track_mcp(
|
|
400
|
+
self,
|
|
401
|
+
*,
|
|
402
|
+
tool: str,
|
|
403
|
+
distinct_id: str,
|
|
404
|
+
input: Any = None,
|
|
405
|
+
output: Any = None,
|
|
406
|
+
error: Optional[str] = None,
|
|
407
|
+
duration_ms: Optional[int] = None,
|
|
408
|
+
session_id: Optional[str] = None,
|
|
409
|
+
client_label: Optional[str] = None,
|
|
410
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
411
|
+
agent_name: str = "MCP",
|
|
412
|
+
started_at: Optional[str] = None,
|
|
413
|
+
ended_at: Optional[str] = None,
|
|
414
|
+
) -> str:
|
|
415
|
+
"""Record a single MCP tool call as a runless span.
|
|
416
|
+
|
|
417
|
+
Use this from inside an MCP server's ``tools/call`` handler — one
|
|
418
|
+
call to ``track_mcp`` = one row in ``agent_spans`` with ``run_id``
|
|
419
|
+
NULL. The MCP server proxies tool calls but never sees the user's
|
|
420
|
+
prompt or the LLM's final answer, so a parent run carries no
|
|
421
|
+
analytical value; the span is the unit of analytics.
|
|
422
|
+
|
|
423
|
+
Required:
|
|
424
|
+
tool: the tool name (becomes ``name = "tool.<tool>"``).
|
|
425
|
+
distinct_id: end-user attribution (email or stable user id).
|
|
426
|
+
|
|
427
|
+
Optional but recommended:
|
|
428
|
+
input / output: the tool's args + full result. Truncated at 64KB.
|
|
429
|
+
error: if set, marks ``status="error"`` and stored as
|
|
430
|
+
``error_message``.
|
|
431
|
+
duration_ms: tool wall time. If omitted, falls back to 0.
|
|
432
|
+
session_id: the ``Mcp-Session-Id`` from the client; stored as
|
|
433
|
+
``conversation_id`` for grouping. Auto-uuid'd if
|
|
434
|
+
omitted (each call becomes its own bucket).
|
|
435
|
+
client_label: which MCP client (anthropic / cursor / chatgpt /
|
|
436
|
+
etc.); merged into attributes.
|
|
437
|
+
attributes: any extra free-form key/values.
|
|
438
|
+
agent_name: defaults to "MCP"; override only for custom tags.
|
|
439
|
+
|
|
440
|
+
Returns the span_id (str) so the caller can correlate.
|
|
441
|
+
"""
|
|
442
|
+
return track_mcp_fn(
|
|
443
|
+
http=self._http,
|
|
444
|
+
tool=tool,
|
|
445
|
+
distinct_id=distinct_id,
|
|
446
|
+
input=input,
|
|
447
|
+
output=output,
|
|
448
|
+
error=error,
|
|
449
|
+
duration_ms=duration_ms,
|
|
450
|
+
session_id=session_id,
|
|
451
|
+
client_label=client_label,
|
|
452
|
+
attributes=attributes,
|
|
453
|
+
agent_name=agent_name,
|
|
454
|
+
started_at=started_at,
|
|
455
|
+
ended_at=ended_at,
|
|
456
|
+
)
|
|
457
|
+
|
|
351
458
|
def fastapi_middleware(self) -> Callable:
|
|
352
459
|
"""Return a FastAPI/Starlette middleware that auto-joins runs."""
|
|
353
460
|
return fastapi_middleware_fn(self)
|
trodo/otel/helpers.py
CHANGED
|
@@ -286,6 +286,96 @@ def llm(
|
|
|
286
286
|
)
|
|
287
287
|
|
|
288
288
|
|
|
289
|
+
def track_mcp(
|
|
290
|
+
*,
|
|
291
|
+
http: Any,
|
|
292
|
+
tool: str,
|
|
293
|
+
distinct_id: str,
|
|
294
|
+
input: Any = None,
|
|
295
|
+
output: Any = None,
|
|
296
|
+
error: Optional[str] = None,
|
|
297
|
+
duration_ms: Optional[int] = None,
|
|
298
|
+
session_id: Optional[str] = None,
|
|
299
|
+
client_label: Optional[str] = None,
|
|
300
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
301
|
+
agent_name: str = "MCP",
|
|
302
|
+
started_at: Optional[str] = None,
|
|
303
|
+
ended_at: Optional[str] = None,
|
|
304
|
+
) -> str:
|
|
305
|
+
"""Record one MCP tool call as a runless span. See ``Client.track_mcp``
|
|
306
|
+
for the customer-facing docstring; this is the implementation that
|
|
307
|
+
builds the span payload and posts it via the HttpClient.
|
|
308
|
+
|
|
309
|
+
Returns the span_id.
|
|
310
|
+
"""
|
|
311
|
+
import json as _json
|
|
312
|
+
import uuid as _uuid
|
|
313
|
+
from datetime import datetime, timedelta, timezone
|
|
314
|
+
|
|
315
|
+
if not tool:
|
|
316
|
+
raise ValueError("track_mcp: 'tool' is required")
|
|
317
|
+
if not distinct_id:
|
|
318
|
+
raise ValueError("track_mcp: 'distinct_id' is required (runless spans must attribute)")
|
|
319
|
+
|
|
320
|
+
span_id = str(_uuid.uuid4())
|
|
321
|
+
conv_id = session_id or str(_uuid.uuid4())
|
|
322
|
+
|
|
323
|
+
now = datetime.now(timezone.utc)
|
|
324
|
+
if ended_at is None:
|
|
325
|
+
ended_at = now.isoformat()
|
|
326
|
+
if started_at is None:
|
|
327
|
+
offset_ms = duration_ms if isinstance(duration_ms, int) and duration_ms > 0 else 0
|
|
328
|
+
started_at = (now - timedelta(milliseconds=offset_ms)).isoformat()
|
|
329
|
+
if duration_ms is None:
|
|
330
|
+
duration_ms = 0
|
|
331
|
+
|
|
332
|
+
status = "error" if error else "ok"
|
|
333
|
+
if error:
|
|
334
|
+
error_payload: Any = {"error": error}
|
|
335
|
+
output_to_record = output if output is not None else error_payload
|
|
336
|
+
else:
|
|
337
|
+
output_to_record = output
|
|
338
|
+
|
|
339
|
+
def _stringify(value: Any) -> Any:
|
|
340
|
+
if value is None or isinstance(value, str):
|
|
341
|
+
return value
|
|
342
|
+
try:
|
|
343
|
+
return _json.dumps(value, default=str)
|
|
344
|
+
except Exception:
|
|
345
|
+
return str(value)
|
|
346
|
+
|
|
347
|
+
merged_attributes: Dict[str, Any] = dict(attributes or {})
|
|
348
|
+
if client_label:
|
|
349
|
+
merged_attributes.setdefault("mcp_client_label", client_label)
|
|
350
|
+
|
|
351
|
+
span_payload: Dict[str, Any] = {
|
|
352
|
+
"span_id": span_id,
|
|
353
|
+
"kind": "tool",
|
|
354
|
+
"name": f"tool.{tool}",
|
|
355
|
+
"status": status,
|
|
356
|
+
"input": _stringify({"tool": tool, "params": input}) if input is not None else None,
|
|
357
|
+
"output": _stringify(output_to_record),
|
|
358
|
+
"tool_name": tool,
|
|
359
|
+
"started_at": started_at,
|
|
360
|
+
"ended_at": ended_at,
|
|
361
|
+
"duration_ms": duration_ms,
|
|
362
|
+
"distinct_id": distinct_id,
|
|
363
|
+
"conversation_id": conv_id,
|
|
364
|
+
"agent_name": agent_name,
|
|
365
|
+
"error_message": error,
|
|
366
|
+
"attributes": merged_attributes,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
http.post_runless_spans([span_payload])
|
|
371
|
+
except Exception:
|
|
372
|
+
# Span loss must never break the caller's request flow. The
|
|
373
|
+
# underlying HttpClient already retries; failures here mean the
|
|
374
|
+
# backend rejected (rare) or the network is permanently down.
|
|
375
|
+
pass
|
|
376
|
+
return span_id
|
|
377
|
+
|
|
378
|
+
|
|
289
379
|
def track_llm_call(
|
|
290
380
|
*,
|
|
291
381
|
model: Optional[str] = None,
|
trodo/otel/processor.py
CHANGED
|
@@ -136,6 +136,25 @@ class TrodoSpanProcessor:
|
|
|
136
136
|
except Exception:
|
|
137
137
|
pass
|
|
138
138
|
|
|
139
|
+
def start_run(self, run: TrodoRun) -> None:
|
|
140
|
+
"""Open a Run row server-side without holding a context manager.
|
|
141
|
+
|
|
142
|
+
Pairs with ``end_run`` for sessions that span multiple processes or
|
|
143
|
+
HTTP requests. Spans emitted between start_run and end_run flush
|
|
144
|
+
incrementally via append_spans (callers are expected to mark_joined).
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
self._http.post_run_start({"run": run.to_dict()})
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
def end_run(self, run_id: str, payload: Dict[str, Any]) -> None:
|
|
152
|
+
"""Finalise a Run opened by start_run."""
|
|
153
|
+
try:
|
|
154
|
+
self._http.post_run_end(run_id, payload)
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
139
158
|
def append_spans(self, run_id: str, spans: List[TrodoSpan]) -> None:
|
|
140
159
|
"""Stream spans for a long-running or joined run without finalising."""
|
|
141
160
|
if not spans:
|
trodo/otel/wrap_agent.py
CHANGED
|
@@ -163,6 +163,76 @@ class SpanHandle:
|
|
|
163
163
|
self.tool_name = tool_name
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def start_run(
|
|
167
|
+
*,
|
|
168
|
+
processor: TrodoSpanProcessor,
|
|
169
|
+
agent_name: str,
|
|
170
|
+
run_id: Optional[str] = None,
|
|
171
|
+
distinct_id: Optional[str] = None,
|
|
172
|
+
conversation_id: Optional[str] = None,
|
|
173
|
+
parent_run_id: Optional[str] = None,
|
|
174
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
175
|
+
input: Any = None,
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Open a Run record without holding a context manager.
|
|
178
|
+
|
|
179
|
+
Pairs with :func:`end_run` for sessions that span multiple processes or
|
|
180
|
+
HTTP requests (e.g. an MCP server where ``initialize`` opens a run and
|
|
181
|
+
later ``tools/call`` requests append spans before a final close).
|
|
182
|
+
|
|
183
|
+
Returns the ``run_id`` (caller-supplied or freshly minted UUID). Between
|
|
184
|
+
``start_run`` and ``end_run`` any process can use ``join_run(run_id, ...)``
|
|
185
|
+
to add spans — they flush incrementally via ``append_spans``.
|
|
186
|
+
"""
|
|
187
|
+
rid = run_id or str(uuid.uuid4())
|
|
188
|
+
run = TrodoRun(
|
|
189
|
+
run_id=rid,
|
|
190
|
+
agent_name=agent_name,
|
|
191
|
+
distinct_id=distinct_id,
|
|
192
|
+
conversation_id=conversation_id,
|
|
193
|
+
parent_run_id=parent_run_id,
|
|
194
|
+
status="running",
|
|
195
|
+
input=_truncate(input),
|
|
196
|
+
started_at=_now_iso(),
|
|
197
|
+
metadata=metadata,
|
|
198
|
+
)
|
|
199
|
+
processor.mark_joined(rid)
|
|
200
|
+
processor.start_run(run)
|
|
201
|
+
return rid
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def end_run(
|
|
205
|
+
run_id: str,
|
|
206
|
+
*,
|
|
207
|
+
processor: TrodoSpanProcessor,
|
|
208
|
+
output: Any = None,
|
|
209
|
+
status: str = "ok",
|
|
210
|
+
error_summary: Optional[str] = None,
|
|
211
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Finalise a Run opened by :func:`start_run`.
|
|
214
|
+
|
|
215
|
+
Aggregates any locally-buffered spans for ``run_id``, POSTs to the
|
|
216
|
+
``/runs/{id}/end`` endpoint, and unmarks the run as joined. Idempotent
|
|
217
|
+
on the local-state side; the backend treats a second call as a row update.
|
|
218
|
+
"""
|
|
219
|
+
pending = processor.get_pending(run_id)
|
|
220
|
+
agg = _aggregate(pending)
|
|
221
|
+
payload: Dict[str, Any] = {
|
|
222
|
+
"ended_at": _now_iso(),
|
|
223
|
+
"status": status,
|
|
224
|
+
**agg,
|
|
225
|
+
}
|
|
226
|
+
if output is not None:
|
|
227
|
+
payload["output"] = _truncate(output)
|
|
228
|
+
if error_summary is not None:
|
|
229
|
+
payload["error_summary"] = _truncate(error_summary, 4_000)
|
|
230
|
+
if metadata is not None:
|
|
231
|
+
payload["metadata"] = metadata
|
|
232
|
+
processor.end_run(run_id, payload)
|
|
233
|
+
processor.unmark_joined(run_id)
|
|
234
|
+
|
|
235
|
+
|
|
166
236
|
class wrap_agent:
|
|
167
237
|
"""Context manager wrapping an agent run.
|
|
168
238
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: trodo-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -263,6 +263,34 @@ with trodo.join_run(
|
|
|
263
263
|
...
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
+
### Long-lived sessions across processes — `start_run` / `end_run`
|
|
267
|
+
|
|
268
|
+
`wrap_agent` is a context manager — it opens *and* closes the run in one
|
|
269
|
+
call stack. For sessions that live across many HTTP requests (an MCP
|
|
270
|
+
server, a websocket-pinned chat, scheduled jobs that resume on different
|
|
271
|
+
workers), use `start_run` to open the run from one process and `end_run`
|
|
272
|
+
to finalise it later. Between the two, any process can use `join_run` to
|
|
273
|
+
add child spans. Same `run_id` threads through everything.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
# Process A — open the run for an MCP session.
|
|
277
|
+
run_id = trodo.start_run(
|
|
278
|
+
'external_mcp_session',
|
|
279
|
+
distinct_id=str(user_id),
|
|
280
|
+
conversation_id=mcp_session_id,
|
|
281
|
+
)
|
|
282
|
+
redis.set(f"mcp:run:{mcp_session_id}", run_id, ex=3600)
|
|
283
|
+
|
|
284
|
+
# Process B (later, possibly a different worker) — append a tool span.
|
|
285
|
+
run_id = redis.get(f"mcp:run:{mcp_session_id}").decode()
|
|
286
|
+
with trodo.join_run(run_id, name='tool.run_funnel_query', kind='tool') as span:
|
|
287
|
+
span.set_input(args)
|
|
288
|
+
span.set_output(result)
|
|
289
|
+
|
|
290
|
+
# When the session ends (timeout sweeper, explicit close):
|
|
291
|
+
trodo.end_run(run_id, status='ok')
|
|
292
|
+
```
|
|
293
|
+
|
|
266
294
|
### Conversation binding & feedback
|
|
267
295
|
|
|
268
296
|
```python
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
trodo/__init__.py,sha256=
|
|
2
|
-
trodo/client.py,sha256=
|
|
1
|
+
trodo/__init__.py,sha256=FFfTNjEugSRfWUELrR4CoqAJ-zvcAYizPGQjhT7QBes,15121
|
|
2
|
+
trodo/client.py,sha256=5sT3mKh-AqHnB9_duTVwvnQcPmJ90hc2GJIe0Z7j1BI,17865
|
|
3
3
|
trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
|
|
4
4
|
trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
|
|
5
5
|
trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
trodo/api/async_client.py,sha256=rZN4aJ2QiKyrHBK260bApCUB9JaMWU6BQtzoSJZh7xk,3408
|
|
7
7
|
trodo/api/endpoints.py,sha256=HKQ3d_Mxf0y4HwlHor0XkSAwUVj-4Xvv--rzE9njxjM,1027
|
|
8
|
-
trodo/api/http_client.py,sha256=
|
|
8
|
+
trodo/api/http_client.py,sha256=bu_tRAhlsqHnLYZhRChs3ltNWuOXzQLArNbWG0P3whg,4676
|
|
9
9
|
trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
|
|
11
11
|
trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -14,16 +14,16 @@ trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8p
|
|
|
14
14
|
trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
|
|
15
15
|
trodo/otel/auto_instrument.py,sha256=J_neFxvO-3YACUvtetY4RdM8xYA_79SZUgPry6hXrm8,9434
|
|
16
16
|
trodo/otel/context.py,sha256=iJ1rE42-SbO8VZHAxhIl2ZJXgNwLIVps5xLg8GKgfFc,1165
|
|
17
|
-
trodo/otel/helpers.py,sha256=
|
|
18
|
-
trodo/otel/processor.py,sha256=
|
|
19
|
-
trodo/otel/wrap_agent.py,sha256=
|
|
17
|
+
trodo/otel/helpers.py,sha256=_ImekkJOWWT0pD8Vm1NuKBIps4wjCjx-72SUu7MpAyk,16172
|
|
18
|
+
trodo/otel/processor.py,sha256=jVZkslZlw50G5uRAa7-GMRgn_yvae58EmlWTZL8tMkQ,6285
|
|
19
|
+
trodo/otel/wrap_agent.py,sha256=mwHYwxtg9A9VUBdlbCKLswrNP0v5oY8ELpvt1JVYE8Q,17495
|
|
20
20
|
trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
trodo/queue/batch_flusher.py,sha256=4Lg6T3Urwi9U0Q4FpFGPmjDYKg4ZliCTR-ND8BJvWaY,1298
|
|
22
22
|
trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,872
|
|
23
23
|
trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
trodo/session/server_session.py,sha256=uBAq1QSYPUUaHFSeoOyM5Yr65dLb8T82OOx3D1BrdrE,1970
|
|
25
25
|
trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
|
|
26
|
-
trodo_python-2.
|
|
27
|
-
trodo_python-2.
|
|
28
|
-
trodo_python-2.
|
|
29
|
-
trodo_python-2.
|
|
26
|
+
trodo_python-2.3.0.dist-info/METADATA,sha256=7d3-wba0IywiPL8MKzEKxyjZ3el8Z_6M8MUCrnBJ55k,16353
|
|
27
|
+
trodo_python-2.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
28
|
+
trodo_python-2.3.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
|
|
29
|
+
trodo_python-2.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|