docent-python 0.1.2a0__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 +874 -326
- docent/trace_alt.py +513 -0
- docent/trace_temp.py +1086 -0
- docent_python-0.1.4a0.dist-info/METADATA +25 -0
- {docent_python-0.1.2a0.dist-info → docent_python-0.1.4a0.dist-info}/RECORD +12 -10
- docent_python-0.1.2a0.dist-info/METADATA +0 -24
- {docent_python-0.1.2a0.dist-info → docent_python-0.1.4a0.dist-info}/WHEEL +0 -0
- {docent_python-0.1.2a0.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,9 +10,12 @@ 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
|
+
from opentelemetry.context import Context
|
|
18
19
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCExporter
|
|
19
20
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPExporter
|
|
20
21
|
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
|
|
@@ -23,50 +24,29 @@ from opentelemetry.instrumentation.langchain import LangchainInstrumentor
|
|
|
23
24
|
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
|
|
24
25
|
from opentelemetry.instrumentation.threading import ThreadingInstrumentor
|
|
25
26
|
from opentelemetry.sdk.resources import Resource
|
|
26
|
-
from opentelemetry.sdk.trace import ReadableSpan, TracerProvider
|
|
27
|
+
from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor, TracerProvider
|
|
27
28
|
from opentelemetry.sdk.trace.export import (
|
|
28
29
|
BatchSpanProcessor,
|
|
29
30
|
ConsoleSpanExporter,
|
|
30
31
|
SimpleSpanProcessor,
|
|
31
32
|
)
|
|
33
|
+
from opentelemetry.trace import Span
|
|
32
34
|
|
|
33
35
|
# Configure logging
|
|
34
36
|
logging.basicConfig(level=logging.INFO)
|
|
35
37
|
logger = logging.getLogger(__name__)
|
|
36
|
-
|
|
38
|
+
logger.disabled = True
|
|
37
39
|
|
|
38
40
|
# Default configuration
|
|
39
41
|
DEFAULT_ENDPOINT = "https://api.docent.transluce.org/rest/telemetry"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _is_async_context() -> bool:
|
|
43
|
-
"""Detect if we're in an async context."""
|
|
44
|
-
try:
|
|
45
|
-
# Check if we're in an async function
|
|
46
|
-
frame = inspect.currentframe()
|
|
47
|
-
while frame:
|
|
48
|
-
if frame.f_code.co_flags & inspect.CO_COROUTINE:
|
|
49
|
-
return True
|
|
50
|
-
frame = frame.f_back
|
|
51
|
-
return False
|
|
52
|
-
except:
|
|
53
|
-
return False
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _is_running_in_event_loop() -> bool:
|
|
57
|
-
"""Check if we're running in an event loop."""
|
|
58
|
-
try:
|
|
59
|
-
asyncio.get_running_loop()
|
|
60
|
-
return True
|
|
61
|
-
except RuntimeError:
|
|
62
|
-
return False
|
|
42
|
+
DEFAULT_COLLECTION_NAME = "default-collection-name"
|
|
63
43
|
|
|
64
44
|
|
|
65
45
|
def _is_notebook() -> bool:
|
|
66
46
|
"""Check if we're running in a Jupyter notebook."""
|
|
67
47
|
try:
|
|
68
48
|
return "ipykernel" in sys.modules
|
|
69
|
-
except:
|
|
49
|
+
except Exception:
|
|
70
50
|
return False
|
|
71
51
|
|
|
72
52
|
|
|
@@ -75,7 +55,7 @@ class DocentTracer:
|
|
|
75
55
|
|
|
76
56
|
def __init__(
|
|
77
57
|
self,
|
|
78
|
-
collection_name: str =
|
|
58
|
+
collection_name: str = DEFAULT_COLLECTION_NAME,
|
|
79
59
|
collection_id: Optional[str] = None,
|
|
80
60
|
agent_run_id: Optional[str] = None,
|
|
81
61
|
endpoint: Union[str, List[str]] = DEFAULT_ENDPOINT,
|
|
@@ -84,7 +64,6 @@ class DocentTracer:
|
|
|
84
64
|
enable_console_export: bool = False,
|
|
85
65
|
enable_otlp_export: bool = True,
|
|
86
66
|
disable_batch: bool = False,
|
|
87
|
-
span_postprocess_callback: Optional[Callable[[ReadableSpan], None]] = None,
|
|
88
67
|
):
|
|
89
68
|
"""
|
|
90
69
|
Initialize Docent tracing manager.
|
|
@@ -99,7 +78,6 @@ class DocentTracer:
|
|
|
99
78
|
enable_console_export: Whether to export to console
|
|
100
79
|
enable_otlp_export: Whether to export to OTLP endpoint
|
|
101
80
|
disable_batch: Whether to disable batch processing (use SimpleSpanProcessor)
|
|
102
|
-
span_postprocess_callback: Optional callback for post-processing spans
|
|
103
81
|
"""
|
|
104
82
|
self.collection_name: str = collection_name
|
|
105
83
|
self.collection_id: str = collection_id if collection_id else str(uuid.uuid4())
|
|
@@ -127,29 +105,48 @@ class DocentTracer:
|
|
|
127
105
|
self.enable_console_export = enable_console_export
|
|
128
106
|
self.enable_otlp_export = enable_otlp_export
|
|
129
107
|
self.disable_batch = disable_batch
|
|
130
|
-
self.span_postprocess_callback = span_postprocess_callback
|
|
131
108
|
|
|
132
109
|
# Use separate tracer provider to avoid interfering with existing OTEL setup
|
|
133
|
-
self._tracer_provider: Optional[
|
|
134
|
-
self.
|
|
135
|
-
self.
|
|
136
|
-
self._tracer: Optional[Any] = None
|
|
110
|
+
self._tracer_provider: Optional[TracerProvider] = None
|
|
111
|
+
self._root_context: Optional[Context] = Context()
|
|
112
|
+
self._tracer: Optional[trace.Tracer] = None
|
|
137
113
|
self._initialized: bool = False
|
|
138
114
|
self._cleanup_registered: bool = False
|
|
139
115
|
self._disabled: bool = False
|
|
140
|
-
self._spans_processors: List[
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
self.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
116
|
+
self._spans_processors: List[Union[BatchSpanProcessor, SimpleSpanProcessor]] = []
|
|
117
|
+
|
|
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
|
|
123
|
+
self._collection_id_var: ContextVar[str] = contextvars.ContextVar("docent_collection_id")
|
|
124
|
+
self._agent_run_id_var: ContextVar[str] = contextvars.ContextVar("docent_agent_run_id")
|
|
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
|
+
)
|
|
129
|
+
self._attributes_var: ContextVar[dict[str, Any]] = contextvars.ContextVar(
|
|
130
|
+
"docent_attributes"
|
|
131
|
+
)
|
|
147
132
|
# Store atomic span order counters per transcript_id to persist across context switches
|
|
148
133
|
self._transcript_counters: defaultdict[str, itertools.count[int]] = defaultdict(
|
|
149
134
|
lambda: itertools.count(0)
|
|
150
135
|
)
|
|
151
136
|
self._transcript_counter_lock = threading.Lock()
|
|
152
137
|
|
|
138
|
+
def get_current_agent_run_id(self) -> Optional[str]:
|
|
139
|
+
"""
|
|
140
|
+
Get the current agent run ID from context.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The current agent run ID if available, None otherwise
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
return self._agent_run_id_var.get()
|
|
147
|
+
except LookupError:
|
|
148
|
+
return self.default_agent_run_id
|
|
149
|
+
|
|
153
150
|
def _register_cleanup(self):
|
|
154
151
|
"""Register cleanup handlers."""
|
|
155
152
|
if self._cleanup_registered:
|
|
@@ -170,13 +167,13 @@ class DocentTracer:
|
|
|
170
167
|
|
|
171
168
|
def _next_span_order(self, transcript_id: str) -> int:
|
|
172
169
|
"""
|
|
173
|
-
Get the next
|
|
170
|
+
Get the next span order for a given transcript_id.
|
|
174
171
|
Thread-safe and guaranteed to be unique and monotonic.
|
|
175
172
|
"""
|
|
176
173
|
with self._transcript_counter_lock:
|
|
177
174
|
return next(self._transcript_counters[transcript_id])
|
|
178
175
|
|
|
179
|
-
def _signal_handler(self, signum: int, frame:
|
|
176
|
+
def _signal_handler(self, signum: int, frame: Optional[object]):
|
|
180
177
|
"""Handle shutdown signals."""
|
|
181
178
|
self.cleanup()
|
|
182
179
|
sys.exit(0)
|
|
@@ -213,13 +210,15 @@ class DocentTracer:
|
|
|
213
210
|
|
|
214
211
|
return exporters
|
|
215
212
|
|
|
216
|
-
def _create_span_processor(
|
|
213
|
+
def _create_span_processor(
|
|
214
|
+
self, exporter: Union[HTTPExporter, GRPCExporter, ConsoleSpanExporter]
|
|
215
|
+
) -> Union[SimpleSpanProcessor, BatchSpanProcessor]:
|
|
217
216
|
"""Create appropriate span processor based on configuration."""
|
|
218
217
|
if self.disable_batch or _is_notebook():
|
|
219
|
-
simple_processor:
|
|
218
|
+
simple_processor: SimpleSpanProcessor = SimpleSpanProcessor(exporter)
|
|
220
219
|
return simple_processor
|
|
221
220
|
else:
|
|
222
|
-
batch_processor:
|
|
221
|
+
batch_processor: BatchSpanProcessor = BatchSpanProcessor(exporter)
|
|
223
222
|
return batch_processor
|
|
224
223
|
|
|
225
224
|
def initialize(self):
|
|
@@ -233,17 +232,16 @@ class DocentTracer:
|
|
|
233
232
|
resource=Resource.create({"service.name": self.collection_name})
|
|
234
233
|
)
|
|
235
234
|
|
|
236
|
-
# Add custom span processor for
|
|
237
|
-
class ContextSpanProcessor:
|
|
235
|
+
# Add custom span processor for agent_run_id and transcript_id
|
|
236
|
+
class ContextSpanProcessor(SpanProcessor):
|
|
238
237
|
def __init__(self, manager: "DocentTracer"):
|
|
239
238
|
self.manager: "DocentTracer" = manager
|
|
240
239
|
|
|
241
|
-
def on_start(self, span:
|
|
242
|
-
# Add collection_id, agent_run_id, transcript_id, and any other current attributes
|
|
243
|
-
# Always add collection_id as it's always available
|
|
240
|
+
def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
|
|
241
|
+
# Add collection_id, agent_run_id, transcript_id, transcript_group_id, and any other current attributes
|
|
244
242
|
span.set_attribute("collection_id", self.manager.collection_id)
|
|
245
243
|
|
|
246
|
-
#
|
|
244
|
+
# Set agent_run_id from context
|
|
247
245
|
try:
|
|
248
246
|
agent_run_id: str = self.manager._agent_run_id_var.get()
|
|
249
247
|
if agent_run_id:
|
|
@@ -255,7 +253,15 @@ class DocentTracer:
|
|
|
255
253
|
span.set_attribute("agent_run_id_default", True)
|
|
256
254
|
span.set_attribute("agent_run_id", self.manager.default_agent_run_id)
|
|
257
255
|
|
|
258
|
-
#
|
|
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
|
|
259
265
|
try:
|
|
260
266
|
transcript_id: str = self.manager._transcript_id_var.get()
|
|
261
267
|
if transcript_id:
|
|
@@ -267,7 +273,7 @@ class DocentTracer:
|
|
|
267
273
|
# transcript_id not available, skip it
|
|
268
274
|
pass
|
|
269
275
|
|
|
270
|
-
#
|
|
276
|
+
# Set custom attributes from context
|
|
271
277
|
try:
|
|
272
278
|
attributes: dict[str, Any] = self.manager._attributes_var.get()
|
|
273
279
|
for key, value in attributes.items():
|
|
@@ -276,14 +282,14 @@ class DocentTracer:
|
|
|
276
282
|
# attributes not available, skip them
|
|
277
283
|
pass
|
|
278
284
|
|
|
279
|
-
def on_end(self, span:
|
|
285
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
280
286
|
pass
|
|
281
287
|
|
|
282
288
|
def shutdown(self) -> None:
|
|
283
289
|
pass
|
|
284
290
|
|
|
285
|
-
def force_flush(self) ->
|
|
286
|
-
|
|
291
|
+
def force_flush(self, timeout_millis: Optional[float] = None) -> bool:
|
|
292
|
+
return True
|
|
287
293
|
|
|
288
294
|
# Configure span exporters for our isolated provider
|
|
289
295
|
if self.enable_otlp_export:
|
|
@@ -294,7 +300,9 @@ class DocentTracer:
|
|
|
294
300
|
if otlp_exporters:
|
|
295
301
|
# Create a processor for each exporter
|
|
296
302
|
for exporter in otlp_exporters:
|
|
297
|
-
otlp_processor:
|
|
303
|
+
otlp_processor: Union[SimpleSpanProcessor, BatchSpanProcessor] = (
|
|
304
|
+
self._create_span_processor(exporter)
|
|
305
|
+
)
|
|
298
306
|
self._tracer_provider.add_span_processor(otlp_processor)
|
|
299
307
|
self._spans_processors.append(otlp_processor)
|
|
300
308
|
|
|
@@ -305,8 +313,10 @@ class DocentTracer:
|
|
|
305
313
|
logger.warning("Failed to initialize OTLP exporter")
|
|
306
314
|
|
|
307
315
|
if self.enable_console_export:
|
|
308
|
-
console_exporter:
|
|
309
|
-
console_processor:
|
|
316
|
+
console_exporter: ConsoleSpanExporter = ConsoleSpanExporter()
|
|
317
|
+
console_processor: Union[SimpleSpanProcessor, BatchSpanProcessor] = (
|
|
318
|
+
self._create_span_processor(console_exporter)
|
|
319
|
+
)
|
|
310
320
|
self._tracer_provider.add_span_processor(console_processor)
|
|
311
321
|
self._spans_processors.append(console_processor)
|
|
312
322
|
|
|
@@ -317,20 +327,6 @@ class DocentTracer:
|
|
|
317
327
|
# Get tracer from our isolated provider (don't set global provider)
|
|
318
328
|
self._tracer = self._tracer_provider.get_tracer(__name__)
|
|
319
329
|
|
|
320
|
-
# Start root span
|
|
321
|
-
if self._tracer is None:
|
|
322
|
-
raise RuntimeError("Failed to get tracer from provider")
|
|
323
|
-
|
|
324
|
-
self._root_span = self._tracer.start_span(
|
|
325
|
-
"application_session",
|
|
326
|
-
attributes={
|
|
327
|
-
"service.name": self.collection_name,
|
|
328
|
-
"session.type": "application_root",
|
|
329
|
-
},
|
|
330
|
-
)
|
|
331
|
-
if self._root_span is not None:
|
|
332
|
-
self._root_context = trace.set_span_in_context(self._root_span)
|
|
333
|
-
|
|
334
330
|
# Instrument threading for better context propagation
|
|
335
331
|
try:
|
|
336
332
|
ThreadingInstrumentor().instrument()
|
|
@@ -377,31 +373,15 @@ class DocentTracer:
|
|
|
377
373
|
raise
|
|
378
374
|
|
|
379
375
|
def cleanup(self):
|
|
380
|
-
"""Clean up Docent tracing resources."""
|
|
376
|
+
"""Clean up Docent tracing resources and signal trace completion to backend."""
|
|
381
377
|
try:
|
|
382
|
-
#
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
attributes={
|
|
388
|
-
"event.type": "trace_end",
|
|
389
|
-
},
|
|
390
|
-
)
|
|
391
|
-
end_span.end()
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
self._root_span
|
|
395
|
-
and hasattr(self._root_span, "is_recording")
|
|
396
|
-
and self._root_span.is_recording()
|
|
397
|
-
):
|
|
398
|
-
self._root_span.end()
|
|
399
|
-
elif self._root_span:
|
|
400
|
-
# Fallback if is_recording is not available
|
|
401
|
-
self._root_span.end()
|
|
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}")
|
|
402
383
|
|
|
403
|
-
self.
|
|
404
|
-
self._root_context = None
|
|
384
|
+
self._root_context = None # type: ignore
|
|
405
385
|
|
|
406
386
|
# Shutdown our isolated tracer provider
|
|
407
387
|
if self._tracer_provider:
|
|
@@ -451,61 +431,19 @@ class DocentTracer:
|
|
|
451
431
|
self.close()
|
|
452
432
|
|
|
453
433
|
@property
|
|
454
|
-
def tracer(self) -> Optional[
|
|
434
|
+
def tracer(self) -> Optional[trace.Tracer]:
|
|
455
435
|
"""Get the tracer instance."""
|
|
456
436
|
if not self._initialized:
|
|
457
437
|
self.initialize()
|
|
458
438
|
return self._tracer
|
|
459
439
|
|
|
460
440
|
@property
|
|
461
|
-
def root_context(self) -> Optional[
|
|
441
|
+
def root_context(self) -> Optional[Context]:
|
|
462
442
|
"""Get the root context."""
|
|
463
443
|
if not self._initialized:
|
|
464
444
|
self.initialize()
|
|
465
445
|
return self._root_context
|
|
466
446
|
|
|
467
|
-
@contextmanager
|
|
468
|
-
def span(self, name: str, attributes: Optional[Dict[str, Any]] = None) -> Iterator[Any]:
|
|
469
|
-
"""
|
|
470
|
-
Context manager for creating spans with attributes.
|
|
471
|
-
"""
|
|
472
|
-
if not self._initialized:
|
|
473
|
-
self.initialize()
|
|
474
|
-
|
|
475
|
-
if self._tracer is None:
|
|
476
|
-
raise RuntimeError("Tracer not initialized")
|
|
477
|
-
|
|
478
|
-
span_attributes: dict[str, Any] = attributes or {}
|
|
479
|
-
|
|
480
|
-
with self._tracer.start_as_current_span(
|
|
481
|
-
name, context=self._root_context, attributes=span_attributes
|
|
482
|
-
) as span:
|
|
483
|
-
yield span
|
|
484
|
-
|
|
485
|
-
@asynccontextmanager
|
|
486
|
-
async def async_span(
|
|
487
|
-
self, name: str, attributes: Optional[Dict[str, Any]] = None
|
|
488
|
-
) -> AsyncIterator[Any]:
|
|
489
|
-
"""
|
|
490
|
-
Async context manager for creating spans with attributes.
|
|
491
|
-
|
|
492
|
-
Args:
|
|
493
|
-
name: Name of the span
|
|
494
|
-
attributes: Dictionary of attributes to add to the span
|
|
495
|
-
"""
|
|
496
|
-
if not self._initialized:
|
|
497
|
-
self.initialize()
|
|
498
|
-
|
|
499
|
-
if self._tracer is None:
|
|
500
|
-
raise RuntimeError("Tracer not initialized")
|
|
501
|
-
|
|
502
|
-
span_attributes: dict[str, Any] = attributes or {}
|
|
503
|
-
|
|
504
|
-
with self._tracer.start_as_current_span(
|
|
505
|
-
name, context=self._root_context, attributes=span_attributes
|
|
506
|
-
) as span:
|
|
507
|
-
yield span
|
|
508
|
-
|
|
509
447
|
@contextmanager
|
|
510
448
|
def agent_run_context(
|
|
511
449
|
self,
|
|
@@ -513,25 +451,22 @@ class DocentTracer:
|
|
|
513
451
|
transcript_id: Optional[str] = None,
|
|
514
452
|
metadata: Optional[Dict[str, Any]] = None,
|
|
515
453
|
**attributes: Any,
|
|
516
|
-
) -> Iterator[
|
|
454
|
+
) -> Iterator[tuple[str, str]]:
|
|
517
455
|
"""
|
|
518
456
|
Context manager for setting up an agent run context.
|
|
519
457
|
|
|
520
458
|
Args:
|
|
521
459
|
agent_run_id: Optional agent run ID (auto-generated if not provided)
|
|
522
460
|
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
523
|
-
metadata: Optional nested dictionary of metadata to
|
|
461
|
+
metadata: Optional nested dictionary of metadata to send to backend
|
|
524
462
|
**attributes: Additional attributes to add to the context
|
|
525
463
|
|
|
526
464
|
Yields:
|
|
527
|
-
Tuple of (
|
|
465
|
+
Tuple of (agent_run_id, transcript_id)
|
|
528
466
|
"""
|
|
529
467
|
if not self._initialized:
|
|
530
468
|
self.initialize()
|
|
531
469
|
|
|
532
|
-
if self._tracer is None:
|
|
533
|
-
raise RuntimeError("Tracer not initialized")
|
|
534
|
-
|
|
535
470
|
if agent_run_id is None:
|
|
536
471
|
agent_run_id = str(uuid.uuid4())
|
|
537
472
|
if transcript_id is None:
|
|
@@ -543,21 +478,14 @@ class DocentTracer:
|
|
|
543
478
|
attributes_token: Token[dict[str, Any]] = self._attributes_var.set(attributes)
|
|
544
479
|
|
|
545
480
|
try:
|
|
546
|
-
#
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
) as _span:
|
|
555
|
-
# Attach metadata as events if provided
|
|
556
|
-
if metadata:
|
|
557
|
-
_add_metadata_event_to_span(_span, metadata)
|
|
558
|
-
|
|
559
|
-
context = trace.get_current_span().get_span_context()
|
|
560
|
-
yield context, 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
|
|
561
489
|
finally:
|
|
562
490
|
self._agent_run_id_var.reset(agent_run_id_token)
|
|
563
491
|
self._transcript_id_var.reset(transcript_id_token)
|
|
@@ -570,7 +498,7 @@ class DocentTracer:
|
|
|
570
498
|
transcript_id: Optional[str] = None,
|
|
571
499
|
metadata: Optional[Dict[str, Any]] = None,
|
|
572
500
|
**attributes: Any,
|
|
573
|
-
) -> AsyncIterator[
|
|
501
|
+
) -> AsyncIterator[tuple[str, str]]:
|
|
574
502
|
"""
|
|
575
503
|
Async context manager for setting up an agent run context.
|
|
576
504
|
Modifies the OpenTelemetry context so all spans inherit agent_run_id and transcript_id.
|
|
@@ -578,141 +506,435 @@ class DocentTracer:
|
|
|
578
506
|
Args:
|
|
579
507
|
agent_run_id: Optional agent run ID (auto-generated if not provided)
|
|
580
508
|
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
581
|
-
metadata: Optional nested dictionary of metadata to
|
|
509
|
+
metadata: Optional nested dictionary of metadata to send to backend
|
|
582
510
|
**attributes: Additional attributes to add to the context
|
|
583
511
|
|
|
584
512
|
Yields:
|
|
585
|
-
Tuple of (
|
|
513
|
+
Tuple of (agent_run_id, transcript_id)
|
|
586
514
|
"""
|
|
587
515
|
if not self._initialized:
|
|
588
516
|
self.initialize()
|
|
589
517
|
|
|
590
|
-
if self._tracer is None:
|
|
591
|
-
raise RuntimeError("Tracer not initialized")
|
|
592
|
-
|
|
593
518
|
if agent_run_id is None:
|
|
594
519
|
agent_run_id = str(uuid.uuid4())
|
|
595
520
|
if transcript_id is None:
|
|
596
521
|
transcript_id = str(uuid.uuid4())
|
|
597
522
|
|
|
598
523
|
# Set context variables for this execution context
|
|
599
|
-
agent_run_id_token:
|
|
600
|
-
transcript_id_token:
|
|
601
|
-
attributes_token: Any = self._attributes_var.set(attributes)
|
|
524
|
+
agent_run_id_token: Token[str] = self._agent_run_id_var.set(agent_run_id)
|
|
525
|
+
transcript_id_token: Token[str] = self._transcript_id_var.set(transcript_id)
|
|
526
|
+
attributes_token: Token[dict[str, Any]] = self._attributes_var.set(attributes)
|
|
602
527
|
|
|
603
528
|
try:
|
|
604
|
-
#
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
) as _span:
|
|
613
|
-
# Attach metadata as events if provided
|
|
614
|
-
if metadata:
|
|
615
|
-
_add_metadata_event_to_span(_span, metadata)
|
|
616
|
-
|
|
617
|
-
context = trace.get_current_span().get_span_context()
|
|
618
|
-
yield context, 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
|
|
619
537
|
finally:
|
|
620
538
|
self._agent_run_id_var.reset(agent_run_id_token)
|
|
621
539
|
self._transcript_id_var.reset(transcript_id_token)
|
|
622
540
|
self._attributes_var.reset(attributes_token)
|
|
623
541
|
|
|
624
|
-
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(
|
|
625
565
|
self,
|
|
626
|
-
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,
|
|
627
667
|
transcript_id: Optional[str] = None,
|
|
628
|
-
|
|
629
|
-
|
|
668
|
+
description: Optional[str] = None,
|
|
669
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
670
|
+
transcript_group_id: Optional[str] = None,
|
|
671
|
+
) -> Iterator[str]:
|
|
630
672
|
"""
|
|
631
|
-
|
|
673
|
+
Context manager for setting up a transcript context.
|
|
632
674
|
|
|
633
675
|
Args:
|
|
634
|
-
|
|
676
|
+
name: Optional transcript name
|
|
635
677
|
transcript_id: Optional transcript ID (auto-generated if not provided)
|
|
636
|
-
|
|
678
|
+
description: Optional transcript description
|
|
679
|
+
metadata: Optional metadata to send to backend
|
|
680
|
+
transcript_group_id: Optional transcript group ID
|
|
637
681
|
|
|
638
|
-
|
|
639
|
-
|
|
682
|
+
Yields:
|
|
683
|
+
The transcript ID
|
|
640
684
|
"""
|
|
641
685
|
if not self._initialized:
|
|
642
|
-
|
|
686
|
+
raise RuntimeError(
|
|
687
|
+
"Tracer is not initialized. Call initialize_tracing() before using transcript context."
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
if transcript_id is None:
|
|
691
|
+
transcript_id = str(uuid.uuid4())
|
|
643
692
|
|
|
644
|
-
|
|
645
|
-
|
|
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
|
+
)
|
|
646
744
|
|
|
647
|
-
if agent_run_id is None:
|
|
648
|
-
agent_run_id = str(uuid.uuid4())
|
|
649
745
|
if transcript_id is None:
|
|
650
746
|
transcript_id = str(uuid.uuid4())
|
|
651
747
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
|
657
755
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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}")
|
|
661
767
|
|
|
662
|
-
|
|
768
|
+
yield transcript_id
|
|
769
|
+
finally:
|
|
770
|
+
# Reset context variable to previous state
|
|
771
|
+
self._transcript_id_var.reset(transcript_id_token)
|
|
663
772
|
|
|
664
|
-
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:
|
|
665
781
|
"""
|
|
666
|
-
|
|
782
|
+
Send transcript group data to the backend.
|
|
667
783
|
|
|
668
784
|
Args:
|
|
669
|
-
|
|
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
|
|
670
790
|
"""
|
|
671
|
-
|
|
672
|
-
|
|
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)
|
|
673
808
|
|
|
674
|
-
|
|
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]:
|
|
675
818
|
"""
|
|
676
|
-
|
|
819
|
+
Context manager for setting up a transcript group context.
|
|
677
820
|
|
|
678
821
|
Args:
|
|
679
|
-
name:
|
|
680
|
-
|
|
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
|
|
681
827
|
|
|
682
|
-
|
|
683
|
-
The
|
|
828
|
+
Yields:
|
|
829
|
+
The transcript group ID
|
|
684
830
|
"""
|
|
685
831
|
if not self._initialized:
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
raise RuntimeError("Tracer not initialized")
|
|
832
|
+
raise RuntimeError(
|
|
833
|
+
"Tracer is not initialized. Call initialize_tracing() before using transcript group context."
|
|
834
|
+
)
|
|
690
835
|
|
|
691
|
-
|
|
836
|
+
if transcript_group_id is None:
|
|
837
|
+
transcript_group_id = str(uuid.uuid4())
|
|
692
838
|
|
|
693
|
-
|
|
694
|
-
|
|
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
|
|
695
850
|
)
|
|
696
851
|
|
|
697
|
-
|
|
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}")
|
|
698
860
|
|
|
699
|
-
|
|
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)
|
|
865
|
+
|
|
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]:
|
|
700
875
|
"""
|
|
701
|
-
|
|
876
|
+
Async context manager for setting up a transcript group context.
|
|
702
877
|
|
|
703
878
|
Args:
|
|
704
|
-
|
|
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
|
|
705
887
|
"""
|
|
706
|
-
if
|
|
707
|
-
|
|
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)
|
|
708
931
|
|
|
709
932
|
|
|
710
|
-
# Global instance for easy access
|
|
711
933
|
_global_tracer: Optional[DocentTracer] = None
|
|
712
934
|
|
|
713
935
|
|
|
714
936
|
def initialize_tracing(
|
|
715
|
-
collection_name: str =
|
|
937
|
+
collection_name: str = DEFAULT_COLLECTION_NAME,
|
|
716
938
|
collection_id: Optional[str] = None,
|
|
717
939
|
endpoint: Union[str, List[str]] = DEFAULT_ENDPOINT,
|
|
718
940
|
headers: Optional[Dict[str, str]] = None,
|
|
@@ -720,7 +942,6 @@ def initialize_tracing(
|
|
|
720
942
|
enable_console_export: bool = False,
|
|
721
943
|
enable_otlp_export: bool = True,
|
|
722
944
|
disable_batch: bool = False,
|
|
723
|
-
span_postprocess_callback: Optional[Callable[[ReadableSpan], None]] = None,
|
|
724
945
|
) -> DocentTracer:
|
|
725
946
|
"""
|
|
726
947
|
Initialize the global Docent tracer.
|
|
@@ -737,7 +958,6 @@ def initialize_tracing(
|
|
|
737
958
|
enable_console_export: Whether to export spans to console
|
|
738
959
|
enable_otlp_export: Whether to export spans to OTLP endpoint
|
|
739
960
|
disable_batch: Whether to disable batch processing (use SimpleSpanProcessor)
|
|
740
|
-
span_postprocess_callback: Optional callback for post-processing spans
|
|
741
961
|
|
|
742
962
|
Returns:
|
|
743
963
|
The initialized Docent tracer
|
|
@@ -763,12 +983,8 @@ def initialize_tracing(
|
|
|
763
983
|
enable_console_export=enable_console_export,
|
|
764
984
|
enable_otlp_export=enable_otlp_export,
|
|
765
985
|
disable_batch=disable_batch,
|
|
766
|
-
span_postprocess_callback=span_postprocess_callback,
|
|
767
986
|
)
|
|
768
987
|
_global_tracer.initialize()
|
|
769
|
-
else:
|
|
770
|
-
# If already initialized, ensure it's properly set up
|
|
771
|
-
_global_tracer.initialize()
|
|
772
988
|
|
|
773
989
|
return _global_tracer
|
|
774
990
|
|
|
@@ -776,8 +992,7 @@ def initialize_tracing(
|
|
|
776
992
|
def get_tracer() -> DocentTracer:
|
|
777
993
|
"""Get the global Docent tracer."""
|
|
778
994
|
if _global_tracer is None:
|
|
779
|
-
|
|
780
|
-
return initialize_tracing()
|
|
995
|
+
raise RuntimeError("Docent tracer not initialized")
|
|
781
996
|
return _global_tracer
|
|
782
997
|
|
|
783
998
|
|
|
@@ -808,20 +1023,9 @@ def set_disabled(disabled: bool) -> None:
|
|
|
808
1023
|
_global_tracer.set_disabled(disabled)
|
|
809
1024
|
|
|
810
1025
|
|
|
811
|
-
def get_api_key() -> Optional[str]:
|
|
812
|
-
"""
|
|
813
|
-
Get the API key from environment variable.
|
|
814
|
-
|
|
815
|
-
Returns:
|
|
816
|
-
The API key from DOCENT_API_KEY environment variable, or None if not set
|
|
817
|
-
"""
|
|
818
|
-
return os.environ.get("DOCENT_API_KEY")
|
|
819
|
-
|
|
820
|
-
|
|
821
1026
|
def agent_run_score(name: str, score: float, attributes: Optional[Dict[str, Any]] = None) -> None:
|
|
822
1027
|
"""
|
|
823
|
-
|
|
824
|
-
Automatically works in both sync and async contexts.
|
|
1028
|
+
Send a score to the backend for the current agent run.
|
|
825
1029
|
|
|
826
1030
|
Args:
|
|
827
1031
|
name: Name of the score metric
|
|
@@ -829,21 +1033,16 @@ def agent_run_score(name: str, score: float, attributes: Optional[Dict[str, Any]
|
|
|
829
1033
|
attributes: Optional additional attributes for the score event
|
|
830
1034
|
"""
|
|
831
1035
|
try:
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
event_attributes.update(attributes)
|
|
841
|
-
|
|
842
|
-
current_span.add_event(name="agent_run_score", attributes=event_attributes)
|
|
843
|
-
else:
|
|
844
|
-
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)
|
|
845
1044
|
except Exception as e:
|
|
846
|
-
logger.error(f"Failed to
|
|
1045
|
+
logger.error(f"Failed to send score: {e}")
|
|
847
1046
|
|
|
848
1047
|
|
|
849
1048
|
def _flatten_dict(d: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
|
|
@@ -858,31 +1057,9 @@ def _flatten_dict(d: Dict[str, Any], prefix: str = "") -> Dict[str, Any]:
|
|
|
858
1057
|
return flattened
|
|
859
1058
|
|
|
860
1059
|
|
|
861
|
-
def _add_metadata_event_to_span(span: Any, metadata: Dict[str, Any]) -> None:
|
|
862
|
-
"""
|
|
863
|
-
Add metadata as an event to a span.
|
|
864
|
-
|
|
865
|
-
Args:
|
|
866
|
-
span: The span to add the event to
|
|
867
|
-
metadata: Dictionary of metadata (can be nested)
|
|
868
|
-
"""
|
|
869
|
-
if span and hasattr(span, "add_event"):
|
|
870
|
-
event_attributes: dict[str, Any] = {
|
|
871
|
-
"event.type": "metadata",
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
# Flatten nested metadata and add as event attributes
|
|
875
|
-
flattened_metadata = _flatten_dict(metadata)
|
|
876
|
-
for key, value in flattened_metadata.items():
|
|
877
|
-
event_attributes[f"metadata.{key}"] = value
|
|
878
|
-
span.add_event(name="agent_run_metadata", attributes=event_attributes)
|
|
879
|
-
|
|
880
|
-
|
|
881
1060
|
def agent_run_metadata(metadata: Dict[str, Any]) -> None:
|
|
882
1061
|
"""
|
|
883
|
-
|
|
884
|
-
Automatically works in both sync and async contexts.
|
|
885
|
-
Supports nested dictionaries by flattening them with dot notation.
|
|
1062
|
+
Send metadata directly to the backend for the current agent run.
|
|
886
1063
|
|
|
887
1064
|
Args:
|
|
888
1065
|
metadata: Dictionary of metadata to attach to the current span (can be nested)
|
|
@@ -892,28 +1069,49 @@ def agent_run_metadata(metadata: Dict[str, Any]) -> None:
|
|
|
892
1069
|
agent_run_metadata({"user": {"id": "123", "name": "John"}, "config": {"model": "gpt-4"}})
|
|
893
1070
|
"""
|
|
894
1071
|
try:
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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)
|
|
900
1079
|
except Exception as e:
|
|
901
|
-
logger.error(f"Failed to
|
|
1080
|
+
logger.error(f"Failed to send metadata: {e}")
|
|
902
1081
|
|
|
903
1082
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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:
|
|
907
1089
|
"""
|
|
908
|
-
|
|
909
|
-
|
|
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")
|
|
910
1102
|
"""
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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}")
|
|
917
1115
|
|
|
918
1116
|
|
|
919
1117
|
class AgentRunContext:
|
|
@@ -933,7 +1131,7 @@ class AgentRunContext:
|
|
|
933
1131
|
self._sync_context: Optional[Any] = None
|
|
934
1132
|
self._async_context: Optional[Any] = None
|
|
935
1133
|
|
|
936
|
-
def __enter__(self) ->
|
|
1134
|
+
def __enter__(self) -> tuple[str, str]:
|
|
937
1135
|
"""Sync context manager entry."""
|
|
938
1136
|
self._sync_context = get_tracer().agent_run_context(
|
|
939
1137
|
self.agent_run_id, self.transcript_id, metadata=self.metadata, **self.attributes
|
|
@@ -945,7 +1143,7 @@ class AgentRunContext:
|
|
|
945
1143
|
if self._sync_context:
|
|
946
1144
|
self._sync_context.__exit__(exc_type, exc_val, exc_tb)
|
|
947
1145
|
|
|
948
|
-
async def __aenter__(self) ->
|
|
1146
|
+
async def __aenter__(self) -> tuple[str, str]:
|
|
949
1147
|
"""Async context manager entry."""
|
|
950
1148
|
self._async_context = get_tracer().async_agent_run_context(
|
|
951
1149
|
self.agent_run_id, self.transcript_id, metadata=self.metadata, **self.attributes
|
|
@@ -963,13 +1161,13 @@ def agent_run(
|
|
|
963
1161
|
):
|
|
964
1162
|
"""
|
|
965
1163
|
Decorator to wrap a function in an agent_run_context (sync or async).
|
|
966
|
-
Injects
|
|
1164
|
+
Injects agent_run_id and transcript_id as function attributes.
|
|
967
1165
|
Optionally accepts metadata to attach to the agent run context.
|
|
968
1166
|
|
|
969
1167
|
Example:
|
|
970
1168
|
@agent_run
|
|
971
1169
|
def my_func(x, y):
|
|
972
|
-
print(my_func.docent.
|
|
1170
|
+
print(my_func.docent.agent_run_id, my_func.docent.transcript_id)
|
|
973
1171
|
|
|
974
1172
|
@agent_run(metadata={"user": "John", "model": "gpt-4"})
|
|
975
1173
|
def my_func_with_metadata(x, y):
|
|
@@ -987,11 +1185,7 @@ def agent_run(
|
|
|
987
1185
|
|
|
988
1186
|
@functools.wraps(f)
|
|
989
1187
|
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
990
|
-
async with AgentRunContext(metadata=metadata) as (
|
|
991
|
-
context,
|
|
992
|
-
agent_run_id,
|
|
993
|
-
transcript_id,
|
|
994
|
-
):
|
|
1188
|
+
async with AgentRunContext(metadata=metadata) as (agent_run_id, transcript_id):
|
|
995
1189
|
# Store docent data as function attributes
|
|
996
1190
|
setattr(
|
|
997
1191
|
async_wrapper,
|
|
@@ -1000,7 +1194,6 @@ def agent_run(
|
|
|
1000
1194
|
"DocentData",
|
|
1001
1195
|
(),
|
|
1002
1196
|
{
|
|
1003
|
-
"context": context,
|
|
1004
1197
|
"agent_run_id": agent_run_id,
|
|
1005
1198
|
"transcript_id": transcript_id,
|
|
1006
1199
|
},
|
|
@@ -1013,7 +1206,7 @@ def agent_run(
|
|
|
1013
1206
|
|
|
1014
1207
|
@functools.wraps(f)
|
|
1015
1208
|
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
1016
|
-
with AgentRunContext(metadata=metadata) as (
|
|
1209
|
+
with AgentRunContext(metadata=metadata) as (agent_run_id, transcript_id):
|
|
1017
1210
|
# Store docent data as function attributes
|
|
1018
1211
|
setattr(
|
|
1019
1212
|
sync_wrapper,
|
|
@@ -1022,7 +1215,6 @@ def agent_run(
|
|
|
1022
1215
|
"DocentData",
|
|
1023
1216
|
(),
|
|
1024
1217
|
{
|
|
1025
|
-
"context": context,
|
|
1026
1218
|
"agent_run_id": agent_run_id,
|
|
1027
1219
|
"transcript_id": transcript_id,
|
|
1028
1220
|
},
|
|
@@ -1058,15 +1250,371 @@ def agent_run_context(
|
|
|
1058
1250
|
|
|
1059
1251
|
Example:
|
|
1060
1252
|
# Sync usage
|
|
1061
|
-
with agent_run_context() as (
|
|
1253
|
+
with agent_run_context() as (agent_run_id, transcript_id):
|
|
1062
1254
|
pass
|
|
1063
1255
|
|
|
1064
1256
|
# Async usage
|
|
1065
|
-
async with agent_run_context() as (
|
|
1257
|
+
async with agent_run_context() as (agent_run_id, transcript_id):
|
|
1066
1258
|
pass
|
|
1067
1259
|
|
|
1068
1260
|
# With metadata
|
|
1069
|
-
with agent_run_context(metadata={"user": "John", "model": "gpt-4"}) as (
|
|
1261
|
+
with agent_run_context(metadata={"user": "John", "model": "gpt-4"}) as (agent_run_id, transcript_id):
|
|
1070
1262
|
pass
|
|
1071
1263
|
"""
|
|
1072
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
|
+
)
|