docent-python 0.1.25a0__py3-none-any.whl → 0.1.26a0__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.
@@ -8,6 +8,7 @@ from pydantic import BaseModel
8
8
  from docent._llm_util.data_models.exceptions import (
9
9
  LLM_ERROR_TYPES,
10
10
  CompletionTooLongException,
11
+ ContextWindowException,
11
12
  LLMException,
12
13
  )
13
14
  from docent._log_util import get_logger
@@ -148,6 +149,13 @@ class LLMOutput:
148
149
  def from_dict(cls, data: dict[str, Any]) -> "LLMOutput":
149
150
  error_type_map = {e.error_type_id: e for e in LLM_ERROR_TYPES}
150
151
  errors = data.get("errors", [])
152
+ error_types_to_not_log: list[str] = [
153
+ CompletionTooLongException.error_type_id,
154
+ ContextWindowException.error_type_id,
155
+ ]
156
+ errors_to_log = [e for e in errors if e not in error_types_to_not_log]
157
+ if errors_to_log:
158
+ logger.error(f"Loading LLM output with errors: {errors}")
151
159
  errors = [error_type_map.get(e, LLMException)() for e in errors]
152
160
 
153
161
  completions = data.get("completions", [])
@@ -176,7 +176,7 @@ async def _parallelize_calls(
176
176
  )
177
177
  if retry_count >= MAX_VALIDATION_ATTEMPTS:
178
178
  logger.error(
179
- f"Validation failed for {model_name} after {MAX_VALIDATION_ATTEMPTS} attempts: {e}"
179
+ f"Validation failed for {model_name} after {retry_count} attempts. Original output: {e.failed_output}"
180
180
  )
