docent-python 0.1.3a0__py3-none-any.whl → 0.1.4a0__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.
Potentially problematic release.
This version of docent-python might be problematic. Click here for more details.
- docent/data_models/__init__.py +2 -9
- docent/data_models/agent_run.py +30 -20
- docent/data_models/metadata.py +229 -229
- docent/data_models/transcript.py +56 -16
- docent/loaders/load_inspect.py +37 -25
- docent/trace.py +820 -286
- docent/trace_alt.py +34 -18
- docent/trace_temp.py +1086 -0
- {docent_python-0.1.3a0.dist-info → docent_python-0.1.4a0.dist-info}/METADATA +1 -1
- {docent_python-0.1.3a0.dist-info → docent_python-0.1.4a0.dist-info}/RECORD +12 -11
- {docent_python-0.1.3a0.dist-info → docent_python-0.1.4a0.dist-info}/WHEEL +0 -0
- {docent_python-0.1.3a0.dist-info → docent_python-0.1.4a0.dist-info}/licenses/LICENSE.md +0 -0
docent/trace.py
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import atexit
|
|
3
2
|
import contextvars
|
|
4
|
-
import inspect
|
|
5
3
|
import itertools
|
|
6
4
|
import logging
|
|
7
5
|
import os
|
|
@@ -12,8 +10,10 @@ import uuid
|
|
|
12
10
|
from collections import defaultdict
|
|
13
11
|
from contextlib import asynccontextmanager, contextmanager
|
|
14
12
|
from contextvars import ContextVar, Token
|
|
13
|
+
from datetime import datetime, timezone
|
|
15
14
|
from typing import Any, AsyncIterator, Callable, Dict, Iterator, List, Optional, Union
|
|
16
15
|
|
|
16
|
+
import requests
|
|
17
17
|
from opentelemetry import trace
|
|
18
18
|
from opentelemetry.context import Context
|
|
19
19
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCExporter
|
|
@@ -39,36 +39,14 @@ logger.disabled = True
|
|
|
39
39
|
|
|
40
40
|
# Default configuration
|
|
41
41
|
DEFAULT_ENDPOINT = "https://api.docent.transluce.org/rest/telemetry"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _is_async_context() -> bool:
|
|
45
|
-
"""Detect if we're in an async context."""
|
|
46
|
-
try:
|
|
47
|
-
# Check if we're in an async function
|
|
48
|
-
frame = inspect.currentframe()
|
|
49
|
-
while frame:
|
|
50
|
-
if frame.f_code.co_flags & inspect.CO_COROUTINE:
|
|
51
|
-
return True
|
|
52
|
-
frame = frame.f_back
|
|
53
|
-
return False
|
|
54
|
-
except:
|
|
55
|
-
return False
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _is_running_in_event_loop() -> bool:
|
|
59
|
-
"""Check if we're running in an event loop."""
|
|
60
|
-
try:
|
|
61
|
-
asyncio.get_running_loop()
|
|
62
|
-
return True
|
|
63
|
-
except RuntimeError:
|
|
64
|
-
return False
|
|
42
|
+
DEFAULT_COLLECTION_NAME = "default-collection-name"
|
|
65
43
|
|
|
66
44
|
|
|
67
45
|
def _is_notebook() -> bool:
|
|
68
46
|
"""Check if we're running in a Jupyter notebook."""
|
|
69
47
|
try:
|
|
70
48
|
return "ipykernel" in sys.modules
|
|
71
|
-
except:
|
|
49
|
+
except Exception:
|
|
72
50
|
return False
|
|
73
51
|
|
|
74
52
|
|
|
@@ -77,7 +55,7 @@ class DocentTracer:
|
|
|
77
55
|
|
|
78
56
|
def __init__(
|
|
79
57
|
self,
|
|
80
|
-
collection_name: str =
|
|
58
|
+
collection_name: str = DEFAULT_COLLECTION_NAME,
|
|
81
59
|
collection_id: Optional[str] = None,
|
|
82
60
|
agent_run_id: Optional[str] = None,
|
|
83
61
|
endpoint: Union[str, List[str]] = DEFAULT_ENDPOINT,
|
|
@@ -86,7 +64,6 @@ class DocentTracer:
|
|
|
86
64
|
enable_console_export: bool = False,
|
|
87
65
|
enable_otlp_export: bool = True,
|
|
88
66
|
disable_batch: bool = False,
|
|
89
|
-
span_postprocess_callback: Optional[Callable[[ReadableSpan], None]] = None,
|
|
90
67
|
):
|
|
91
68
|
"""
|
|
92
69
|
Initialize Docent tracing manager.
|
|
@@ -101,7 +78,6 @@ class DocentTracer:
|
|
|
101
78
|
enable_console_export: Whether to export to console
|
|
102
79
|
enable_otlp_export: Whether to export to OTLP endpoint
|
|
103
80
|
disable_batch: Whether to disable batch processing (use SimpleSpanProcessor)
|
|
104
|
-
span_postprocess_callback: Optional callback for post-processing spans
|
|
105
81
|
"""
|
|
106
82
|
self.collection_name: str = collection_name
|
|
107
83
|
self.collection_id: str = collection_id if collection_id else str(uuid.uuid4())
|
|
@@ -129,22 +105,27 @@ class DocentTracer:
|
|
|
129
105
|
self.enable_console_export = enable_console_export
|
|
130
106
|
self.enable_otlp_export = enable_otlp_export
|
|
131
107
|
self.disable_batch = disable_batch
|
|
132
|
-
self.span_postprocess_callback = span_postprocess_callback
|
|
133
108
|
|
|
134
109
|
# Use separate tracer provider to avoid interfering with existing OTEL setup
|
|
135
110
|
self._tracer_provider: Optional[TracerProvider] = None
|
|
136
|
-
self.
|
|
137
|
-
self._root_context: Context = Context()
|
|
111
|
+
self._root_context: Optional[Context] = Context()
|
|
138
112
|
self._tracer: Optional[trace.Tracer] = None
|
|
139
113
|
self._initialized: bool = False
|
|
140
114
|
self._cleanup_registered: bool = False
|
|
141
115
|
self._disabled: bool = False
|
|
142
116
|
self._spans_processors: List[Union[BatchSpanProcessor, SimpleSpanProcessor]] = []
|
|
143
117
|
|
|
144
|
-
#
|
|
118
|
+
# Base HTTP endpoint for direct API calls (scores, metadata, trace-done)
|
|
119
|
+
if len(self.endpoints) > 0:
|
|
120
|
+
self._api_endpoint_base: Optional[str] = self.endpoints[0]
|
|
121
|
+
|
|
122
|
+
# Context variables for agent_run_id and transcript_id
|
|
145
123
|
self._collection_id_var: ContextVar[str] = contextvars.ContextVar("docent_collection_id")
|
|
146
124
|
self._agent_run_id_var: ContextVar[str] = contextvars.ContextVar("docent_agent_run_id")
|
|
147
125
|
self._transcript_id_var: ContextVar[str] = contextvars.ContextVar("docent_transcript_id")
|
|
126
|
+
self._transcript_group_id_var: ContextVar[str] = contextvars.ContextVar(
|
|
127
|
+
"docent_transcript_group_id"
|
|
128
|
+
)
|
|
148
129
|
self._attributes_var: ContextVar[dict[str, Any]] = contextvars.ContextVar(
|
|
149
130
|
"docent_attributes"
|
|
150
131
|
)
|
|
@@ -154,18 +135,17 @@ class DocentTracer:
|
|
|
154
135
|
)
|
|
155
136
|
self._transcript_counter_lock = threading.Lock()
|
|
156
137
|
|
|
157
|
-
def
|
|
138
|
+
def get_current_agent_run_id(self) -> Optional[str]:
|
|
158
139
|
"""
|
|
159
|
-
Get the current
|
|
160
|
-
This never touches the global OpenTelemetry context.
|
|
161
|
-
"""
|
|
162
|
-
if self._root_context is None:
|
|
163
|
-
return None
|
|
140
|
+
Get the current agent run ID from context.
|
|
164
141
|
|
|
142
|
+
Returns:
|
|
143
|
+
The current agent run ID if available, None otherwise
|
|
144
|
+
"""
|
|
165
145
|
try:
|
|
166
|
-
return
|
|
167
|
-
except
|
|
168
|
-
return
|
|
146
|
+
return self._agent_run_id_var.get()
|
|
147
|
+
except LookupError:
|
|
148
|
+
return self.default_agent_run_id
|
|
169
149
|
|
|
170
150
|
def _register_cleanup(self):
|
|
171
151
|
"""Register cleanup handlers."""
|
|
@@ -187,7 +167,7 @@ class DocentTracer:
|
|
|
187
167
|
|
|
188
168
|
def _next_span_order(self, transcript_id: str) -> int:
|
|
189
169
|
"""
|
|
190
|
-
Get the next
|
|
170
|
+
Get the next span order for a given transcript_id.
|
|
191
171
|
Thread-safe and guaranteed to be unique and monotonic.
|
|
192
172
|
"""
|
|
193
173
|
with self._transcript_counter_lock:
|
|
@@ -252,17 +232,16 @@ class DocentTracer:
|
|
|
252
232
|
resource=Resource.create({"service.name": self.collection_name})
|
|
253
233
|
)
|
|
254
234
|
|
|
255
|
-
# Add custom span processor for
|
|
235
|
+
# Add custom span processor for agent_run_id and transcript_id
|
|
256
236
|
class ContextSpanProcessor(SpanProcessor):
|
|
257
237
|
def __init__(self, manager: "DocentTracer"):
|
|
258
238
|
self.manager: "DocentTracer" = manager
|
|
259
239
|
|
|
260
240
|
def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
|
|
261
|
-
# Add collection_id, agent_run_id, transcript_id, and any other current attributes
|
|
262
|
-
# Always add collection_id as it's always available
|
|
241
|
+
# Add collection_id, agent_run_id, transcript_id, transcript_group_id, and any other current attributes
|
|
263
242
|
span.set_attribute("collection_id", self.manager.collection_id)
|
|
264
243
|
|
|
265
|
-
#
|
|
244
|
+
# Set agent_run_id from context
|
|
266
245
|
try:
|
|
267
246
|
agent_run_id: str = self.manager._agent_run_id_var.get()
|
|
268
247
|
if agent_run_id:
|
|
@@ -274,7 +253,15 @@ class DocentTracer:
|
|
|
274
253
|
span.set_attribute("agent_run_id_default", True)
|
|
275
254
|
span.set_attribute("agent_run_id", self.manager.default_agent_run_id)
|
|
276
255
|
|
|
277
|
-
#
|
|
256
|
+
# Set transcript_group_id from context
|
|
257
|
+
try:
|
|
258
|
+
transcript_group_id: str = self.manager._transcript_group_id_var.get()
|
|
259
|
+
if transcript_group_id:
|
|
260
|
+
span.set_attribute("transcript_group_id", transcript_group_id)
|
|
261
|
+
except LookupError:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# Set transcript_id from context
|
|
278
265
|
try:
|
|
279
266
|
transcript_id: str = self.manager._transcript_id_var.get()
|
|
280
267
|
if transcript_id:
|
|
@@ -286,7 +273,7 @@ class DocentTracer:
|
|
|
286
273
|
# transcript_id not available, skip it
|
|
287
274
|
pass
|
|
288
275
|
|
|
289
|
-
#
|
|
276
|
+
# Set custom attributes from context
|
|
290
277
|
try:
|
|
291
278
|
attributes: dict[str, Any] = self.manager._attributes_var.get()
|
|
292
279
|
for key, value in attributes.items():
|
|
@@ -340,18 +327,6 @@ class DocentTracer:
|
|
|
340
327
|
# Get tracer from our isolated provider (don't set global provider)
|
|
341
328
|
self._tracer = self._tracer_provider.get_tracer(__name__)
|
|
342
329
|
|
|
343
|
-
# Start root span
|
|
344
|
-
self._root_span = self._tracer.start_span(
|
|
345
|
-
"application_session",
|
|
346
|
-
attributes={
|
|
347
|
-
"service.name": self.collection_name,
|
|
348
|
-
"session.type": "application_root",
|
|
349
|
-
},
|
|
350
|
-
)
|
|
351
|
-
self._root_context = trace.set_span_in_context(
|
|
352
|
-
self._root_span, context=self._root_context
|
|
353
|
-
)
|
|
354
|
-
|
|
355
330
|
# Instrument threading for better context propagation
|
|
356
331
|
try:
|
|
357
332
|
ThreadingInstrumentor().instrument()
|
|
@@ -398,30 +373,14 @@ class DocentTracer:
|
|
|
398
373
|
raise
|
|
399
374
|
|
|
400
375
|
def cleanup(self):
|
|
401
|
-
"""Clean up Docent tracing resources."""
|
|
376
|
+
"""Clean up Docent tracing resources and signal trace completion to backend."""
|
|
402
377
|
try:
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
"event.type": "trace_end",
|
|
410
|
-
},
|
|
411
|
-
)
|
|
412
|
-
end_span.end()
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
self._root_span
|
|
416
|
-
and hasattr(self._root_span, "is_recording")
|
|
417
|
-
and self._root_span.is_recording()
|
|
418
|
-
):
|
|
419
|
-
self._root_span.end()
|
|
420
|
-
elif self._root_span:
|
|
421
|
-
# Fallback if is_recording is not available
|
|
422
|
-
self._root_span.end()
|
|
423
|
-
|
|
424
|
-
self._root_span = None
|
|
378
|
+
# Notify backend that trace is done (no span creation)
|
|
379
|
+
try:
|
|
380
|
+
self._send_trace_done()
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.warning(f"Failed to notify trace done: {e}")
|
|
383
|
+
|
|
425
384
|
self._root_context = None # type: ignore
|
|
426
385
|
|
|
427
386
|
# Shutdown our isolated tracer provider
|
|
@@ -485,48 +444,6 @@ class DocentTracer:
|
|
|
485
444
|
self.initialize()
|
|
486
445
|
return self._root_context
|
|
487
446
|
|
|
488
|
-
@contextmanager
|
|
489
|
-
def span(self, name: str, attributes: Optional[Dict[str, Any]] = None) -> Iterator[Span]:
|
|
490
|
-
"""
|
|
491
|
-
Context manager for creating spans with attributes.
|
|
492
|
-
"""
|
|
493
|
-
if not self._initialized:
|
|
494
|
-
self.initialize()
|
|
495
|
-
|
|
496
|
-
if self._tracer is None:
|
|
497
|
-
raise RuntimeError("Tracer not initialized")
|
|
498
|
-
|
|
499
|
-
span_attributes: dict[str, Any] = attributes or {}
|
|
500
|
-
|
|
501
|
-
with self._tracer.start_as_current_span(
|
|
502
|
-
name, context=self._root_context, attributes=span_attributes
|
|
503
|
-
) as span:
|
|
504
|
-
yield span
|
|
505
|
-
|
|
506
|
-
@asynccontextmanager
|
|
507
|
-
async def async_span(
|
|
508
|
-
self, name: str, attributes: Optional[Dict[str, Any]] = None
|
|
509
|
-
) -> AsyncIterator[Span]:
|
|
510
|
-
"""
|
|
511
|
-
Async context manager for creating spans with attributes.
|
|
512
|
-
|
|
513
|
-
Args:
|
|
514
|
-
name: Name of the span
|
|
515
|
-
attributes: Dictionary of attributes to add to the span
|
|
516
|
-
"""
|
|
517
|
-
if not self._initialized:
|
|
518
|
-
self.initialize()
|
|
519
|
-
|
|
520
|
-
if self._tracer is None:
|
|
521
|
-
raise RuntimeError("Tracer not initialized")
|
|
522
|
-
|
|
523
|
-
span_attributes: dict[str, Any] = attributes or {}
|
|
524
|
-
|
|
525
|
-
with self._tracer.start_as_current_span(
|
|
526
|
-
name, context=self._root_context, attributes=span_attributes
|
|
527
|
-
) as span:
|
|
528
|
-
yield span
|
|
529
|
-
|
|
530
447
|
@contextmanager
|
|
531
448
|
def agent_run_context(
|
|
532
449
|
self,
|
|
@@ -541,7 +458,7 @@ class DocentTracer:
|
|
|
541
458
|
Args:
|
|
542
459
|
agent_run_id: Optional agent run ID (auto-generated if not provided)
|
|
543
460
|
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
544
|
-
metadata: Optional nested dictionary of metadata to
|
|
461
|
+
metadata: Optional nested dictionary of metadata to send to backend
|
|
545
462
|
**attributes: Additional attributes to add to the context
|
|
546
463
|
|
|
547
464
|
Yields:
|
|
@@ -550,9 +467,6 @@ class DocentTracer:
|
|
|
550
467
|
if not self._initialized:
|
|
551
468
|
self.initialize()
|
|
552
469
|
|
|
553
|
-
if self._tracer is None:
|
|
554
|
-
raise RuntimeError("Tracer not initialized")
|
|
555
|
-
|
|
556
470
|
if agent_run_id is None:
|
|
557
471
|
agent_run_id = str(uuid.uuid4())
|
|
558
472
|
if transcript_id is None:
|
|
@@ -564,20 +478,14 @@ class DocentTracer:
|
|
|
564
478
|
attributes_token: Token[dict[str, Any]] = self._attributes_var.set(attributes)
|
|
565
479
|
|
|
566
480
|
try:
|
|
567
|
-
#
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
) as _span:
|
|
576
|
-
# Attach metadata as events if provided
|
|
577
|
-
if metadata:
|
|
578
|
-
_add_metadata_event_to_span(_span, metadata)
|
|
579
|
-
|
|
580
|
-
yield agent_run_id, transcript_id
|
|
481
|
+
# Send metadata directly to backend if provided
|
|
482
|
+
if metadata:
|
|
483
|
+
try:
|
|
484
|
+
self.send_agent_run_metadata(agent_run_id, metadata)
|
|
485
|
+
except Exception as e:
|
|
486
|
+
logger.warning(f"Failed sending agent run metadata: {e}")
|
|
487
|
+
|
|
488
|
+
yield agent_run_id, transcript_id
|
|
581
489
|
finally:
|
|
582
490
|
self._agent_run_id_var.reset(agent_run_id_token)
|
|
583
491
|
self._transcript_id_var.reset(transcript_id_token)
|
|
@@ -598,7 +506,7 @@ class DocentTracer:
|
|
|
598
506
|
Args:
|
|
599
507
|
agent_run_id: Optional agent run ID (auto-generated if not provided)
|
|
600
508
|
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
601
|
-
metadata: Optional nested dictionary of metadata to
|
|
509
|
+
metadata: Optional nested dictionary of metadata to send to backend
|
|
602
510
|
**attributes: Additional attributes to add to the context
|
|
603
511
|
|
|
604
512
|
Yields:
|
|
@@ -607,9 +515,6 @@ class DocentTracer:
|
|
|
607
515
|
if not self._initialized:
|
|
608
516
|
self.initialize()
|
|
609
517
|
|
|
610
|
-
if self._tracer is None:
|
|
611
|
-
raise RuntimeError("Tracer not initialized")
|
|
612
|
-
|
|
613
518
|
if agent_run_id is None:
|
|
614
519
|
agent_run_id = str(uuid.uuid4())
|
|
615
520
|
if transcript_id is None:
|
|
@@ -621,117 +526,415 @@ class DocentTracer:
|
|
|
621
526
|
attributes_token: Token[dict[str, Any]] = self._attributes_var.set(attributes)
|
|
622
527
|
|
|
623
528
|
try:
|
|
624
|
-
#
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
) as _span:
|
|
633
|
-
# Attach metadata as events if provided
|
|
634
|
-
if metadata:
|
|
635
|
-
_add_metadata_event_to_span(_span, metadata)
|
|
636
|
-
|
|
637
|
-
yield agent_run_id, transcript_id
|
|
529
|
+
# Send metadata directly to backend if provided
|
|
530
|
+
if metadata:
|
|
531
|
+
try:
|
|
532
|
+
self.send_agent_run_metadata(agent_run_id, metadata)
|
|
533
|
+
except Exception as e:
|
|
534
|
+
logger.warning(f"Failed sending agent run metadata: {e}")
|
|
535
|
+
|
|
536
|
+
yield agent_run_id, transcript_id
|
|
638
537
|
finally:
|
|
639
538
|
self._agent_run_id_var.reset(agent_run_id_token)
|
|
640
539
|
self._transcript_id_var.reset(transcript_id_token)
|
|
641
540
|
self._attributes_var.reset(attributes_token)
|
|
642
541
|
|
|
643
|
-
def
|
|
542
|
+
def _api_headers(self) -> Dict[str, str]:
|
|
543
|
+
"""
|
|
544
|
+
Get the API headers for HTTP requests.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
Dictionary of headers including Authorization
|
|
548
|
+
"""
|
|
549
|
+
return {
|
|
550
|
+
"Content-Type": "application/json",
|
|
551
|
+
"Authorization": f"Bearer {self.headers.get('Authorization', '').replace('Bearer ', '')}",
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
def _post_json(self, path: str, data: Dict[str, Any]) -> None:
|
|
555
|
+
if not self._api_endpoint_base:
|
|
556
|
+
raise RuntimeError("API endpoint base is not configured")
|
|
557
|
+
url = f"{self._api_endpoint_base}{path}"
|
|
558
|
+
try:
|
|
559
|
+
resp = requests.post(url, json=data, headers=self._api_headers(), timeout=10)
|
|
560
|
+
resp.raise_for_status()
|
|
561
|
+
except requests.exceptions.RequestException as e:
|
|
562
|
+
logger.error(f"Failed POST {url}: {e}")
|
|
563
|
+
|
|
564
|
+
def send_agent_run_score(
|
|
644
565
|
self,
|
|
645
|
-
agent_run_id:
|
|
566
|
+
agent_run_id: str,
|
|
567
|
+
name: str,
|
|
568
|
+
score: float,
|
|
569
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
570
|
+
) -> None:
|
|
571
|
+
"""
|
|
572
|
+
Send a score to the backend for a specific agent run.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
agent_run_id: The agent run ID
|
|
576
|
+
name: Name of the score metric
|
|
577
|
+
score: Numeric score value
|
|
578
|
+
attributes: Optional additional attributes
|
|
579
|
+
"""
|
|
580
|
+
collection_id = self.collection_id
|
|
581
|
+
payload: Dict[str, Any] = {
|
|
582
|
+
"collection_id": collection_id,
|
|
583
|
+
"agent_run_id": agent_run_id,
|
|
584
|
+
"score_name": name,
|
|
585
|
+
"score_value": score,
|
|
586
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
587
|
+
}
|
|
588
|
+
if attributes:
|
|
589
|
+
payload.update(attributes)
|
|
590
|
+
self._post_json("/v1/scores", payload)
|
|
591
|
+
|
|
592
|
+
def send_agent_run_metadata(self, agent_run_id: str, metadata: Dict[str, Any]) -> None:
|
|
593
|
+
collection_id = self.collection_id
|
|
594
|
+
payload: Dict[str, Any] = {
|
|
595
|
+
"collection_id": collection_id,
|
|
596
|
+
"agent_run_id": agent_run_id,
|
|
597
|
+
"metadata": metadata,
|
|
598
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
599
|
+
}
|
|
600
|
+
self._post_json("/v1/agent-run-metadata", payload)
|
|
601
|
+
|
|
602
|
+
def send_transcript_metadata(
|
|
603
|
+
self,
|
|
604
|
+
transcript_id: str,
|
|
605
|
+
name: Optional[str] = None,
|
|
606
|
+
description: Optional[str] = None,
|
|
607
|
+
transcript_group_id: Optional[str] = None,
|
|
608
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
609
|
+
) -> None:
|
|
610
|
+
"""
|
|
611
|
+
Send transcript data to the backend.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
transcript_id: The transcript ID
|
|
615
|
+
name: Optional transcript name
|
|
616
|
+
description: Optional transcript description
|
|
617
|
+
transcript_group_id: Optional transcript group ID
|
|
618
|
+
metadata: Optional metadata to send
|
|
619
|
+
"""
|
|
620
|
+
collection_id = self.collection_id
|
|
621
|
+
payload: Dict[str, Any] = {
|
|
622
|
+
"collection_id": collection_id,
|
|
623
|
+
"transcript_id": transcript_id,
|
|
624
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
# Only add fields that are provided
|
|
628
|
+
if name is not None:
|
|
629
|
+
payload["name"] = name
|
|
630
|
+
if description is not None:
|
|
631
|
+
payload["description"] = description
|
|
632
|
+
if transcript_group_id is not None:
|
|
633
|
+
payload["transcript_group_id"] = transcript_group_id
|
|
634
|
+
if metadata is not None:
|
|
635
|
+
payload["metadata"] = metadata
|
|
636
|
+
|
|
637
|
+
self._post_json("/v1/transcript-metadata", payload)
|
|
638
|
+
|
|
639
|
+
def get_current_transcript_id(self) -> Optional[str]:
|
|
640
|
+
"""
|
|
641
|
+
Get the current transcript ID from context.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
The current transcript ID if available, None otherwise
|
|
645
|
+
"""
|
|
646
|
+
try:
|
|
647
|
+
return self._transcript_id_var.get()
|
|
648
|
+
except LookupError:
|
|
649
|
+
return None
|
|
650
|
+
|
|
651
|
+
def get_current_transcript_group_id(self) -> Optional[str]:
|
|
652
|
+
"""
|
|
653
|
+
Get the current transcript group ID from context.
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
The current transcript group ID if available, None otherwise
|
|
657
|
+
"""
|
|
658
|
+
try:
|
|
659
|
+
return self._transcript_group_id_var.get()
|
|
660
|
+
except LookupError:
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
@contextmanager
|
|
664
|
+
def transcript_context(
|
|
665
|
+
self,
|
|
666
|
+
name: Optional[str] = None,
|
|
646
667
|
transcript_id: Optional[str] = None,
|
|
647
|
-
|
|
648
|
-
|
|
668
|
+
description: Optional[str] = None,
|
|
669
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
670
|
+
transcript_group_id: Optional[str] = None,
|
|
671
|
+
) -> Iterator[str]:
|
|
649
672
|
"""
|
|
650
|
-
|
|
673
|
+
Context manager for setting up a transcript context.
|
|
651
674
|
|
|
652
675
|
Args:
|
|
653
|
-
|
|
676
|
+
name: Optional transcript name
|
|
654
677
|
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
655
|
-
|
|
678
|
+
description: Optional transcript description
|
|
679
|
+
metadata: Optional metadata to send to backend
|
|
680
|
+
transcript_group_id: Optional transcript group ID
|
|
656
681
|
|
|
657
|
-
|
|
658
|
-
|
|
682
|
+
Yields:
|
|
683
|
+
The transcript ID
|
|
659
684
|
"""
|
|
660
685
|
if not self._initialized:
|
|
661
|
-
|
|
686
|
+
raise RuntimeError(
|
|
687
|
+
"Tracer is not initialized. Call initialize_tracing() before using transcript context."
|
|
688
|
+
)
|
|
662
689
|
|
|
663
|
-
if
|
|
664
|
-
|
|
690
|
+
if transcript_id is None:
|
|
691
|
+
transcript_id = str(uuid.uuid4())
|
|
692
|
+
|
|
693
|
+
# Determine transcript group ID before setting new context
|
|
694
|
+
if transcript_group_id is None:
|
|
695
|
+
try:
|
|
696
|
+
transcript_group_id = self._transcript_group_id_var.get()
|
|
697
|
+
except LookupError:
|
|
698
|
+
# No current transcript group context, this transcript has no group
|
|
699
|
+
transcript_group_id = None
|
|
700
|
+
|
|
701
|
+
# Set context variable for this execution context
|
|
702
|
+
transcript_id_token: Token[str] = self._transcript_id_var.set(transcript_id)
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
# Send transcript data and metadata to backend
|
|
706
|
+
try:
|
|
707
|
+
self.send_transcript_metadata(
|
|
708
|
+
transcript_id, name, description, transcript_group_id, metadata
|
|
709
|
+
)
|
|
710
|
+
except Exception as e:
|
|
711
|
+
logger.warning(f"Failed sending transcript data: {e}")
|
|
712
|
+
|
|
713
|
+
yield transcript_id
|
|
714
|
+
finally:
|
|
715
|
+
# Reset context variable to previous state
|
|
716
|
+
self._transcript_id_var.reset(transcript_id_token)
|
|
717
|
+
|
|
718
|
+
@asynccontextmanager
|
|
719
|
+
async def async_transcript_context(
|
|
720
|
+
self,
|
|
721
|
+
name: Optional[str] = None,
|
|
722
|
+
transcript_id: Optional[str] = None,
|
|
723
|
+
description: Optional[str] = None,
|
|
724
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
725
|
+
transcript_group_id: Optional[str] = None,
|
|
726
|
+
) -> AsyncIterator[str]:
|
|
727
|
+
"""
|
|
728
|
+
Async context manager for setting up a transcript context.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
name: Optional transcript name
|
|
732
|
+
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
733
|
+
description: Optional transcript description
|
|
734
|
+
metadata: Optional metadata to send to backend
|
|
735
|
+
transcript_group_id: Optional transcript group ID
|
|
736
|
+
|
|
737
|
+
Yields:
|
|
738
|
+
The transcript ID
|
|
739
|
+
"""
|
|
740
|
+
if not self._initialized:
|
|
741
|
+
raise RuntimeError(
|
|
742
|
+
"Tracer is not initialized. Call initialize_tracing() before using transcript context."
|
|
743
|
+
)
|
|
665
744
|
|
|
666
|
-
if agent_run_id is None:
|
|
667
|
-
agent_run_id = str(uuid.uuid4())
|
|
668
745
|
if transcript_id is None:
|
|
669
746
|
transcript_id = str(uuid.uuid4())
|
|
670
747
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
748
|
+
# Determine transcript group ID before setting new context
|
|
749
|
+
if transcript_group_id is None:
|
|
750
|
+
try:
|
|
751
|
+
transcript_group_id = self._transcript_group_id_var.get()
|
|
752
|
+
except LookupError:
|
|
753
|
+
# No current transcript group context, this transcript has no group
|
|
754
|
+
transcript_group_id = None
|
|
676
755
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
756
|
+
# Set context variable for this execution context
|
|
757
|
+
transcript_id_token: Token[str] = self._transcript_id_var.set(transcript_id)
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
# Send transcript data and metadata to backend
|
|
761
|
+
try:
|
|
762
|
+
self.send_transcript_metadata(
|
|
763
|
+
transcript_id, name, description, transcript_group_id, metadata
|
|
764
|
+
)
|
|
765
|
+
except Exception as e:
|
|
766
|
+
logger.warning(f"Failed sending transcript data: {e}")
|
|
680
767
|
|
|
681
|
-
|
|
768
|
+
yield transcript_id
|
|
769
|
+
finally:
|
|
770
|
+
# Reset context variable to previous state
|
|
771
|
+
self._transcript_id_var.reset(transcript_id_token)
|
|
682
772
|
|
|
683
|
-
def
|
|
773
|
+
def send_transcript_group_metadata(
|
|
774
|
+
self,
|
|
775
|
+
transcript_group_id: str,
|
|
776
|
+
name: Optional[str] = None,
|
|
777
|
+
description: Optional[str] = None,
|
|
778
|
+
parent_transcript_group_id: Optional[str] = None,
|
|
779
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
780
|
+
) -> None:
|
|
684
781
|
"""
|
|
685
|
-
|
|
782
|
+
Send transcript group data to the backend.
|
|
686
783
|
|
|
687
784
|
Args:
|
|
688
|
-
|
|
785
|
+
transcript_group_id: The transcript group ID
|
|
786
|
+
name: Optional transcript group name
|
|
787
|
+
description: Optional transcript group description
|
|
788
|
+
parent_transcript_group_id: Optional parent transcript group ID
|
|
789
|
+
metadata: Optional metadata to send
|
|
689
790
|
"""
|
|
690
|
-
|
|
691
|
-
|
|
791
|
+
collection_id = self.collection_id
|
|
792
|
+
payload: Dict[str, Any] = {
|
|
793
|
+
"collection_id": collection_id,
|
|
794
|
+
"transcript_group_id": transcript_group_id,
|
|
795
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if name is not None:
|
|
799
|
+
payload["name"] = name
|
|
800
|
+
if description is not None:
|
|
801
|
+
payload["description"] = description
|
|
802
|
+
if parent_transcript_group_id is not None:
|
|
803
|
+
payload["parent_transcript_group_id"] = parent_transcript_group_id
|
|
804
|
+
if metadata is not None:
|
|
805
|
+
payload["metadata"] = metadata
|
|
806
|
+
|
|
807
|
+
self._post_json("/v1/transcript-group-metadata", payload)
|
|
692
808
|
|
|
693
|
-
|
|
809
|
+
@contextmanager
|
|
810
|
+
def transcript_group_context(
|
|
811
|
+
self,
|
|
812
|
+
name: Optional[str] = None,
|
|
813
|
+
transcript_group_id: Optional[str] = None,
|
|
814
|
+
description: Optional[str] = None,
|
|
815
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
816
|
+
parent_transcript_group_id: Optional[str] = None,
|
|
817
|
+
) -> Iterator[str]:
|
|
694
818
|
"""
|
|
695
|
-
|
|
819
|
+
Context manager for setting up a transcript group context.
|
|
696
820
|
|
|
697
821
|
Args:
|
|
698
|
-
name:
|
|
699
|
-
|
|
822
|
+
name: Optional transcript group name
|
|
823
|
+
transcript_group_id: Optional transcript group ID (auto-generated if not provided)
|
|
824
|
+
description: Optional transcript group description
|
|
825
|
+
metadata: Optional metadata to send to backend
|
|
826
|
+
parent_transcript_group_id: Optional parent transcript group ID
|
|
700
827
|
|
|
701
|
-
|
|
702
|
-
The
|
|
828
|
+
Yields:
|
|
829
|
+
The transcript group ID
|
|
703
830
|
"""
|
|
704
831
|
if not self._initialized:
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
raise RuntimeError("Tracer not initialized")
|
|
832
|
+
raise RuntimeError(
|
|
833
|
+
"Tracer is not initialized. Call initialize_tracing() before using transcript group context."
|
|
834
|
+
)
|
|
709
835
|
|
|
710
|
-
|
|
836
|
+
if transcript_group_id is None:
|
|
837
|
+
transcript_group_id = str(uuid.uuid4())
|
|
711
838
|
|
|
712
|
-
|
|
713
|
-
|
|
839
|
+
# Determine parent transcript group ID before setting new context
|
|
840
|
+
if parent_transcript_group_id is None:
|
|
841
|
+
try:
|
|
842
|
+
parent_transcript_group_id = self._transcript_group_id_var.get()
|
|
843
|
+
except LookupError:
|
|
844
|
+
# No current transcript group context, this becomes a root group
|
|
845
|
+
parent_transcript_group_id = None
|
|
846
|
+
|
|
847
|
+
# Set context variable for this execution context
|
|
848
|
+
transcript_group_id_token: Token[str] = self._transcript_group_id_var.set(
|
|
849
|
+
transcript_group_id
|
|
714
850
|
)
|
|
715
851
|
|
|
716
|
-
|
|
852
|
+
try:
|
|
853
|
+
# Send transcript group data and metadata to backend
|
|
854
|
+
try:
|
|
855
|
+
self.send_transcript_group_metadata(
|
|
856
|
+
transcript_group_id, name, description, parent_transcript_group_id, metadata
|
|
857
|
+
)
|
|
858
|
+
except Exception as e:
|
|
859
|
+
logger.warning(f"Failed sending transcript group data: {e}")
|
|
860
|
+
|
|
861
|
+
yield transcript_group_id
|
|
862
|
+
finally:
|
|
863
|
+
# Reset context variable to previous state
|
|
864
|
+
self._transcript_group_id_var.reset(transcript_group_id_token)
|
|
717
865
|
|
|
718
|
-
|
|
866
|
+
@asynccontextmanager
|
|
867
|
+
async def async_transcript_group_context(
|
|
868
|
+
self,
|
|
869
|
+
name: Optional[str] = None,
|
|
870
|
+
transcript_group_id: Optional[str] = None,
|
|
871
|
+
description: Optional[str] = None,
|
|
872
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
873
|
+
parent_transcript_group_id: Optional[str] = None,
|
|
874
|
+
) -> AsyncIterator[str]:
|
|
719
875
|
"""
|
|
720
|
-
|
|
876
|
+
Async context manager for setting up a transcript group context.
|
|
721
877
|
|
|
722
878
|
Args:
|
|
723
|
-
|
|
879
|
+
name: Optional transcript group name
|
|
880
|
+
transcript_group_id: Optional transcript group ID (auto-generated if not provided)
|
|
881
|
+
description: Optional transcript group description
|
|
882
|
+
metadata: Optional metadata to send to backend
|
|
883
|
+
parent_transcript_group_id: Optional parent transcript group ID
|
|
884
|
+
|
|
885
|
+
Yields:
|
|
886
|
+
The transcript group ID
|
|
724
887
|
"""
|
|
725
|
-
if
|
|
726
|
-
|
|
888
|
+
if not self._initialized:
|
|
889
|
+
raise RuntimeError(
|
|
890
|
+
"Tracer is not initialized. Call initialize_tracing() before using transcript group context."
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
if transcript_group_id is None:
|
|
894
|
+
transcript_group_id = str(uuid.uuid4())
|
|
895
|
+
|
|
896
|
+
# Determine parent transcript group ID before setting new context
|
|
897
|
+
if parent_transcript_group_id is None:
|
|
898
|
+
try:
|
|
899
|
+
parent_transcript_group_id = self._transcript_group_id_var.get()
|
|
900
|
+
except LookupError:
|
|
901
|
+
# No current transcript group context, this becomes a root group
|
|
902
|
+
parent_transcript_group_id = None
|
|
903
|
+
|
|
904
|
+
# Set context variable for this execution context
|
|
905
|
+
transcript_group_id_token: Token[str] = self._transcript_group_id_var.set(
|
|
906
|
+
transcript_group_id
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
try:
|
|
910
|
+
# Send transcript group data and metadata to backend
|
|
911
|
+
try:
|
|
912
|
+
self.send_transcript_group_metadata(
|
|
913
|
+
transcript_group_id, name, description, parent_transcript_group_id, metadata
|
|
914
|
+
)
|
|
915
|
+
except Exception as e:
|
|
916
|
+
logger.warning(f"Failed sending transcript group data: {e}")
|
|
917
|
+
|
|
918
|
+
yield transcript_group_id
|
|
919
|
+
finally:
|
|
920
|
+
# Reset context variable to previous state
|
|
921
|
+
self._transcript_group_id_var.reset(transcript_group_id_token)
|
|
922
|
+
|
|
923
|
+
def _send_trace_done(self) -> None:
|
|
924
|
+
collection_id = self.collection_id
|
|
925
|
+
payload: Dict[str, Any] = {
|
|
926
|
+
"collection_id": collection_id,
|
|
927
|
+
"status": "completed",
|
|
928
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
929
|
+
}
|
|
930
|
+
self._post_json("/v1/trace-done", payload)
|
|
727
931
|
|
|
728
932
|
|
|
729
|
-
# Global instance for easy access
|
|
730
933
|
_global_tracer: Optional[DocentTracer] = None
|
|
731
934
|
|
|
732
935
|
|
|
733
936
|
def initialize_tracing(
|
|
734
|
-
collection_name: str =
|
|
937
|
+
collection_name: str = DEFAULT_COLLECTION_NAME,
|
|
735
938
|
collection_id: Optional[str] = None,
|
|
736
939
|
endpoint: Union[str, List[str]] = DEFAULT_ENDPOINT,
|
|
737
940
|
headers: Optional[Dict[str, str]] = None,
|
|
@@ -739,7 +942,6 @@ def initialize_tracing(
|
|
|
739
942
|
enable_console_export: bool = False,
|
|
740
943
|
enable_otlp_export: bool = True,
|
|
741
944
|
disable_batch: bool = False,
|
|
742
|
-
span_postprocess_callback: Optional[Callable[[ReadableSpan], None]] = None,
|
|
743
945
|
) -> DocentTracer:
|
|
744
946
|
"""
|
|
745
947
|
Initialize the global Docent tracer.
|
|
@@ -756,7 +958,6 @@ def initialize_tracing(
|
|
|
756
958
|
enable_console_export: Whether to export spans to console
|
|
757
959
|
enable_otlp_export: Whether to export spans to OTLP endpoint
|
|
758
960
|
disable_batch: Whether to disable batch processing (use SimpleSpanProcessor)
|
|
759
|
-
span_postprocess_callback: Optional callback for post-processing spans
|
|
760
961
|
|
|
761
962
|
Returns:
|
|
762
963
|
The initialized Docent tracer
|
|
@@ -782,12 +983,8 @@ def initialize_tracing(
|
|
|
782
983
|
enable_console_export=enable_console_export,
|
|
783
984
|
enable_otlp_export=enable_otlp_export,
|
|
784
985
|
disable_batch=disable_batch,
|
|
785
|
-
span_postprocess_callback=span_postprocess_callback,
|
|
786
986
|
)
|
|
787
987
|
_global_tracer.initialize()
|
|
788
|
-
else:
|
|
789
|
-
# If already initialized, ensure it's properly set up
|
|
790
|
-
_global_tracer.initialize()
|
|
791
988
|
|
|
792
989
|
return _global_tracer
|
|
793
990
|
|
|
@@ -795,8 +992,7 @@ def initialize_tracing(
|
|
|
795
992
|
def get_tracer() -> DocentTracer:
|
|
796
993
|
"""Get the global Docent tracer."""
|
|
797
994
|
if _global_tracer is None:
|
|
798
|
-
|
|
799
|
-
return initialize_tracing()
|
|
995
|
+
raise RuntimeError("Docent tracer not initialized")
|
|
800
996
|
return _global_tracer
|
|
801
997
|
|
|
802
998
|
|
|
@@ -827,20 +1023,9 @@ def set_disabled(disabled: bool) -> None:
|
|
|
827
1023
|
_global_tracer.set_disabled(disabled)
|
|
828
1024
|
|
|
829
1025
|
|
|
830
|
-
def get_api_key() -> Optional[str]:
|
|
831
|
-
"""
|
|
832
|
-
Get the API key from environment variable.
|
|
833
|
-
|
|
834
|
-
Returns:
|
|
835
|
-
The API key from DOCENT_API_KEY environment variable, or None if not set
|
|
836
|
-
"""
|
|
837
|
-
return os.environ.get("DOCENT_API_KEY")
|
|
838
|
-
|
|
839
|
-
|
|
840
1026
|
def agent_run_score(name: str, score: float, attributes: Optional[Dict[str, Any]] = None) -> None:
|
|
841
1027
|
"""
|
|
842
|
-
|
|
843
|
-
Automatically works in both sync and async contexts.
|
|
1028
|
+
Send a score to the backend for the current agent run.
|
|
844
1029
|
|
|
845
1030
|
Args:
|
|
846
1031
|
name: Name of the score metric
|
|
@@ -848,22 +1033,16 @@ def agent_run_score(name: str, score: float, attributes: Optional[Dict[str, Any]
|
|
|
848
1033
|
attributes: Optional additional attributes for the score event
|
|
849
1034
|
"""
|
|
850
1035
|
try:
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
if attributes:
|
|
860
|
-
event_attributes.update(attributes)
|
|
861
|
-
|
|
862
|
-
current_span.add_event(name="agent_run_score", attributes=event_attributes)
|
|
863
|
-
else:
|
|
864
|
-
logger.warning("No current span available for recording score")
|
|
1036
|
+
tracer: DocentTracer = get_tracer()
|
|
1037
|
+
agent_run_id = tracer.get_current_agent_run_id()
|
|
1038
|
+
|
|
1039
|
+
if not agent_run_id:
|
|
1040
|
+
logger.warning("No active agent run context. Score will not be sent.")
|
|
1041
|
+
return
|
|
1042
|
+
|
|
1043
|
+
tracer.send_agent_run_score(agent_run_id, name, score, attributes)
|
|
865
1044
|
except Exception as e:
|
|
866
|
-
logger.error(f"Failed to
|
|
1045
|
+
logger.error(f"Failed to send score: {e}")
|
|
867
1046
|
|
|
868
1047
|
|
|
869
1048
|
def _flatten_dict(d: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
|
|
@@ -878,31 +1057,9 @@ def _flatten_dict(d: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
|
|
|
878
1057
|
return flattened
|
|
879
1058
|
|
|
880
1059
|
|
|
881
|
-
def _add_metadata_event_to_span(span: Span, metadata: Dict[str, Any]) -> None:
|
|
882
|
-
"""
|
|
883
|
-
Add metadata as an event to a span.
|
|
884
|
-
|
|
885
|
-
Args:
|
|
886
|
-
span: The span to add the event to
|
|
887
|
-
metadata: Dictionary of metadata (can be nested)
|
|
888
|
-
"""
|
|
889
|
-
if span and hasattr(span, "add_event"):
|
|
890
|
-
event_attributes: dict[str, Any] = {
|
|
891
|
-
"event.type": "metadata",
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
# Flatten nested metadata and add as event attributes
|
|
895
|
-
flattened_metadata = _flatten_dict(metadata)
|
|
896
|
-
for key, value in flattened_metadata.items():
|
|
897
|
-
event_attributes[f"metadata.{key}"] = value
|
|
898
|
-
span.add_event(name="agent_run_metadata", attributes=event_attributes)
|
|
899
|
-
|
|
900
|
-
|
|
901
1060
|
def agent_run_metadata(metadata: Dict[str, Any]) -> None:
|
|
902
1061
|
"""
|
|
903
|
-
|
|
904
|
-
Automatically works in both sync and async contexts.
|
|
905
|
-
Supports nested dictionaries by flattening them with dot notation.
|
|
1062
|
+
Send metadata directly to the backend for the current agent run.
|
|
906
1063
|
|
|
907
1064
|
Args:
|
|
908
1065
|
metadata: Dictionary of metadata to attach to the current span (can be nested)
|
|
@@ -912,28 +1069,49 @@ def agent_run_metadata(metadata: Dict[str, Any]) -> None:
|
|
|
912
1069
|
agent_run_metadata({"user": {"id": "123", "name": "John"}, "config": {"model": "gpt-4"}})
|
|
913
1070
|
"""
|
|
914
1071
|
try:
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1072
|
+
tracer = get_tracer()
|
|
1073
|
+
agent_run_id = tracer.get_current_agent_run_id()
|
|
1074
|
+
if not agent_run_id:
|
|
1075
|
+
logger.warning("No active agent run context. Metadata will not be sent.")
|
|
1076
|
+
return
|
|
1077
|
+
|
|
1078
|
+
tracer.send_agent_run_metadata(agent_run_id, metadata)
|
|
920
1079
|
except Exception as e:
|
|
921
|
-
logger.error(f"Failed to
|
|
1080
|
+
logger.error(f"Failed to send metadata: {e}")
|
|
922
1081
|
|
|
923
1082
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1083
|
+
def transcript_metadata(
|
|
1084
|
+
name: Optional[str] = None,
|
|
1085
|
+
description: Optional[str] = None,
|
|
1086
|
+
transcript_group_id: Optional[str] = None,
|
|
1087
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1088
|
+
) -> None:
|
|
927
1089
|
"""
|
|
928
|
-
|
|
929
|
-
|
|
1090
|
+
Send transcript metadata directly to the backend for the current transcript.
|
|
1091
|
+
|
|
1092
|
+
Args:
|
|
1093
|
+
name: Optional transcript name
|
|
1094
|
+
description: Optional transcript description
|
|
1095
|
+
parent_transcript_id: Optional parent transcript ID
|
|
1096
|
+
metadata: Optional metadata to send
|
|
1097
|
+
|
|
1098
|
+
Example:
|
|
1099
|
+
transcript_metadata(name="data_processing", description="Process user data")
|
|
1100
|
+
transcript_metadata(metadata={"user": "John", "model": "gpt-4"})
|
|
1101
|
+
transcript_metadata(name="validation", parent_transcript_id="parent-123")
|
|
930
1102
|
"""
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1103
|
+
try:
|
|
1104
|
+
tracer = get_tracer()
|
|
1105
|
+
transcript_id = tracer.get_current_transcript_id()
|
|
1106
|
+
if not transcript_id:
|
|
1107
|
+
logger.warning("No active transcript context. Metadata will not be sent.")
|
|
1108
|
+
return
|
|
1109
|
+
|
|
1110
|
+
tracer.send_transcript_metadata(
|
|
1111
|
+
transcript_id, name, description, transcript_group_id, metadata
|
|
1112
|
+
)
|
|
1113
|
+
except Exception as e:
|
|
1114
|
+
logger.error(f"Failed to send transcript metadata: {e}")
|
|
937
1115
|
|
|
938
1116
|
|
|
939
1117
|
class AgentRunContext:
|
|
@@ -1084,3 +1262,359 @@ def agent_run_context(
|
|
|
1084
1262
|
pass
|
|
1085
1263
|
"""
|
|
1086
1264
|
return AgentRunContext(agent_run_id, transcript_id, metadata=metadata, **attributes)
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
class TranscriptContext:
|
|
1268
|
+
"""Context manager for creating and managing transcripts."""
|
|
1269
|
+
|
|
1270
|
+
def __init__(
|
|
1271
|
+
self,
|
|
1272
|
+
name: Optional[str] = None,
|
|
1273
|
+
transcript_id: Optional[str] = None,
|
|
1274
|
+
description: Optional[str] = None,
|
|
1275
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1276
|
+
transcript_group_id: Optional[str] = None,
|
|
1277
|
+
):
|
|
1278
|
+
self.name = name
|
|
1279
|
+
self.transcript_id = transcript_id
|
|
1280
|
+
self.description = description
|
|
1281
|
+
self.metadata = metadata
|
|
1282
|
+
self.transcript_group_id = transcript_group_id
|
|
1283
|
+
self._sync_context: Optional[Any] = None
|
|
1284
|
+
self._async_context: Optional[Any] = None
|
|
1285
|
+
|
|
1286
|
+
def __enter__(self) -> str:
|
|
1287
|
+
"""Sync context manager entry."""
|
|
1288
|
+
self._sync_context = get_tracer().transcript_context(
|
|
1289
|
+
name=self.name,
|
|
1290
|
+
transcript_id=self.transcript_id,
|
|
1291
|
+
description=self.description,
|
|
1292
|
+
metadata=self.metadata,
|
|
1293
|
+
transcript_group_id=self.transcript_group_id,
|
|
1294
|
+
)
|
|
1295
|
+
return self._sync_context.__enter__()
|
|
1296
|
+
|
|
1297
|
+
def __exit__(self, exc_type: type[BaseException], exc_val: Any, exc_tb: Any) -> None:
|
|
1298
|
+
"""Sync context manager exit."""
|
|
1299
|
+
if self._sync_context:
|
|
1300
|
+
self._sync_context.__exit__(exc_type, exc_val, exc_tb)
|
|
1301
|
+
|
|
1302
|
+
async def __aenter__(self) -> str:
|
|
1303
|
+
"""Async context manager entry."""
|
|
1304
|
+
self._async_context = get_tracer().async_transcript_context(
|
|
1305
|
+
name=self.name,
|
|
1306
|
+
transcript_id=self.transcript_id,
|
|
1307
|
+
description=self.description,
|
|
1308
|
+
metadata=self.metadata,
|
|
1309
|
+
transcript_group_id=self.transcript_group_id,
|
|
1310
|
+
)
|
|
1311
|
+
return await self._async_context.__aenter__()
|
|
1312
|
+
|
|
1313
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
1314
|
+
"""Async context manager exit."""
|
|
1315
|
+
if self._async_context:
|
|
1316
|
+
await self._async_context.__aexit__(exc_type, exc_val, exc_tb)
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def transcript(
|
|
1320
|
+
func: Optional[Callable[..., Any]] = None,
|
|
1321
|
+
*,
|
|
1322
|
+
name: Optional[str] = None,
|
|
1323
|
+
transcript_id: Optional[str] = None,
|
|
1324
|
+
description: Optional[str] = None,
|
|
1325
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1326
|
+
transcript_group_id: Optional[str] = None,
|
|
1327
|
+
):
|
|
1328
|
+
"""
|
|
1329
|
+
Decorator to wrap a function in a transcript context.
|
|
1330
|
+
Injects transcript_id as a function attribute.
|
|
1331
|
+
|
|
1332
|
+
Example:
|
|
1333
|
+
@transcript
|
|
1334
|
+
def my_func(x, y):
|
|
1335
|
+
print(my_func.docent.transcript_id)
|
|
1336
|
+
|
|
1337
|
+
@transcript(name="data_processing", description="Process user data")
|
|
1338
|
+
def my_func_with_name(x, y):
|
|
1339
|
+
print(my_func_with_name.docent.transcript_id)
|
|
1340
|
+
|
|
1341
|
+
@transcript(metadata={"user": "John", "model": "gpt-4"})
|
|
1342
|
+
async def my_async_func(z):
|
|
1343
|
+
print(my_async_func.docent.transcript_id)
|
|
1344
|
+
"""
|
|
1345
|
+
import functools
|
|
1346
|
+
import inspect
|
|
1347
|
+
|
|
1348
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
1349
|
+
if inspect.iscoroutinefunction(f):
|
|
1350
|
+
|
|
1351
|
+
@functools.wraps(f)
|
|
1352
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1353
|
+
async with TranscriptContext(
|
|
1354
|
+
name=name,
|
|
1355
|
+
transcript_id=transcript_id,
|
|
1356
|
+
description=description,
|
|
1357
|
+
metadata=metadata,
|
|
1358
|
+
transcript_group_id=transcript_group_id,
|
|
1359
|
+
) as transcript_id_result:
|
|
1360
|
+
# Store docent data as function attributes
|
|
1361
|
+
setattr(
|
|
1362
|
+
async_wrapper,
|
|
1363
|
+
"docent",
|
|
1364
|
+
type(
|
|
1365
|
+
"DocentData",
|
|
1366
|
+
(),
|
|
1367
|
+
{
|
|
1368
|
+
"transcript_id": transcript_id_result,
|
|
1369
|
+
},
|
|
1370
|
+
)(),
|
|
1371
|
+
)
|
|
1372
|
+
return await f(*args, **kwargs)
|
|
1373
|
+
|
|
1374
|
+
return async_wrapper
|
|
1375
|
+
else:
|
|
1376
|
+
|
|
1377
|
+
@functools.wraps(f)
|
|
1378
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1379
|
+
with TranscriptContext(
|
|
1380
|
+
name=name,
|
|
1381
|
+
transcript_id=transcript_id,
|
|
1382
|
+
description=description,
|
|
1383
|
+
metadata=metadata,
|
|
1384
|
+
transcript_group_id=transcript_group_id,
|
|
1385
|
+
) as transcript_id_result:
|
|
1386
|
+
# Store docent data as function attributes
|
|
1387
|
+
setattr(
|
|
1388
|
+
sync_wrapper,
|
|
1389
|
+
"docent",
|
|
1390
|
+
type(
|
|
1391
|
+
"DocentData",
|
|
1392
|
+
(),
|
|
1393
|
+
{
|
|
1394
|
+
"transcript_id": transcript_id_result,
|
|
1395
|
+
},
|
|
1396
|
+
)(),
|
|
1397
|
+
)
|
|
1398
|
+
return f(*args, **kwargs)
|
|
1399
|
+
|
|
1400
|
+
return sync_wrapper
|
|
1401
|
+
|
|
1402
|
+
if func is None:
|
|
1403
|
+
return decorator
|
|
1404
|
+
else:
|
|
1405
|
+
return decorator(func)
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def transcript_context(
|
|
1409
|
+
name: Optional[str] = None,
|
|
1410
|
+
transcript_id: Optional[str] = None,
|
|
1411
|
+
description: Optional[str] = None,
|
|
1412
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1413
|
+
transcript_group_id: Optional[str] = None,
|
|
1414
|
+
) -> TranscriptContext:
|
|
1415
|
+
"""
|
|
1416
|
+
Create a transcript context for tracing.
|
|
1417
|
+
|
|
1418
|
+
Args:
|
|
1419
|
+
name: Optional transcript name
|
|
1420
|
+
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
1421
|
+
description: Optional transcript description
|
|
1422
|
+
metadata: Optional metadata to attach to the transcript
|
|
1423
|
+
parent_transcript_id: Optional parent transcript ID
|
|
1424
|
+
|
|
1425
|
+
Returns:
|
|
1426
|
+
A context manager that can be used with both 'with' and 'async with'
|
|
1427
|
+
|
|
1428
|
+
Example:
|
|
1429
|
+
# Sync usage
|
|
1430
|
+
with transcript_context(name="data_processing") as transcript_id:
|
|
1431
|
+
pass
|
|
1432
|
+
|
|
1433
|
+
# Async usage
|
|
1434
|
+
async with transcript_context(description="Process user data") as transcript_id:
|
|
1435
|
+
pass
|
|
1436
|
+
|
|
1437
|
+
# With metadata
|
|
1438
|
+
with transcript_context(metadata={"user": "John", "model": "gpt-4"}) as transcript_id:
|
|
1439
|
+
pass
|
|
1440
|
+
"""
|
|
1441
|
+
return TranscriptContext(name, transcript_id, description, metadata, transcript_group_id)
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
class TranscriptGroupContext:
|
|
1445
|
+
"""Context manager for creating and managing transcript groups."""
|
|
1446
|
+
|
|
1447
|
+
def __init__(
|
|
1448
|
+
self,
|
|
1449
|
+
name: Optional[str] = None,
|
|
1450
|
+
transcript_group_id: Optional[str] = None,
|
|
1451
|
+
description: Optional[str] = None,
|
|
1452
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1453
|
+
parent_transcript_group_id: Optional[str] = None,
|
|
1454
|
+
):
|
|
1455
|
+
self.name = name
|
|
1456
|
+
self.transcript_group_id = transcript_group_id
|
|
1457
|
+
self.description = description
|
|
1458
|
+
self.metadata = metadata
|
|
1459
|
+
self.parent_transcript_group_id = parent_transcript_group_id
|
|
1460
|
+
self._sync_context: Optional[Any] = None
|
|
1461
|
+
self._async_context: Optional[Any] = None
|
|
1462
|
+
|
|
1463
|
+
def __enter__(self) -> str:
|
|
1464
|
+
"""Sync context manager entry."""
|
|
1465
|
+
self._sync_context = get_tracer().transcript_group_context(
|
|
1466
|
+
name=self.name,
|
|
1467
|
+
transcript_group_id=self.transcript_group_id,
|
|
1468
|
+
description=self.description,
|
|
1469
|
+
metadata=self.metadata,
|
|
1470
|
+
parent_transcript_group_id=self.parent_transcript_group_id,
|
|
1471
|
+
)
|
|
1472
|
+
return self._sync_context.__enter__()
|
|
1473
|
+
|
|
1474
|
+
def __exit__(self, exc_type: type[BaseException], exc_val: Any, exc_tb: Any) -> None:
|
|
1475
|
+
"""Sync context manager exit."""
|
|
1476
|
+
if self._sync_context:
|
|
1477
|
+
self._sync_context.__exit__(exc_type, exc_val, exc_tb)
|
|
1478
|
+
|
|
1479
|
+
async def __aenter__(self) -> str:
|
|
1480
|
+
"""Async context manager entry."""
|
|
1481
|
+
self._async_context = get_tracer().async_transcript_group_context(
|
|
1482
|
+
name=self.name,
|
|
1483
|
+
transcript_group_id=self.transcript_group_id,
|
|
1484
|
+
description=self.description,
|
|
1485
|
+
metadata=self.metadata,
|
|
1486
|
+
parent_transcript_group_id=self.parent_transcript_group_id,
|
|
1487
|
+
)
|
|
1488
|
+
return await self._async_context.__aenter__()
|
|
1489
|
+
|
|
1490
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
1491
|
+
"""Async context manager exit."""
|
|
1492
|
+
if self._async_context:
|
|
1493
|
+
await self._async_context.__aexit__(exc_type, exc_val, exc_tb)
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def transcript_group(
|
|
1497
|
+
func: Optional[Callable[..., Any]] = None,
|
|
1498
|
+
*,
|
|
1499
|
+
name: Optional[str] = None,
|
|
1500
|
+
transcript_group_id: Optional[str] = None,
|
|
1501
|
+
description: Optional[str] = None,
|
|
1502
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1503
|
+
parent_transcript_group_id: Optional[str] = None,
|
|
1504
|
+
):
|
|
1505
|
+
"""
|
|
1506
|
+
Decorator to wrap a function in a transcript group context.
|
|
1507
|
+
Injects transcript_group_id as a function attribute.
|
|
1508
|
+
|
|
1509
|
+
Example:
|
|
1510
|
+
@transcript_group
|
|
1511
|
+
def my_func(x, y):
|
|
1512
|
+
print(my_func.docent.transcript_group_id)
|
|
1513
|
+
|
|
1514
|
+
@transcript_group(name="data_processing", description="Process user data")
|
|
1515
|
+
def my_func_with_name(x, y):
|
|
1516
|
+
print(my_func_with_name.docent.transcript_group_id)
|
|
1517
|
+
|
|
1518
|
+
@transcript_group(metadata={"user": "John", "model": "gpt-4"})
|
|
1519
|
+
async def my_async_func(z):
|
|
1520
|
+
print(my_async_func.docent.transcript_group_id)
|
|
1521
|
+
"""
|
|
1522
|
+
import functools
|
|
1523
|
+
import inspect
|
|
1524
|
+
|
|
1525
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
1526
|
+
if inspect.iscoroutinefunction(f):
|
|
1527
|
+
|
|
1528
|
+
@functools.wraps(f)
|
|
1529
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1530
|
+
async with TranscriptGroupContext(
|
|
1531
|
+
name=name,
|
|
1532
|
+
transcript_group_id=transcript_group_id,
|
|
1533
|
+
description=description,
|
|
1534
|
+
metadata=metadata,
|
|
1535
|
+
parent_transcript_group_id=parent_transcript_group_id,
|
|
1536
|
+
) as transcript_group_id_result:
|
|
1537
|
+
# Store docent data as function attributes
|
|
1538
|
+
setattr(
|
|
1539
|
+
async_wrapper,
|
|
1540
|
+
"docent",
|
|
1541
|
+
type(
|
|
1542
|
+
"DocentData",
|
|
1543
|
+
(),
|
|
1544
|
+
{
|
|
1545
|
+
"transcript_group_id": transcript_group_id_result,
|
|
1546
|
+
},
|
|
1547
|
+
)(),
|
|
1548
|
+
)
|
|
1549
|
+
return await f(*args, **kwargs)
|
|
1550
|
+
|
|
1551
|
+
return async_wrapper
|
|
1552
|
+
else:
|
|
1553
|
+
|
|
1554
|
+
@functools.wraps(f)
|
|
1555
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1556
|
+
with TranscriptGroupContext(
|
|
1557
|
+
name=name,
|
|
1558
|
+
transcript_group_id=transcript_group_id,
|
|
1559
|
+
description=description,
|
|
1560
|
+
metadata=metadata,
|
|
1561
|
+
parent_transcript_group_id=parent_transcript_group_id,
|
|
1562
|
+
) as transcript_group_id_result:
|
|
1563
|
+
# Store docent data as function attributes
|
|
1564
|
+
setattr(
|
|
1565
|
+
sync_wrapper,
|
|
1566
|
+
"docent",
|
|
1567
|
+
type(
|
|
1568
|
+
"DocentData",
|
|
1569
|
+
(),
|
|
1570
|
+
{
|
|
1571
|
+
"transcript_group_id": transcript_group_id_result,
|
|
1572
|
+
},
|
|
1573
|
+
)(),
|
|
1574
|
+
)
|
|
1575
|
+
return f(*args, **kwargs)
|
|
1576
|
+
|
|
1577
|
+
return sync_wrapper
|
|
1578
|
+
|
|
1579
|
+
if func is None:
|
|
1580
|
+
return decorator
|
|
1581
|
+
else:
|
|
1582
|
+
return decorator(func)
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def transcript_group_context(
|
|
1586
|
+
name: Optional[str] = None,
|
|
1587
|
+
transcript_group_id: Optional[str] = None,
|
|
1588
|
+
description: Optional[str] = None,
|
|
1589
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1590
|
+
parent_transcript_group_id: Optional[str] = None,
|
|
1591
|
+
) -> TranscriptGroupContext:
|
|
1592
|
+
"""
|
|
1593
|
+
Create a transcript group context for tracing.
|
|
1594
|
+
|
|
1595
|
+
Args:
|
|
1596
|
+
name: Optional transcript group name
|
|
1597
|
+
transcript_group_id: Optional transcript group ID (auto-generated if not provided)
|
|
1598
|
+
description: Optional transcript group description
|
|
1599
|
+
metadata: Optional metadata to attach to the transcript group
|
|
1600
|
+
parent_transcript_group_id: Optional parent transcript group ID
|
|
1601
|
+
|
|
1602
|
+
Returns:
|
|
1603
|
+
A context manager that can be used with both 'with' and 'async with'
|
|
1604
|
+
|
|
1605
|
+
Example:
|
|
1606
|
+
# Sync usage
|
|
1607
|
+
with transcript_group_context(name="data_processing") as transcript_group_id:
|
|
1608
|
+
pass
|
|
1609
|
+
|
|
1610
|
+
# Async usage
|
|
1611
|
+
async with transcript_group_context(description="Process user data") as transcript_group_id:
|
|
1612
|
+
pass
|
|
1613
|
+
|
|
1614
|
+
# With metadata
|
|
1615
|
+
with transcript_group_context(metadata={"user": "John", "model": "gpt-4"}) as transcript_group_id:
|
|
1616
|
+
pass
|
|
1617
|
+
"""
|
|
1618
|
+
return TranscriptGroupContext(
|
|
1619
|
+
name, transcript_group_id, description, metadata, parent_transcript_group_id
|
|
1620
|
+
)
|