docent-python 0.1.19a0__py3-none-any.whl → 0.1.27a0__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.

Files changed (38) hide show
  1. docent/_llm_util/__init__.py +0 -0
  2. docent/_llm_util/data_models/__init__.py +0 -0
  3. docent/_llm_util/data_models/exceptions.py +48 -0
  4. docent/_llm_util/data_models/llm_output.py +331 -0
  5. docent/_llm_util/llm_cache.py +193 -0
  6. docent/_llm_util/llm_svc.py +472 -0
  7. docent/_llm_util/model_registry.py +130 -0
  8. docent/_llm_util/providers/__init__.py +0 -0
  9. docent/_llm_util/providers/anthropic.py +537 -0
  10. docent/_llm_util/providers/common.py +41 -0
  11. docent/_llm_util/providers/google.py +530 -0
  12. docent/_llm_util/providers/openai.py +745 -0
  13. docent/_llm_util/providers/openrouter.py +375 -0
  14. docent/_llm_util/providers/preference_types.py +104 -0
  15. docent/_llm_util/providers/provider_registry.py +164 -0
  16. docent/data_models/__init__.py +2 -2
  17. docent/data_models/agent_run.py +1 -0
  18. docent/data_models/judge.py +7 -4
  19. docent/data_models/transcript.py +2 -0
  20. docent/data_models/util.py +170 -0
  21. docent/judges/__init__.py +23 -0
  22. docent/judges/analysis.py +77 -0
  23. docent/judges/impl.py +587 -0
  24. docent/judges/runner.py +129 -0
  25. docent/judges/stats.py +205 -0
  26. docent/judges/types.py +311 -0
  27. docent/judges/util/forgiving_json.py +108 -0
  28. docent/judges/util/meta_schema.json +86 -0
  29. docent/judges/util/meta_schema.py +29 -0
  30. docent/judges/util/parse_output.py +87 -0
  31. docent/judges/util/voting.py +139 -0
  32. docent/sdk/client.py +181 -44
  33. docent/trace.py +362 -44
  34. {docent_python-0.1.19a0.dist-info → docent_python-0.1.27a0.dist-info}/METADATA +11 -5
  35. docent_python-0.1.27a0.dist-info/RECORD +59 -0
  36. docent_python-0.1.19a0.dist-info/RECORD +0 -32
  37. {docent_python-0.1.19a0.dist-info → docent_python-0.1.27a0.dist-info}/WHEEL +0 -0
  38. {docent_python-0.1.19a0.dist-info → docent_python-0.1.27a0.dist-info}/licenses/LICENSE.md +0 -0
docent/trace.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import atexit
2
2
  import contextvars
3
3
  import itertools
4
+ import json
4
5
  import logging
5
6
  import os
6
7
  import sys
@@ -12,7 +13,19 @@ from contextvars import ContextVar, Token
12
13
  from datetime import datetime, timezone
13
14
  from enum import Enum
14
15
  from importlib.metadata import Distribution, distributions
15
- from typing import Any, AsyncIterator, Callable, Dict, Iterator, List, Optional, Set, Union
16
+ from typing import (
17
+ Any,
18
+ AsyncIterator,
19
+ Callable,
20
+ Dict,
21
+ Iterator,
22
+ List,
23
+ Mapping,
24
+ Optional,
25
+ Set,
26
+ Union,
27
+ cast,
28
+ )
16
29
 
17
30
  import requests
18
31
  from opentelemetry import trace
@@ -28,12 +41,23 @@ from opentelemetry.sdk.trace.export import (
28
41
  SimpleSpanProcessor,
29
42
  )
30
43
  from opentelemetry.trace import Span
44
+ from requests import Response
31
45
 
32
46
  logger = logging.getLogger(__name__)
33
47
 
34
48
  # Default configuration
35
49
  DEFAULT_ENDPOINT = "https://api.docent.transluce.org/rest/telemetry"
36
50
  DEFAULT_COLLECTION_NAME = "default-collection-name"
51
+ ERROR_DETAIL_MAX_CHARS = 500
52
+
53
+ # Sentinel values for when tracing is disabled
54
+ DISABLED_AGENT_RUN_ID = "disabled"
55
+ DISABLED_TRANSCRIPT_ID = "disabled"
56
+ DISABLED_TRANSCRIPT_GROUP_ID = "disabled"
57
+
58
+
59
+ class DocentTelemetryRequestError(RuntimeError):
60
+ """Raised when the Docent telemetry backend rejects a client request."""
37
61
 
