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.
- docent/_llm_util/__init__.py +0 -0
- docent/_llm_util/data_models/__init__.py +0 -0
- docent/_llm_util/data_models/exceptions.py +48 -0
- docent/_llm_util/data_models/llm_output.py +331 -0
- docent/_llm_util/llm_cache.py +193 -0
- docent/_llm_util/llm_svc.py +472 -0
- docent/_llm_util/model_registry.py +130 -0
- docent/_llm_util/providers/__init__.py +0 -0
- docent/_llm_util/providers/anthropic.py +537 -0
- docent/_llm_util/providers/common.py +41 -0
- docent/_llm_util/providers/google.py +530 -0
- docent/_llm_util/providers/openai.py +745 -0
- docent/_llm_util/providers/openrouter.py +375 -0
- docent/_llm_util/providers/preference_types.py +104 -0
- docent/_llm_util/providers/provider_registry.py +164 -0
- docent/data_models/__init__.py +2 -2
- docent/data_models/agent_run.py +1 -0
- docent/data_models/judge.py +7 -4
- docent/data_models/transcript.py +2 -0
- docent/data_models/util.py +170 -0
- docent/judges/__init__.py +23 -0
- docent/judges/analysis.py +77 -0
- docent/judges/impl.py +587 -0
- docent/judges/runner.py +129 -0
- docent/judges/stats.py +205 -0
- docent/judges/types.py +311 -0
- docent/judges/util/forgiving_json.py +108 -0
- docent/judges/util/meta_schema.json +86 -0
- docent/judges/util/meta_schema.py +29 -0
- docent/judges/util/parse_output.py +87 -0
- docent/judges/util/voting.py +139 -0
- docent/sdk/client.py +181 -44
- docent/trace.py +362 -44
- {docent_python-0.1.19a0.dist-info → docent_python-0.1.27a0.dist-info}/METADATA +11 -5
- docent_python-0.1.27a0.dist-info/RECORD +59 -0
- docent_python-0.1.19a0.dist-info/RECORD +0 -32
- {docent_python-0.1.19a0.dist-info → docent_python-0.1.27a0.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
-
|
|
494
|
-
|
|
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.
|
|
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
|
-
|
|
551
|
-
|
|
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
|
|
608
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
881
|
-
payload["name"] =
|
|
882
|
-
if
|
|
883
|
-
payload["description"] =
|
|
884
|
-
if
|
|
885
|
-
payload["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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
1236
|
-
transcript_metadata(
|
|
1237
|
-
transcript_metadata(
|
|
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.
|
|
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.
|
|
18
|
-
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.
|
|
19
|
-
Requires-Dist: opentelemetry-instrumentation-
|
|
20
|
-
Requires-Dist: opentelemetry-instrumentation-
|
|
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
|