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/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
- logging.disable()
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 = "default-collection-name",
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[Any] = None
134
- self._root_span: Optional[Any] = None
135
- self._root_context: Optional[Any] = None
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[Any] = []
141
-
142
- # Context variables for agent_run_id and transcript_id (thread/async safe)
143
- self._collection_id_var: ContextVar[str] = contextvars.ContextVar("collection_id")
144
- self._agent_run_id_var: ContextVar[str] = contextvars.ContextVar("agent_run_id")
145
- self._transcript_id_var: ContextVar[str] = contextvars.ContextVar("transcript_id")
146
- self._attributes_var: ContextVar[dict[str, Any]] = contextvars.ContextVar("attributes")
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 atomic span order for a given transcript_id.
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: Any):
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(self, exporter: Any) -> Any:
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: Any = SimpleSpanProcessor(exporter)
218
+ simple_processor: SimpleSpanProcessor = SimpleSpanProcessor(exporter)
220
219
  return simple_processor
221
220
  else:
222
- batch_processor: Any = BatchSpanProcessor(exporter)
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 run_id and transcript_id
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: Any, parent_context: Any = None) -> None:
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
- # Handle agent_run_id
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
- # Handle transcript_id
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
- # Handle attributes
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: Any) -> None:
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) -> None:
286
- pass
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: Any = self._create_span_processor(exporter)
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: Any = ConsoleSpanExporter()
309
- console_processor: Any = self._create_span_processor(console_exporter)
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
- # Create an explicit end-of-trace span before ending the root span
383
- if self._tracer and self._root_span:
384
- end_span = self._tracer.start_span(
385
- "trace_end",
386
- context=self._root_context,
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._root_span = None
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[Any]:
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[Any]:
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[Any]:
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 attach as events
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 (context, agent_run_id, transcript_id)
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
- # Create a span with the agent run attributes
547
- span_attributes: dict[str, Any] = {
548
- "agent_run_id": agent_run_id,
549
- "transcript_id": transcript_id,
550
- **attributes,
551
- }
552
- with self._tracer.start_as_current_span(
553
- "agent_run_context", context=self._root_context, attributes=span_attributes
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[Any]:
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 attach as events
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 (context, agent_run_id, transcript_id)
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: Any = self._agent_run_id_var.set(agent_run_id)
600
- transcript_id_token: Any = self._transcript_id_var.set(transcript_id)
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
- # Create a span with the agent run attributes
605
- span_attributes: dict[str, Any] = {
606
- "agent_run_id": agent_run_id,
607
- "transcript_id": transcript_id,
608
- **attributes,
609
- }
610
- with self._tracer.start_as_current_span(
611
- "agent_run_context", context=self._root_context, attributes=span_attributes
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 start_transcript(
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: Optional[str] = None,
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
- **attributes: Any,
629
- ) -> tuple[Any, str, str]:
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
- Manually start a transcript span.
673
+ Context manager for setting up a transcript context.
632
674
 
633
675
  Args:
634
- agent_run_id: Optional agent run ID (auto-generated if not provided)
676
+ name: Optional transcript name
635
677
  transcript_id: Optional transcript ID (auto-generated if not provided)
636
- **attributes: Additional attributes to add to the span
678
+ description: Optional transcript description
679
+ metadata: Optional metadata to send to backend
680
+ transcript_group_id: Optional transcript group ID
637
681
 
638
- Returns:
639
- Tuple of (span, agent_run_id, transcript_id)
682
+ Yields:
683
+ The transcript ID
640
684
  """
641
685
  if not self._initialized:
642
- self.initialize()
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
- if self._tracer is None:
645
- raise RuntimeError("Tracer not initialized")
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
- span_attributes: dict[str, Any] = {
653
- "agent_run_id": agent_run_id,
654
- "transcript_id": transcript_id,
655
- **attributes,
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
- span: Any = self._tracer.start_span(
659
- "transcript_span", context=self._root_context, attributes=span_attributes
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
- return span, agent_run_id, transcript_id
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 stop_transcript(self, span: Any) -> None:
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
- Manually stop a transcript span.
782
+ Send transcript group data to the backend.
667
783
 
668
784
  Args:
669
- span: The span to stop
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
- if span and hasattr(span, "end"):
672
- span.end()
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
- def start_span(self, name: str, attributes: Optional[Dict[str, Any]] = None) -> Any:
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
- Manually start a span.
819
+ Context manager for setting up a transcript group context.
677
820
 
678
821
  Args:
679
- name: Name of the span
680
- attributes: Dictionary of attributes to add to the span
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
- Returns:
683
- The created span
828
+ Yields:
829
+ The transcript group ID
684
830
  """
685
831
  if not self._initialized:
686
- self.initialize()
687
-
688
- if self._tracer is None:
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
- span_attributes: dict[str, Any] = attributes or {}
836
+ if transcript_group_id is None:
837
+ transcript_group_id = str(uuid.uuid4())
692
838
 
693
- span: Any = self._tracer.start_span(
694
- name, context=self._root_context, attributes=span_attributes
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
- return span
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
- def stop_span(self, span: Any) -> None:
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
- Manually stop a span.
876
+ Async context manager for setting up a transcript group context.
702
877
 
703
878
  Args:
704
- span: The span to stop
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 span and hasattr(span, "end"):
707
- span.end()
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 = "default-service",
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
- # Auto-initialize with defaults if not already done
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
- Record a score event on the current span.
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
- current_span: Any = trace.get_current_span()
833
- if current_span and hasattr(current_span, "add_event"):
834
- event_attributes: dict[str, Any] = {
835
- "score.name": name,
836
- "score.value": score,
837
- "event.type": "score",
838
- }
839
- if attributes:
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 record score event: {e}")
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
- Record metadata as an event on the current span.
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
- current_span: Any = trace.get_current_span()
896
- if current_span:
897
- _add_metadata_event_to_span(current_span, metadata)
898
- else:
899
- logger.warning("No current span available for recording metadata")
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 record metadata event: {e}")
1080
+ logger.error(f"Failed to send metadata: {e}")
902
1081
 
903
1082
 
904
- # Unified functions that automatically detect context
905
- @asynccontextmanager
906
- async def span(name: str, attributes: Optional[Dict[str, Any]] = None) -> AsyncIterator[Any]:
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
- Automatically choose sync or async span based on context.
909
- Can be used with both 'with' and 'async with'.
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
- if _is_async_context() or _is_running_in_event_loop():
912
- async with get_tracer().async_span(name, attributes) as span:
913
- yield span
914
- else:
915
- with get_tracer().span(name, attributes) as span:
916
- yield span
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) -> Any:
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) -> Any:
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 context, agent_run_id, and transcript_id as function attributes.
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.context, my_func.docent.agent_run_id, my_func.docent.transcript_id)
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 (context, agent_run_id, transcript_id):
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 (context, agent_run_id, transcript_id):
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 (context, agent_run_id, transcript_id):
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 (context, agent_run_id, transcript_id):
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
+ )