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.
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/PKG-INFO +1 -1
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/llm_output.py +8 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/llm_svc.py +3 -3
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/openai.py +6 -1
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/trace.py +108 -8
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/pyproject.toml +1 -1
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/.gitignore +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/LICENSE.md +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/README.md +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/exceptions.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/llm_cache.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/model_registry.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/anthropic.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/common.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/google.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/openrouter.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/preference_types.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/provider_registry.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_log_util/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_log_util/logger.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/_tiktoken_util.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/agent_run.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/content.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/message.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/chat/tool.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/citation.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/judge.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/metadata_util.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/regex.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/remove_invalid_citation_ranges.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/shared_types.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/transcript.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/data_models/util.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/analysis.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/impl.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/runner.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/stats.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/types.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/forgiving_json.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/meta_schema.json +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/meta_schema.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/parse_output.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/judges/util/voting.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/loaders/load_inspect.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/py.typed +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/load.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/log.eval +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/samples/tb_airline.json +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/sdk/__init__.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/sdk/agent_run_writer.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/sdk/client.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/trace_temp.py +0 -0
- {docent_python-0.1.24a0 → docent_python-0.1.26a0}/uv.lock +0 -0
{docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/llm_output.py
RENAMED
|
@@ -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 {
|
|
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.
|
|
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
|
|
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
|
|
899
|
-
payload["name"] =
|
|
900
|
-
if
|
|
901
|
-
payload["description"] =
|
|
902
|
-
if
|
|
903
|
-
payload["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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/data_models/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/preference_types.py
RENAMED
|
File without changes
|
{docent_python-0.1.24a0 → docent_python-0.1.26a0}/docent/_llm_util/providers/provider_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|