docent-python 0.1.24a0__tar.gz → 0.1.26a0__tar.gz

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 (61) hide show
  1. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/PKG-INFO +1 -1
  2. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/llm_output.py +8 -0
  3. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/llm_svc.py +3 -3
  4. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/openai.py +6 -1
  5. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/trace.py +108 -8
  6. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/pyproject.toml +1 -1
  7. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/.gitignore +0 -0
  8. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/LICENSE.md +0 -0
  9. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/README.md +0 -0
  10. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/__init__.py +0 -0
  11. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/__init__.py +0 -0
  12. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/__init__.py +0 -0
  13. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/exceptions.py +0 -0
  14. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/llm_cache.py +0 -0
  15. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/model_registry.py +0 -0
  16. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/__init__.py +0 -0
  17. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/anthropic.py +0 -0
  18. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/common.py +0 -0
  19. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/google.py +0 -0
  20. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/openrouter.py +0 -0
  21. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/preference_types.py +0 -0
  22. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/provider_registry.py +0 -0
  23. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_log_util/__init__.py +0 -0
  24. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_log_util/logger.py +0 -0
  25. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/__init__.py +0 -0
  26. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/_tiktoken_util.py +0 -0
  27. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/agent_run.py +0 -0
  28. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/__init__.py +0 -0
  29. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/content.py +0 -0
  30. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/message.py +0 -0
  31. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/tool.py +0 -0
  32. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/citation.py +0 -0
  33. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/judge.py +0 -0
  34. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/metadata_util.py +0 -0
  35. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/regex.py +0 -0
  36. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/remove_invalid_citation_ranges.py +0 -0
  37. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/shared_types.py +0 -0
  38. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/transcript.py +0 -0
  39. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/util.py +0 -0
  40. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/__init__.py +0 -0
  41. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/analysis.py +0 -0
  42. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/impl.py +0 -0
  43. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/runner.py +0 -0
  44. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/stats.py +0 -0
  45. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/types.py +0 -0
  46. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/forgiving_json.py +0 -0
  47. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/meta_schema.json +0 -0
  48. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/meta_schema.py +0 -0
  49. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/parse_output.py +0 -0
  50. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/voting.py +0 -0
  51. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/loaders/load_inspect.py +0 -0
  52. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/py.typed +0 -0
  53. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/__init__.py +0 -0
  54. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/load.py +0 -0
  55. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/log.eval +0 -0
  56. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/tb_airline.json +0 -0
  57. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/sdk/__init__.py +0 -0
  58. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/sdk/agent_run_writer.py +0 -0
  59. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/sdk/client.py +0 -0
  60. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/trace_temp.py +0 -0
  61. {docent_python-0.1.24a0 → docent_python-0.1.26a0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docent-python
3
- Version: 0.1.24a0
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
@@ -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
@@ -18,8 +18,13 @@ from openai import (
18
18
  PermissionDeniedError,
19
19
  RateLimitError,
20
20
  UnprocessableEntityError,
21
- omit,
22
21
  )
22
+ try:
23
+ from openai import omit
24
+ except ImportError:
25
+ from openai import Omit as _OpenAIOmit
26
+
27
+ omit = _OpenAIOmit()
23
28
  from openai.types.chat import (
24
29
  ChatCompletion,
25
30
  ChatCompletionAssistantMessageParam,
@@ -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
@@ -129,7 +132,13 @@ class DocentTracer:
129
132
  lambda: itertools.count(0)
130
133
  )
131
134
  self._transcript_counter_lock = threading.Lock()
135
+ self._transcript_group_states: dict[str, dict[str, Optional[str]]] = {}
136
+ self._transcript_group_state_lock = threading.Lock()
132
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()
133
142
 
134
143
  def get_current_agent_run_id(self) -> Optional[str]:
135
144
  """
@@ -439,6 +448,12 @@ class DocentTracer:
439
448
  try:
440
449
  self.flush()
441
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
+
442
457
  if self._tracer_provider:
443
458
  self._tracer_provider.shutdown()
444
459
  self._tracer_provider = None
@@ -469,6 +484,7 @@ class DocentTracer:
469
484
  if hasattr(processor, "force_flush"):
470
485
  logger.debug(f"Flushing span processor {i}")
471
486
  processor.force_flush(timeout_millis=50)
487
+ self._wait_for_http_requests()
472
488
  logger.debug("Span flush completed")
473
489
  except Exception as e:
474
490
  logger.error(f"Error during flush: {e}")
@@ -615,7 +631,66 @@ class DocentTracer:
615
631
 
616
632
  return headers
617
633
 
618
- 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:
619
694
  if not self._api_endpoint_base:
620
695
  raise RuntimeError("API endpoint base is not configured")
621
696
  url = f"{self._api_endpoint_base}{path}"
@@ -660,6 +735,8 @@ class DocentTracer:
660
735
  if self._disabled:
661
736
  return
662
737
 
738
+ self._ensure_json_serializable_metadata(metadata, "Agent run")
739
+
663
740
  collection_id = self.collection_id
664
741
  payload: Dict[str, Any] = {
665
742
  "collection_id": collection_id,
@@ -667,7 +744,7 @@ class DocentTracer:
667
744
  "metadata": metadata,
668
745
  "timestamp": datetime.now(timezone.utc).isoformat(),
669
746
  }
670
- self._post_json("/v1/agent-run-metadata", payload)
747
+ self._post_json("/v1/agent-run-metadata", payload, allow_background=True)
671
748
 
672
749
  def send_transcript_metadata(
673
750
  self,
@@ -705,6 +782,7 @@ class DocentTracer:
705
782
  if transcript_group_id is not None:
706
783
  payload["transcript_group_id"] = transcript_group_id
707
784
  if metadata is not None:
785
+ self._ensure_json_serializable_metadata(metadata, "Transcript")
708
786
  payload["metadata"] = metadata
709
787
 
710
788
  self._post_json("/v1/transcript-metadata", payload)
@@ -888,6 +966,27 @@ class DocentTracer:
888
966
  )
889
967
  return
890
968
 
969
+ with self._transcript_group_state_lock:
970
+ state: dict[str, Optional[str]] = self._transcript_group_states.setdefault(
971
+ transcript_group_id, {}
972
+ )
973
+ final_name: Optional[str] = name if name is not None else state.get("name")
974
+ final_description: Optional[str] = (
975
+ description if description is not None else state.get("description")
976
+ )
977
+ final_parent_transcript_group_id: Optional[str] = (
978
+ parent_transcript_group_id
979
+ if parent_transcript_group_id is not None
980
+ else state.get("parent_transcript_group_id")
981
+ )
982
+
983
+ if final_name is not None:
984
+ state["name"] = final_name
985
+ if final_description is not None:
986
+ state["description"] = final_description
987
+ if final_parent_transcript_group_id is not None:
988
+ state["parent_transcript_group_id"] = final_parent_transcript_group_id
989
+
891
990
  payload: Dict[str, Any] = {
892
991
  "collection_id": collection_id,
893
992
  "transcript_group_id": transcript_group_id,
@@ -895,13 +994,14 @@ class DocentTracer:
895
994
  "timestamp": datetime.now(timezone.utc).isoformat(),
896
995
  }
897
996
 
898
- if name is not None:
899
- payload["name"] = name
900
- if description is not None:
901
- payload["description"] = description
902
- if parent_transcript_group_id is not None:
903
- payload["parent_transcript_group_id"] = parent_transcript_group_id
997
+ if final_name is not None:
998
+ payload["name"] = final_name
999
+ if final_description is not None:
1000
+ payload["description"] = final_description
1001
+ if final_parent_transcript_group_id is not None:
1002
+ payload["parent_transcript_group_id"] = final_parent_transcript_group_id
904
1003
  if metadata is not None:
1004
+ self._ensure_json_serializable_metadata(metadata, "Transcript group")
905
1005
  payload["metadata"] = metadata
906
1006
 
907
1007
  self._post_json("/v1/transcript-group-metadata", payload)
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "docent-python"
3
3
  description = "Docent SDK"
4
- version = "0.1.24-alpha"
4
+ version = "0.1.26-alpha"
5
5
  authors = [
6
6
  { name="Transluce", email="info@transluce.org" },
7
7
  ]