181
181
  result = LLMOutput(
182
182
  model=model_name,
@@ -195,8 +195,8 @@ async def _parallelize_calls(
195
195
  break
196
196
  except Exception as e:
197
197
  if not isinstance(e, LLMException):
198
- logger.warning(
199
- f"LLM call raised an exception that is not an LLMException: {e}"
198
+ logger.error(
199
+ f"LLM call raised an exception that is not an LLMException: {e}. Failure traceback:\n{traceback.format_exc()}"
200
200
  )
201
201
  llm_exception = LLMException(e)
202
202
  llm_exception.__cause__ = e
docent/trace.py CHANGED
@@ -1,12 +1,15 @@
1
+ import asyncio
1
2
  import atexit
2
3
  import contextvars
3
4
  import itertools
5
+ import json
4
6
  import logging
5
7
  import os
6
8
  import sys
7
9
  import threading
8
10
  import uuid
9
11
  from collections import defaultdict
12
+ from concurrent.futures import Future, ThreadPoolExecutor
10
13
  from contextlib import asynccontextmanager, contextmanager
11
14
  from contextvars import ContextVar, Token
12
15
  from datetime import datetime, timezone
@@ -132,6 +135,10 @@ class DocentTracer:
132
135
  self._transcript_group_states: dict[str, dict[str, Optional[str]]] = {}
133
136
  self._transcript_group_state_lock = threading.Lock()
134
137
  self._flush_lock = threading.Lock()
138
+ self._http_executor: Optional[ThreadPoolExecutor] = None
139
+ self._http_executor_lock = threading.Lock()
140
+ self._pending_http_futures: Set[Future[Any]] = set()
141
+ self._pending_http_lock = threading.Lock()
135
142
 
136
143
  def get_current_agent_run_id(self) -> Optional[str]:
137
144
  """
@@ -441,6 +448,12 @@ class DocentTracer:
441
448
  try:
442
449
  self.flush()
443
450
 
451
+ if self._http_executor:
452
+ self._http_executor.shutdown(wait=True)
453
+ self._http_executor = None
454
+ with self._pending_http_lock:
455
+ self._pending_http_futures.clear()
456
+
444
457
  if self._tracer_provider:
445
458
  self._tracer_provider.shutdown()
446
459
  self._tracer_provider = None
@@ -471,6 +484,7 @@ class DocentTracer:
471
484
  if hasattr(processor, "force_flush"):
472
485
  logger.debug(f"Flushing span processor {i}")
473
486
  processor.force_flush(timeout_millis=50)
487
+ self._wait_for_http_requests()
474
488
  logger.debug("Span flush completed")
475
489
  except Exception as e:
476
490
  logger.error(f"Error during flush: {e}")
@@ -617,7 +631,66 @@ class DocentTracer:
617
631
 
618
632
  return headers
619
633
 
620
- def _post_json(self, path: str, data: Dict[str, Any]) -> None:
634
+ def _get_http_executor(self) -> ThreadPoolExecutor:
635
+ with self._http_executor_lock:
636
+ if self._http_executor is None:
637
+ self._http_executor = ThreadPoolExecutor(
638
+ max_workers=4, thread_name_prefix="docent-http"
639
+ )
640
+ return self._http_executor
641
+
642
+ def _should_run_http_in_background(self) -> bool:
643
+ try:
644
+ loop = asyncio.get_running_loop()
645
+ except RuntimeError:
646
+ return False
647
+ return loop.is_running()
648
+
649
+ def _on_http_future_done(self, future: Future[Any]) -> None:
650
+ with self._pending_http_lock:
651
+ self._pending_http_futures.discard(future)
652
+ try:
653
+ future.result()
654
+ except Exception as exc: # pragma: no cover - defensive logging
655
+ logger.error(f"Background HTTP request failed: {exc}")
656
+
657
+ def _schedule_background_post(self, task: Callable[[], None]) -> None:
658
+ executor = self._get_http_executor()
659
+ future = executor.submit(task)
660
+ with self._pending_http_lock:
661
+ self._pending_http_futures.add(future)
662
+ future.add_done_callback(self._on_http_future_done)
663
+
664
+ def _wait_for_http_requests(self) -> None:
665
+ while True:
666
+ with self._pending_http_lock:
667
+ pending = list(self._pending_http_futures)
668
+ if not pending:
669
+ break
670
+ for future in pending:
671
+ try:
672
+ future.result()
673
+ except Exception as exc: # pragma: no cover - defensive logging
674
+ logger.error(f"Background HTTP request failed: {exc}")
675
+
676
+ def _ensure_json_serializable_metadata(self, metadata: Dict[str, Any], context: str) -> None:
677
+ """
678
+ Validate that metadata can be serialized to JSON before sending it to the backend.
679
+ """
680
+ try:
681
+ json.dumps(metadata)
682
+ except (TypeError, ValueError) as exc:
683
+ raise TypeError(f"{context} metadata must be JSON serializable") from exc
684
+
685
+ def _post_json(
686
+ self, path: str, data: Dict[str, Any], *, allow_background: bool = False
687
+ ) -> None:
688
+ if allow_background and self._should_run_http_in_background():
689
+ self._schedule_background_post(lambda: self._post_json_sync(path, data))
690
+ return
691
+ self._post_json_sync(path, data)
692
+
693
+ def _post_json_sync(self, path: str, data: Dict[str, Any]) -> None:
621
694
  if not self._api_endpoint_base:
622
695
  raise RuntimeError("API endpoint base is not configured")
623
696
  url = f"{self._api_endpoint_base}{path}"
@@ -662,6 +735,8 @@ class DocentTracer:
662
735
  if self._disabled:
663
736
  return
664
737
 
738
+ self._ensure_json_serializable_metadata(metadata, "Agent run")
739
+
665
740
  collection_id = self.collection_id
666
741
  payload: Dict[str, Any] = {
667
742
  "collection_id": collection_id,
@@ -669,7 +744,7 @@ class DocentTracer:
669
744
  "metadata": metadata,
670
745
  "timestamp": datetime.now(timezone.utc).isoformat(),
671
746
  }
672
- self._post_json("/v1/agent-run-metadata", payload)
747
+ self._post_json("/v1/agent-run-metadata", payload, allow_background=True)
673
748
 
674
749
  def send_transcript_metadata(
675
750
  self,
@@ -707,6 +782,7 @@ class DocentTracer:
707
782
  if transcript_group_id is not None:
708
783
  payload["transcript_group_id"] = transcript_group_id
709
784
  if metadata is not None:
785
+ self._ensure_json_serializable_metadata(metadata, "Transcript")
710
786
  payload["metadata"] = metadata
711
787
 
712
788
  self._post_json("/v1/transcript-metadata", payload)
@@ -925,6 +1001,7 @@ class DocentTracer:
925
1001
  if final_parent_transcript_group_id is not None:
926
1002
  payload["parent_transcript_group_id"] = final_parent_transcript_group_id
927
1003
  if metadata is not None:
1004
+ self._ensure_json_serializable_metadata(metadata, "Transcript group")
928
1005
  payload["metadata"] = metadata
929
1006
 
930
1007
  self._post_json("/v1/transcript-group-metadata", payload)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docent-python
3
- Version: 0.1.25a0
3
+ Version: 0.1.26a0
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
@@ -1,14 +1,14 @@
1
1
  docent/__init__.py,sha256=fuhETwJPcesiB76Zxa64HBJxeaaTyRalIH-fs77TWsU,112
2
2
  docent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- docent/trace.py,sha256=loJJDD3AX-rrP-QsZ8WkkFPxKd4u9wiiBG0gtxSgY0I,69743
3
+ docent/trace.py,sha256=3Z6xy4ZsP5S1_xrN6luwSsHfZlQL94qLZazJ3v-fzxQ,72953
4
4
  docent/trace_temp.py,sha256=Z0lAPwVzXjFvxpiU-CuvfWIslq9Q4alNkZMoQ77Xudk,40711
5
5
  docent/_llm_util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  docent/_llm_util/llm_cache.py,sha256=nGrvfFikFbEnfmzZRvWvZ60gfVSTvW1iC8-ciCXwbAk,6430
7
- docent/_llm_util/llm_svc.py,sha256=PQ-96UDJrnPa9csTKL_JDO8jzOrLzysVBqUHywuij0w,18046
7
+ docent/_llm_util/llm_svc.py,sha256=WbLx8F0BfToL0PUIUKNWVwjAoGPE3AUCbVJLBIWTeWA,18109
8
8
  docent/_llm_util/model_registry.py,sha256=8Y4VwrA2f2EX78cG1VBIBHVvT_p4qqBTdu9a9zJpfTo,3382
9
9
  docent/_llm_util/data_models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  docent/_llm_util/data_models/exceptions.py,sha256=IW4BVMVp8r5TufNXyrhy3acgwJiQQQPQjB9VA4RVXw8,1489
11
- docent/_llm_util/data_models/llm_output.py,sha256=ZAIIcgfxMZtTft8bXTPAhUcXEO48GLG3epkul_4gQNQ,10239
11
+ docent/_llm_util/data_models/llm_output.py,sha256=UCYewoXN72skigN_fm414TzQol1KxmVbQGwgGVROE_4,10602
12
12
  docent/_llm_util/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  docent/_llm_util/providers/anthropic.py,sha256=-1oPd5FB4aFwKSmNvXzG8PVewjhgsogLRX1SCpnCxoA,18720
14
14
  docent/_llm_util/providers/common.py,sha256=dgcTuU4XkCKoAaM48UW8zMgRYUzj7TDBhvWqtnxBO7g,1166
@@ -53,7 +53,7 @@ docent/samples/tb_airline.json,sha256=eR2jFFRtOw06xqbEglh6-dPewjifOk-cuxJq67Dtu5
53
53
  docent/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  docent/sdk/agent_run_writer.py,sha256=0AWdxejoqZyuj9JSA39WlEwGcMSYTWNqnzIuluySY-M,11043
55
55
  docent/sdk/client.py,sha256=aB_ILmzzK9JAC2kobtnp50stfINpSfNh54siaDlMEKc,19880
56
- docent_python-0.1.25a0.dist-info/METADATA,sha256=kBFVQJ-HqomY9dG6wd3xCw9OyC5gRP7Gz3ZIQHpqq0c,1351
57
- docent_python-0.1.25a0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
58
- docent_python-0.1.25a0.dist-info/licenses/LICENSE.md,sha256=QIMv2UiT6MppRasso4ymaA0w7ltkqmlL0HCt8CLD7Rc,580
59
- docent_python-0.1.25a0.dist-info/RECORD,,
56
+ docent_python-0.1.26a0.dist-info/METADATA,sha256=TWwg2-VbK4CCWDDx36P4tnbExZw7ljYGoVMhWqfofZk,1351
57
+ docent_python-0.1.26a0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
58
+ docent_python-0.1.26a0.dist-info/licenses/LICENSE.md,sha256=QIMv2UiT6MppRasso4ymaA0w7ltkqmlL0HCt8CLD7Rc,580
59
+ docent_python-0.1.26a0.dist-info/RECORD,,