38
62
 
39
63
  class Instruments(Enum):
@@ -43,6 +67,7 @@ class Instruments(Enum):
43
67
  ANTHROPIC = "anthropic"
44
68
  BEDROCK = "bedrock"
45
69
  LANGCHAIN = "langchain"
70
+ GOOGLE_GENERATIVEAI = "google_generativeai"
46
71
 
47
72
 
48
73
  class DocentTracer:
@@ -128,6 +153,8 @@ class DocentTracer:
128
153
  lambda: itertools.count(0)
129
154
  )
130
155
  self._transcript_counter_lock = threading.Lock()
156
+ self._transcript_group_states: dict[str, dict[str, Optional[str]]] = {}
157
+ self._transcript_group_state_lock = threading.Lock()
131
158
  self._flush_lock = threading.Lock()
132
159
 
133
160
  def get_current_agent_run_id(self) -> Optional[str]:
@@ -226,7 +253,7 @@ class DocentTracer:
226
253
  try:
227
254
 
228
255
  # Check for OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT environment variable
229
- default_attribute_limit = 1024
256
+ default_attribute_limit = 1024 * 16
230
257
  env_value = os.environ.get("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "0")
231
258
  env_limit = int(env_value) if env_value.isdigit() else 0
232
259
  attribute_limit = max(env_limit, default_attribute_limit)
@@ -392,6 +419,23 @@ class DocentTracer:
392
419
  except Exception as e:
393
420
  logger.warning(f"Failed to instrument LangChain: {e}")
394
421
 
422
+ # Instrument Google Generative AI with our isolated tracer provider
423
+ if Instruments.GOOGLE_GENERATIVEAI in enabled_instruments:
424
+ try:
425
+ if is_package_installed("google-generativeai") or is_package_installed(
426
+ "google-genai"
427
+ ):
428
+ from opentelemetry.instrumentation.google_generativeai import (
429
+ GoogleGenerativeAiInstrumentor,
430
+ )
431
+
432
+ GoogleGenerativeAiInstrumentor().instrument(
433
+ tracer_provider=self._tracer_provider
434
+ )
435
+ logger.info("Instrumented Google Generative AI")
436
+ except Exception as e:
437
+ logger.warning(f"Failed to instrument Google Generative AI: {e}")
438
+
395
439
  # Register cleanup handlers
396
440
  self._register_cleanup()
397
441
 
@@ -469,6 +513,24 @@ class DocentTracer:
469
513
  """Verify if the manager is properly initialized."""
470
514
  return self._initialized
471
515
 
516
+ def get_disabled_agent_run_id(self, agent_run_id: Optional[str]) -> str:
517
+ """Return sentinel value for agent run ID when tracing is disabled."""
518
+ if agent_run_id is None:
519
+ return DISABLED_AGENT_RUN_ID
520
+ return agent_run_id
521
+
522
+ def get_disabled_transcript_id(self, transcript_id: Optional[str]) -> str:
523
+ """Return sentinel value for transcript ID when tracing is disabled."""
524
+ if transcript_id is None:
525
+ return DISABLED_TRANSCRIPT_ID
526
+ return transcript_id
527
+
528
+ def get_disabled_transcript_group_id(self, transcript_group_id: Optional[str]) -> str:
529
+ """Return sentinel value for transcript group ID when tracing is disabled."""
530
+ if transcript_group_id is None:
531
+ return DISABLED_TRANSCRIPT_GROUP_ID
532
+ return transcript_group_id
533
+
472
534
  @contextmanager
473
535
  def agent_run_context(
474
536
  self,
@@ -490,11 +552,8 @@ class DocentTracer:
490
552
  Tuple of (agent_run_id, transcript_id)
491
553
  """
492
554
  if self._disabled:
493
- # Return dummy IDs when tracing is disabled
494
- if agent_run_id is None:
495
- agent_run_id = str(uuid.uuid4())
496
- if transcript_id is None:
497
- transcript_id = str(uuid.uuid4())
555
+ agent_run_id = self.get_disabled_agent_run_id(agent_run_id)
556
+ transcript_id = self.get_disabled_transcript_id(transcript_id)
498
557
  yield agent_run_id, transcript_id
499
558
  return
500
559
 
@@ -517,7 +576,7 @@ class DocentTracer:
517
576
  try:
518
577
  self.send_agent_run_metadata(agent_run_id, metadata)
519
578
  except Exception as e:
520
- logger.warning(f"Failed sending agent run metadata: {e}")
579
+ logger.error(f"Failed sending agent run metadata: {e}")
521
580
 
522
581
  yield agent_run_id, transcript_id
523
582
  finally:
@@ -547,11 +606,8 @@ class DocentTracer:
547
606
  Tuple of (agent_run_id, transcript_id)
548
607
  """
549
608
  if self._disabled:
550
- # Return dummy IDs when tracing is disabled
551
- if agent_run_id is None:
552
- agent_run_id = str(uuid.uuid4())
553
- if transcript_id is None:
554
- transcript_id = str(uuid.uuid4())
609
+ agent_run_id = self.get_disabled_agent_run_id(agent_run_id)
610
+ transcript_id = self.get_disabled_transcript_id(transcript_id)
555
611
  yield agent_run_id, transcript_id
556
612
  return
557
613
 
@@ -597,15 +653,184 @@ class DocentTracer:
597
653
 
598
654
  return headers
599
655
 
656
+ def _ensure_json_serializable_metadata(self, metadata: Dict[str, Any], context: str) -> None:
657
+ """
658
+ Validate that metadata can be serialized to JSON before sending it to the backend.
659
+ """
660
+ try:
661
+ json.dumps(metadata)
662
+ except (TypeError, ValueError) as exc:
663
+ raise TypeError(f"{context} metadata must be JSON serializable") from exc
664
+ offending_path = self._find_null_character_path(metadata)
665
+ if offending_path is not None:
666
+ raise ValueError(
667
+ f"{context} metadata cannot contain null characters (found at {offending_path}). "
668
+ "Remove or replace '\\u0000' before calling Docent tracing APIs."
669
+ )
670
+
600
671
  def _post_json(self, path: str, data: Dict[str, Any]) -> None:
672
+ self._post_json_sync(path, data)
673
+
674
+ def _post_json_sync(self, path: str, data: Dict[str, Any]) -> None:
601
675
  if not self._api_endpoint_base:
602
676
  raise RuntimeError("API endpoint base is not configured")
603
677
  url = f"{self._api_endpoint_base}{path}"
604
678
  try:
605
679
  resp = requests.post(url, json=data, headers=self._api_headers(), timeout=(10, 60))
606
680
  resp.raise_for_status()
607
- except requests.exceptions.RequestException as e:
608
- logger.error(f"Failed POST {url}: {e}")
681
+ except requests.exceptions.RequestException as exc:
682
+ message = self._format_request_exception(url, exc)
683
+ raise DocentTelemetryRequestError(message) from exc
684
+
685
+ def _format_request_exception(self, url: str, exc: requests.exceptions.RequestException) -> str:
686
+ response: Optional[Response] = getattr(exc, "response", None)
687
+ message_parts: List[str] = [f"Failed POST {url}"]
688
+ suggestion: Optional[str]
689
+
690
+ if response is not None:
691
+ status_phrase = f"HTTP {response.status_code}"
692
+ if response.reason:
693
+ status_phrase = f"{status_phrase} {response.reason}"
694
+ message_parts.append(f"({status_phrase})")
695
+
696
+ detail = self._extract_response_detail(response)
697
+ if detail:
698
+ message_parts.append(f"- Backend detail: {detail}")
699
+
700
+ request_id = response.headers.get("x-request-id")
701
+ if request_id:
702
+ message_parts.append(f"(request-id: {request_id})")
703
+
704
+ suggestion = self._suggest_fix_for_status(response.status_code)
705
+ else:
706
+ message_parts.append(f"- {exc}")
707
+ suggestion = self._suggest_fix_for_status(None)
708
+
709
+ if suggestion:
710
+ message_parts.append(suggestion)
711
+
712
+ return " ".join(part for part in message_parts if part)
713
+
714
+ def _extract_response_detail(self, response: Response) -> Optional[str]:
715
+ try:
716
+ body = response.json()
717
+ except ValueError:
718
+ text = response.text.strip()
719
+ if not text:
720
+ return None
721
+ normalized = " ".join(text.split())
722
+ return self._truncate_error_message(normalized)
723
+
724
+ if isinstance(body, dict):
725
+ typed_body = cast(Dict[str, Any], body)
726
+ structured_message = self._structured_detail_message(typed_body)
727
+ if structured_message:
728
+ return self._truncate_error_message(structured_message)
729
+ return self._truncate_error_message(self._normalize_error_value(typed_body))
730
+
731
+ return self._truncate_error_message(self._normalize_error_value(body))
732
+
733
+ def _structured_detail_message(self, data: Dict[str, Any]) -> Optional[str]:
734
+ for key in ("detail", "message", "error"):
735
+ if key in data:
736
+ structured_value = self._structured_detail_value(data[key])
737
+ if structured_value:
738
+ return structured_value
739
+ return self._structured_detail_value(data)
740
+
741
+ def _structured_detail_value(self, value: Any) -> Optional[str]:
742
+ if isinstance(value, Mapping):
743
+ mapping_value = cast(Mapping[str, Any], value)
744
+ message = mapping_value.get("message")
745
+ hint = mapping_value.get("hint")
746
+ error_code = mapping_value.get("error_code")
747
+ request_id = mapping_value.get("request_id")
748
+ fallback_detail = mapping_value.get("detail")
749
+
750
+ parts: List[str] = []
751
+ if isinstance(message, str) and message.strip():
752
+ parts.append(message.strip())
753
+ elif isinstance(fallback_detail, str) and fallback_detail.strip():
754
+ parts.append(fallback_detail.strip())
755
+
756
+ if isinstance(hint, str) and hint.strip():
757
+ parts.append(f"(hint: {hint.strip()})")
758
+ if isinstance(error_code, str) and error_code.strip():
759
+ parts.append(f"[code: {error_code.strip()}]")
760
+ if isinstance(request_id, str) and request_id.strip():
761
+ parts.append(f"(request-id: {request_id.strip()})")
762
+
763
+ return " ".join(parts) if parts else None
764
+
765
+ if isinstance(value, str) and value.strip():
766
+ return value.strip()
767
+
768
+ return None
769
+
770
+ def _normalize_error_value(self, value: Any) -> str:
771
+ if isinstance(value, str):
772
+ return " ".join(value.split())
773
+
774
+ try:
775
+ serialized = json.dumps(value)
776
+ except (TypeError, ValueError):
777
+ serialized = str(value)
778
+
779
+ return " ".join(serialized.split())
780
+
781
+ def _truncate_error_message(self, message: str) -> str:
782
+ message = message.strip()
783
+ if len(message) <= ERROR_DETAIL_MAX_CHARS:
784
+ return message
785
+ return f"{message[:ERROR_DETAIL_MAX_CHARS]}..."
786
+
787
+ def _suggest_fix_for_status(self, status_code: Optional[int]) -> Optional[str]:
788
+ if status_code in (401, 403):
789
+ return (
790
+ "Verify that the Authorization header or DOCENT_API_KEY grants write access to the "
791
+ "target collection."
792
+ )
793
+ if status_code == 404:
794
+ return (
795
+ "Ensure the tracing endpoint passed to initialize_tracing matches the Docent server's "
796
+ "/rest/telemetry route."
797
+ )
798
+ if status_code in (400, 422):
799
+ return (
800
+ "Confirm the payload includes collection_id, agent_run_id, metadata, and timestamp in "
801
+ "the expected format."
802
+ )
803
+ if status_code and status_code >= 500:
804
+ return "Inspect the Docent backend logs for the referenced request."
805
+ if status_code is None:
806
+ return "Confirm the Docent telemetry endpoint is reachable from this process."
807
+ return None
808
+
809
+ def _find_null_character_path(self, value: Any, path: str = "") -> Optional[str]:
810
+ """Backend rejects NUL bytes, so detect them before we send metadata to the backend."""
811
+ return None
812
+ if isinstance(value, str):
813
+ if "\x00" in value or "\\u0000" in value or "\\x00" in value:
814
+ return path or "<root>"
815
+ return None
816
+
817
+ if isinstance(value, dict):
818
+ for key, item in value.items():
819
+ next_path = f"{path}.{key}" if path else str(key)
820
+ result = self._find_null_character_path(item, next_path)
821
+ if result:
822
+ return result
823
+ return None
824
+
825
+ if isinstance(value, (list, tuple)):
826
+ for index, item in enumerate(value):
827
+ next_path = f"{path}[{index}]" if path else f"[{index}]"
828
+ result = self._find_null_character_path(item, next_path)
829
+ if result:
830
+ return result
831
+ return None
832
+
833
+ return None
609
834
 
610
835
  def send_agent_run_score(
611
836
  self,
@@ -642,6 +867,8 @@ class DocentTracer:
642
867
  if self._disabled:
643
868
  return
644
869
 
870
+ self._ensure_json_serializable_metadata(metadata, "Agent run")
871
+
645
872
  collection_id = self.collection_id
646
873
  payload: Dict[str, Any] = {
647
874
  "collection_id": collection_id,
@@ -687,6 +914,7 @@ class DocentTracer:
687
914
  if transcript_group_id is not None:
688
915
  payload["transcript_group_id"] = transcript_group_id
689
916
  if metadata is not None:
917
+ self._ensure_json_serializable_metadata(metadata, "Transcript")
690
918
  payload["metadata"] = metadata
691
919
 
692
920
  self._post_json("/v1/transcript-metadata", payload)
@@ -738,9 +966,7 @@ class DocentTracer:
738
966
  The transcript ID
739
967
  """
740
968
  if self._disabled:
741
- # Return dummy ID when tracing is disabled
742
- if transcript_id is None:
743
- transcript_id = str(uuid.uuid4())
969
+ transcript_id = self.get_disabled_transcript_id(transcript_id)
744
970
  yield transcript_id
745
971
  return
746
972
 
@@ -770,7 +996,7 @@ class DocentTracer:
770
996
  transcript_id, name, description, transcript_group_id, metadata
771
997
  )
772
998
  except Exception as e:
773
- logger.warning(f"Failed sending transcript data: {e}")
999
+ logger.error(f"Failed sending transcript data: {e}")
774
1000
 
775
1001
  yield transcript_id
776
1002
  finally:
@@ -800,9 +1026,7 @@ class DocentTracer:
800
1026
  The transcript ID
801
1027
  """
802
1028
  if self._disabled:
803
- # Return dummy ID when tracing is disabled
804
- if transcript_id is None:
805
- transcript_id = str(uuid.uuid4())
1029
+ transcript_id = self.get_disabled_transcript_id(transcript_id)
806
1030
  yield transcript_id
807
1031
  return
808
1032
 
@@ -832,7 +1056,7 @@ class DocentTracer:
832
1056
  transcript_id, name, description, transcript_group_id, metadata
833
1057
  )
834
1058
  except Exception as e:
835
- logger.warning(f"Failed sending transcript data: {e}")
1059
+ logger.error(f"Failed sending transcript data: {e}")
836
1060
 
837
1061
  yield transcript_id
838
1062
  finally:
@@ -870,6 +1094,27 @@ class DocentTracer:
870
1094
  )
871
1095
  return
872
1096
 
1097
+ with self._transcript_group_state_lock:
1098
+ state: dict[str, Optional[str]] = self._transcript_group_states.setdefault(
1099
+ transcript_group_id, {}
1100
+ )
1101
+ final_name: Optional[str] = name if name is not None else state.get("name")
1102
+ final_description: Optional[str] = (
1103
+ description if description is not None else state.get("description")
1104
+ )
1105
+ final_parent_transcript_group_id: Optional[str] = (
1106
+ parent_transcript_group_id
1107
+ if parent_transcript_group_id is not None
1108
+ else state.get("parent_transcript_group_id")
1109
+ )
1110
+
1111
+ if final_name is not None:
1112
+ state["name"] = final_name
1113
+ if final_description is not None:
1114
+ state["description"] = final_description
1115
+ if final_parent_transcript_group_id is not None:
1116
+ state["parent_transcript_group_id"] = final_parent_transcript_group_id
1117
+
873
1118
  payload: Dict[str, Any] = {
874
1119
  "collection_id": collection_id,
875
1120
  "transcript_group_id": transcript_group_id,
@@ -877,13 +1122,14 @@ class DocentTracer:
877
1122
  "timestamp": datetime.now(timezone.utc).isoformat(),
878
1123
  }
879
1124
 
880
- if name is not None:
881
- payload["name"] = name
882
- if description is not None:
883
- payload["description"] = description
884
- if parent_transcript_group_id is not None:
885
- payload["parent_transcript_group_id"] = parent_transcript_group_id
1125
+ if final_name is not None:
1126
+ payload["name"] = final_name
1127
+ if final_description is not None:
1128
+ payload["description"] = final_description
1129
+ if final_parent_transcript_group_id is not None:
1130
+ payload["parent_transcript_group_id"] = final_parent_transcript_group_id
886
1131
  if metadata is not None:
1132
+ self._ensure_json_serializable_metadata(metadata, "Transcript group")
887
1133
  payload["metadata"] = metadata
888
1134
 
889
1135
  self._post_json("/v1/transcript-group-metadata", payload)
@@ -911,9 +1157,7 @@ class DocentTracer:
911
1157
  The transcript group ID
912
1158
  """
913
1159
  if self._disabled:
914
- # Return dummy ID when tracing is disabled
915
- if transcript_group_id is None:
916
- transcript_group_id = str(uuid.uuid4())
1160
+ transcript_group_id = self.get_disabled_transcript_group_id(transcript_group_id)
917
1161
  yield transcript_group_id
918
1162
  return
919
1163
 
@@ -945,7 +1189,7 @@ class DocentTracer:
945
1189
  transcript_group_id, name, description, parent_transcript_group_id, metadata
946
1190
  )
947
1191
  except Exception as e:
948
- logger.warning(f"Failed sending transcript group data: {e}")
1192
+ logger.error(f"Failed sending transcript group data: {e}")
949
1193
 
950
1194
  yield transcript_group_id
951
1195
  finally:
@@ -975,9 +1219,7 @@ class DocentTracer:
975
1219
  The transcript group ID
976
1220
  """
977
1221
  if self._disabled:
978
- # Return dummy ID when tracing is disabled
979
- if transcript_group_id is None:
980
- transcript_group_id = str(uuid.uuid4())
1222
+ transcript_group_id = self.get_disabled_transcript_group_id(transcript_group_id)
981
1223
  yield transcript_group_id
982
1224
  return
983
1225
 
@@ -1009,7 +1251,7 @@ class DocentTracer:
1009
1251
  transcript_group_id, name, description, parent_transcript_group_id, metadata
1010
1252
  )
1011
1253
  except Exception as e:
1012
- logger.warning(f"Failed sending transcript group data: {e}")
1254
+ logger.error(f"Failed sending transcript group data: {e}")
1013
1255
 
1014
1256
  yield transcript_group_id
1015
1257
  finally:
@@ -1213,28 +1455,33 @@ def agent_run_metadata(metadata: Dict[str, Any]) -> None:
1213
1455
 
1214
1456
  tracer.send_agent_run_metadata(agent_run_id, metadata)
1215
1457
  except Exception as e:
1216
- logger.error(f"Failed to send metadata: {e}")
1458
+ logger.error(f"Failed to send agent run metadata: {e}")
1217
1459
 
1218
1460
 
1219
1461
  def transcript_metadata(
1462
+ metadata: Dict[str, Any],
1463
+ *,
1220
1464
  name: Optional[str] = None,
1221
1465
  description: Optional[str] = None,
1222
1466
  transcript_group_id: Optional[str] = None,
1223
- metadata: Optional[Dict[str, Any]] = None,
1224
1467
  ) -> None:
1225
1468
  """
1226
1469
  Send transcript metadata directly to the backend for the current transcript.
1227
1470
 
1228
1471
  Args:
1472
+ metadata: Dictionary of metadata to attach to the current transcript (required)
1229
1473
  name: Optional transcript name
1230
1474
  description: Optional transcript description
1231
- parent_transcript_id: Optional parent transcript ID
1232
- metadata: Optional metadata to send
1475
+ transcript_group_id: Optional transcript group ID to associate with
1233
1476
 
1234
1477
  Example:
1235
- transcript_metadata(name="data_processing", description="Process user data")
1236
- transcript_metadata(metadata={"user": "John", "model": "gpt-4"})
1237
- transcript_metadata(name="validation", parent_transcript_id="parent-123")
1478
+ transcript_metadata({"user": "John", "model": "gpt-4"})
1479
+ transcript_metadata({"env": "prod"}, name="data_processing")
1480
+ transcript_metadata(
1481
+ {"team": "search"},
1482
+ name="validation",
1483
+ transcript_group_id="group-123",
1484
+ )
1238
1485
  """
1239
1486
  try:
1240
1487
  tracer = get_tracer()
@@ -1252,6 +1499,47 @@ def transcript_metadata(
1252
1499
  logger.error(f"Failed to send transcript metadata: {e}")
1253
1500
 
1254
1501
 
1502
+ def transcript_group_metadata(
1503
+ metadata: Dict[str, Any],
1504
+ *,
1505
+ name: Optional[str] = None,
1506
+ description: Optional[str] = None,
1507
+ parent_transcript_group_id: Optional[str] = None,
1508
+ ) -> None:
1509
+ """
1510
+ Send transcript group metadata directly to the backend for the current transcript group.
1511
+
1512
+ Args:
1513
+ metadata: Dictionary of metadata to attach to the current transcript group (required)
1514
+ name: Optional transcript group name
1515
+ description: Optional transcript group description
1516
+ parent_transcript_group_id: Optional parent transcript group ID
1517
+
1518
+ Example:
1519
+ transcript_group_metadata({"team": "search", "env": "prod"})
1520
+ transcript_group_metadata({"env": "prod"}, name="pipeline")
1521
+ transcript_group_metadata(
1522
+ {"team": "search"},
1523
+ name="pipeline",
1524
+ parent_transcript_group_id="root-group",
1525
+ )
1526
+ """
1527
+ try:
1528
+ tracer = get_tracer()
1529
+ if tracer.is_disabled():
1530
+ return
1531
+ transcript_group_id = tracer.get_current_transcript_group_id()
1532
+ if not transcript_group_id:
1533
+ logger.warning("No active transcript group context. Metadata will not be sent.")
1534
+ return
1535
+
1536
+ tracer.send_transcript_group_metadata(
1537
+ transcript_group_id, name, description, parent_transcript_group_id, metadata
1538
+ )
1539
+ except Exception as e:
1540
+ logger.error(f"Failed to send transcript group metadata: {e}")
1541
+
1542
+
1255
1543
  class AgentRunContext:
1256
1544
  """Context manager that works in both sync and async contexts."""
1257
1545
 
@@ -1271,6 +1559,11 @@ class AgentRunContext:
1271
1559
 
1272
1560
  def __enter__(self) -> tuple[str, str]:
1273
1561
  """Sync context manager entry."""
1562
+ if is_disabled():
1563
+ tracer = get_tracer()
1564
+ self.agent_run_id = tracer.get_disabled_agent_run_id(self.agent_run_id)
1565
+ self.transcript_id = tracer.get_disabled_transcript_id(self.transcript_id)
1566
+ return self.agent_run_id, self.transcript_id
1274
1567
  self._sync_context = get_tracer().agent_run_context(
1275
1568
  self.agent_run_id, self.transcript_id, metadata=self.metadata, **self.attributes
1276
1569
  )
@@ -1283,6 +1576,11 @@ class AgentRunContext:
1283
1576
 
1284
1577
  async def __aenter__(self) -> tuple[str, str]:
1285
1578
  """Async context manager entry."""
1579
+ if is_disabled():
1580
+ tracer = get_tracer()
1581
+ self.agent_run_id = tracer.get_disabled_agent_run_id(self.agent_run_id)
1582
+ self.transcript_id = tracer.get_disabled_transcript_id(self.transcript_id)
1583
+ return self.agent_run_id, self.transcript_id
1286
1584
  self._async_context = get_tracer().async_agent_run_context(
1287
1585
  self.agent_run_id, self.transcript_id, metadata=self.metadata, **self.attributes
1288
1586
  )
@@ -1423,6 +1721,10 @@ class TranscriptContext:
1423
1721
 
1424
1722
  def __enter__(self) -> str:
1425
1723
  """Sync context manager entry."""
1724
+ if is_disabled():
1725
+ tracer = get_tracer()
1726
+ self.transcript_id = tracer.get_disabled_transcript_id(self.transcript_id)
1727
+ return self.transcript_id
1426
1728
  self._sync_context = get_tracer().transcript_context(
1427
1729
  name=self.name,
1428
1730
  transcript_id=self.transcript_id,
@@ -1439,6 +1741,10 @@ class TranscriptContext:
1439
1741
 
1440
1742
  async def __aenter__(self) -> str:
1441
1743
  """Async context manager entry."""
1744
+ if is_disabled():
1745
+ tracer = get_tracer()
1746
+ self.transcript_id = tracer.get_disabled_transcript_id(self.transcript_id)
1747
+ return self.transcript_id
1442
1748
  self._async_context = get_tracer().async_transcript_context(
1443
1749
  name=self.name,
1444
1750
  transcript_id=self.transcript_id,
@@ -1600,6 +1906,12 @@ class TranscriptGroupContext:
1600
1906
 
1601
1907
  def __enter__(self) -> str:
1602
1908
  """Sync context manager entry."""
1909
+ if is_disabled():
1910
+ tracer = get_tracer()
1911
+ self.transcript_group_id = tracer.get_disabled_transcript_group_id(
1912
+ self.transcript_group_id
1913
+ )
1914
+ return self.transcript_group_id
1603
1915
  self._sync_context = get_tracer().transcript_group_context(
1604
1916
  name=self.name,
1605
1917
  transcript_group_id=self.transcript_group_id,
@@ -1616,6 +1928,12 @@ class TranscriptGroupContext:
1616
1928
 
1617
1929
  async def __aenter__(self) -> str:
1618
1930
  """Async context manager entry."""
1931
+ if is_disabled():
1932
+ tracer = get_tracer()
1933
+ self.transcript_group_id = tracer.get_disabled_transcript_group_id(
1934
+ self.transcript_group_id
1935
+ )
1936
+ return self.transcript_group_id
1619
1937
  self._async_context = get_tracer().async_transcript_group_context(
1620
1938
  name=self.name,
1621
1939
  transcript_group_id=self.transcript_group_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docent-python
3
- Version: 0.1.19a0
3
+ Version: 0.1.27a0
4
4
  Summary: Docent SDK
5
5
  Project-URL: Homepage, https://github.com/TransluceAI/docent
6
6
  Project-URL: Issues, https://github.com/TransluceAI/docent/issues
@@ -9,17 +9,23 @@ Author-email: Transluce <info@transluce.org>
9
9
  License-Expression: Apache-2.0
10
10
  License-File: LICENSE.md
11
11
  Requires-Python: >=3.11
12
+ Requires-Dist: anthropic>=0.47.0
12
13
  Requires-Dist: backoff>=2.2.1
14
+ Requires-Dist: google-genai>=1.16.1
13
15
  Requires-Dist: inspect-ai>=0.3.132
16
+ Requires-Dist: jsonschema>=4.24.0
17
+ Requires-Dist: openai>=1.68.0
14
18
  Requires-Dist: opentelemetry-api>=1.34.1
15
19
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.34.1
16
20
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.34.1
17
- Requires-Dist: opentelemetry-instrumentation-anthropic>=0.44.1
18
- Requires-Dist: opentelemetry-instrumentation-bedrock>=0.44.1
19
- Requires-Dist: opentelemetry-instrumentation-langchain>=0.44.1
20
- Requires-Dist: opentelemetry-instrumentation-openai>=0.44.1
21
+ Requires-Dist: opentelemetry-instrumentation-anthropic>=0.40.14
22
+ Requires-Dist: opentelemetry-instrumentation-bedrock>=0.40.14
23
+ Requires-Dist: opentelemetry-instrumentation-google-generativeai>=0.40.14
24
+ Requires-Dist: opentelemetry-instrumentation-langchain>=0.40.14
25
+ Requires-Dist: opentelemetry-instrumentation-openai>=0.40.14
21
26
  Requires-Dist: opentelemetry-instrumentation-threading>=0.55b1
22
27
  Requires-Dist: opentelemetry-sdk>=1.34.1
28
+ Requires-Dist: orjson>=3.11.3
23
29
  Requires-Dist: pydantic>=2.11.7
24
30
  Requires-Dist: pyyaml>=6.0.2
25
31
  Requires-Dist: tiktoken>=0.7.0