docent-python 0.1.38a0__tar.gz → 0.1.40a0__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.
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/PKG-INFO +1 -1
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/model_registry.py +4 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/preference_types.py +1 -1
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/agent_run.py +44 -8
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/chat/message.py +2 -2
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/judge.py +0 -2
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/sdk/client.py +272 -16
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/sdk/llm_context.py +3 -1
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/pyproject.toml +1 -1
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/.gitignore +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/LICENSE.md +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/README.md +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/data_models/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/data_models/exceptions.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/data_models/llm_output.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/llm_cache.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/llm_svc.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/anthropic.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/common.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/google.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/openai.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/openrouter.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/provider_registry.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_log_util/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_log_util/logger.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/_tiktoken_util.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/chat/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/chat/content.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/chat/tool.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/citation.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/collection.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/formatted_objects.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/metadata_util.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/regex.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/transcript.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/data_models/util.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/analysis.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/impl.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/runner.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/stats.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/types.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/util/forgiving_json.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/util/meta_schema.json +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/util/meta_schema.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/util/parse_output.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/judges/util/voting.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/loaders/load_inspect.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/py.typed +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/samples/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/samples/load.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/samples/log.eval +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/samples/tb_airline.json +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/sdk/__init__.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/sdk/agent_run_writer.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/trace.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/trace_temp.py +0 -0
- {docent_python-0.1.38a0 → docent_python-0.1.40a0}/uv.lock +0 -0
|
@@ -54,6 +54,10 @@ _REGISTRY: list[tuple[str, ModelInfo]] = [
|
|
|
54
54
|
"claude-sonnet-4",
|
|
55
55
|
ModelInfo(rate={"input": 3.0, "output": 15.0}, context_window=200_000),
|
|
56
56
|
),
|
|
57
|
+
(
|
|
58
|
+
"claude-sonnet-4-5",
|
|
59
|
+
ModelInfo(rate={"input": 3.0, "output": 15.0}, context_window=200_000),
|
|
60
|
+
),
|
|
57
61
|
(
|
|
58
62
|
"claude-haiku-4-5",
|
|
59
63
|
ModelInfo(rate={"input": 1.0, "output": 5.0}, context_window=200_000),
|
{docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/providers/preference_types.py
RENAMED
|
@@ -95,7 +95,7 @@ class PublicProviderPreferences(BaseModel):
|
|
|
95
95
|
ModelOption(provider="openai", model_name="gpt-5-mini", reasoning_effort="high"),
|
|
96
96
|
ModelOption(
|
|
97
97
|
provider="anthropic",
|
|
98
|
-
model_name="claude-sonnet-4-
|
|
98
|
+
model_name="claude-sonnet-4-5",
|
|
99
99
|
reasoning_effort="medium",
|
|
100
100
|
),
|
|
101
101
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import textwrap
|
|
3
|
+
from collections import deque
|
|
3
4
|
from datetime import datetime
|
|
4
|
-
from queue import Queue
|
|
5
5
|
from typing import Any, Literal, TypedDict, cast
|
|
6
6
|
from uuid import uuid4
|
|
7
7
|
|
|
@@ -257,6 +257,13 @@ class AgentRun(BaseModel):
|
|
|
257
257
|
self._transcript_group_dict = {tg.id: tg for tg in self.transcript_groups}
|
|
258
258
|
return self._transcript_group_dict
|
|
259
259
|
|
|
260
|
+
def _invalidate_caches(self) -> None:
|
|
261
|
+
"""Reset cached lookups after mutating transcripts or transcript groups."""
|
|
262
|
+
self._transcript_dict = None
|
|
263
|
+
self._transcript_group_dict = None
|
|
264
|
+
self._canonical_tree_cache.clear()
|
|
265
|
+
self._transcript_ids_ordered_cache.clear()
|
|
266
|
+
|
|
260
267
|
def get_canonical_tree(
|
|
261
268
|
self, full_tree: bool = False
|
|
262
269
|
) -> dict[str | None, list[tuple[Literal["t", "tg"], str]]]:
|
|
@@ -328,14 +335,11 @@ class AgentRun(BaseModel):
|
|
|
328
335
|
tg_tree.setdefault(t.transcript_group_id or "__global_root", set()).add(("t", t_id))
|
|
329
336
|
else:
|
|
330
337
|
# Initialize q with "important" tgs
|
|
331
|
-
q, seen =
|
|
332
|
-
for tg_id in tgs_to_transcripts.keys():
|
|
333
|
-
q.put(tg_id)
|
|
334
|
-
seen.add(tg_id)
|
|
338
|
+
q, seen = deque(tgs_to_transcripts.keys()), set(tgs_to_transcripts.keys())
|
|
335
339
|
|
|
336
340
|
# Do an "upwards BFS" from leaves up to the root. Builds a tree of only relevant nodes.
|
|
337
|
-
while q
|
|
338
|
-
u_id = q.
|
|
341
|
+
while q:
|
|
342
|
+
u_id = q.popleft()
|
|
339
343
|
u = tg_dict.get(u_id) # None if __global_root
|
|
340
344
|
|
|
341
345
|
# Add the transcripts under this tg
|
|
@@ -349,7 +353,7 @@ class AgentRun(BaseModel):
|
|
|
349
353
|
tg_tree.setdefault(par_id, set()).add(("tg", u_id))
|
|
350
354
|
# If we haven't investigated the parent before, add to q
|
|
351
355
|
if par_id not in seen:
|
|
352
|
-
q.
|
|
356
|
+
q.append(par_id)
|
|
353
357
|
seen.add(par_id)
|
|
354
358
|
|
|
355
359
|
# For each node, sort by created_at timestamp
|
|
@@ -384,6 +388,38 @@ class AgentRun(BaseModel):
|
|
|
384
388
|
|
|
385
389
|
return c_tree, transcript_idx_map
|
|
386
390
|
|
|
391
|
+
def delete_transcript_group_subtree(self, transcript_group_id: str) -> None:
|
|
392
|
+
"""Delete a transcript group and all descendant groups/transcripts using the canonical tree."""
|
|
393
|
+
if transcript_group_id == "__global_root":
|
|
394
|
+
raise ValueError("Cannot delete the global root sentinel")
|
|
395
|
+
if transcript_group_id not in self.transcript_group_dict:
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"Transcript group '{transcript_group_id}' does not exist on this run."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
canonical_tree = self.get_canonical_tree(full_tree=True)
|
|
401
|
+
groups_to_delete: set[str] = set()
|
|
402
|
+
transcripts_to_delete: set[str] = set()
|
|
403
|
+
|
|
404
|
+
queue: deque[str] = deque([transcript_group_id])
|
|
405
|
+
while queue:
|
|
406
|
+
current_group = queue.popleft()
|
|
407
|
+
groups_to_delete.add(current_group)
|
|
408
|
+
for child_type, child_id in canonical_tree.get(current_group, []):
|
|
409
|
+
if child_type == "tg":
|
|
410
|
+
queue.append(child_id)
|
|
411
|
+
else:
|
|
412
|
+
transcripts_to_delete.add(child_id)
|
|
413
|
+
|
|
414
|
+
if groups_to_delete:
|
|
415
|
+
self.transcript_groups = [
|
|
416
|
+
tg for tg in self.transcript_groups if tg.id not in groups_to_delete
|
|
417
|
+
]
|
|
418
|
+
if transcripts_to_delete:
|
|
419
|
+
self.transcripts = [t for t in self.transcripts if t.id not in transcripts_to_delete]
|
|
420
|
+
|
|
421
|
+
self._invalidate_caches()
|
|
422
|
+
|
|
387
423
|
def to_text_new(
|
|
388
424
|
self,
|
|
389
425
|
agent_run_alias: int | str = 0,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from logging import getLogger
|
|
2
2
|
from typing import Annotated, Any, Literal
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel, Discriminator
|
|
4
|
+
from pydantic import BaseModel, Discriminator
|
|
5
5
|
|
|
6
6
|
from docent.data_models.chat.content import Content
|
|
7
7
|
from docent.data_models.chat.tool import ToolCall
|
|
@@ -23,7 +23,7 @@ class BaseChatMessage(BaseModel):
|
|
|
23
23
|
id: str | None = None
|
|
24
24
|
content: str | list[Content]
|
|
25
25
|
role: Literal["system", "user", "assistant", "tool"]
|
|
26
|
-
metadata: dict[str, Any] =
|
|
26
|
+
metadata: dict[str, Any] | None = None
|
|
27
27
|
|
|
28
28
|
@property
|
|
29
29
|
def text(self) -> str:
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import gzip
|
|
1
2
|
import itertools
|
|
3
|
+
import json
|
|
2
4
|
import os
|
|
5
|
+
import time
|
|
3
6
|
import webbrowser
|
|
4
7
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Literal
|
|
8
|
+
from typing import Any, Iterator, Literal
|
|
6
9
|
|
|
7
10
|
import pandas as pd
|
|
8
11
|
import requests
|
|
12
|
+
from pydantic_core import to_jsonable_python
|
|
9
13
|
from tqdm import tqdm
|
|
10
14
|
|
|
11
15
|
from docent._log_util.logger import get_logger
|
|
@@ -16,6 +20,61 @@ from docent.judges.util.meta_schema import validate_judge_result_schema
|
|
|
16
20
|
from docent.loaders import load_inspect
|
|
17
21
|
from docent.sdk.llm_context import LLMContext, LLMContextItem
|
|
18
22
|
|
|
23
|
+
MAX_AGENT_RUN_PAYLOAD_BYTES = 100 * 1024 * 1024 # 100MB backend limit
|
|
24
|
+
_AGENT_RUNS_PAYLOAD_PREFIX = b'{"agent_runs":['
|
|
25
|
+
_AGENT_RUNS_PAYLOAD_SUFFIX = b"]}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _serialize_agent_run(agent_run: AgentRun) -> bytes:
|
|
29
|
+
"""Serialize an AgentRun to compact JSON bytes."""
|
|
30
|
+
return json.dumps(to_jsonable_python(agent_run), separators=(",", ":")).encode("utf-8")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_agent_runs_payload(serialized_runs: list[bytes]) -> bytes:
|
|
34
|
+
"""Wrap serialized individual runs into the API payload envelope."""
|
|
35
|
+
body = b",".join(serialized_runs)
|
|
36
|
+
return _AGENT_RUNS_PAYLOAD_PREFIX + body + _AGENT_RUNS_PAYLOAD_SUFFIX
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _yield_agent_run_batches_by_size(
|
|
40
|
+
agent_runs: list[AgentRun], max_payload_bytes: int
|
|
41
|
+
) -> Iterator[tuple[int, bytes]]:
|
|
42
|
+
"""Yield batches of agent runs whose serialized payloads stay within max_payload_bytes."""
|
|
43
|
+
envelope_len = len(_AGENT_RUNS_PAYLOAD_PREFIX) + len(_AGENT_RUNS_PAYLOAD_SUFFIX)
|
|
44
|
+
comma_len = 1
|
|
45
|
+
|
|
46
|
+
current_serialized: list[bytes] = []
|
|
47
|
+
current_size = envelope_len
|
|
48
|
+
|
|
49
|
+
for agent_run in agent_runs:
|
|
50
|
+
serialized = _serialize_agent_run(agent_run)
|
|
51
|
+
serialized_len = len(serialized)
|
|
52
|
+
|
|
53
|
+
if envelope_len + serialized_len > max_payload_bytes:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"A single agent run (id={agent_run.id}) exceeds the maximum payload size of "
|
|
56
|
+
f"{max_payload_bytes} bytes. Reduce the size of that run before uploading."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
delimiter = 0 if not current_serialized else comma_len
|
|
60
|
+
projected_size = current_size + delimiter + serialized_len
|
|
61
|
+
|
|
62
|
+
# If adding the next run would exceed the max payload size, yield the current batch
|
|
63
|
+
if current_serialized and projected_size > max_payload_bytes:
|
|
64
|
+
yield len(current_serialized), _build_agent_runs_payload(current_serialized)
|
|
65
|
+
|
|
66
|
+
# Add the "next run" as the first run in the next batch
|
|
67
|
+
current_serialized = [serialized]
|
|
68
|
+
current_size = envelope_len + serialized_len
|
|
69
|
+
# Otherwise, add to the current batch and continue
|
|
70
|
+
else:
|
|
71
|
+
current_serialized.append(serialized)
|
|
72
|
+
current_size = projected_size
|
|
73
|
+
|
|
74
|
+
if current_serialized:
|
|
75
|
+
yield len(current_serialized), _build_agent_runs_payload(current_serialized)
|
|
76
|
+
|
|
77
|
+
|
|
19
78
|
logger = get_logger(__name__)
|
|
20
79
|
|
|
21
80
|
|
|
@@ -37,6 +96,7 @@ class Docent:
|
|
|
37
96
|
self,
|
|
38
97
|
*,
|
|
39
98
|
domain: str = "docent.transluce.org",
|
|
99
|
+
use_https: bool = True,
|
|
40
100
|
api_key: str | None = None,
|
|
41
101
|
# Deprecated
|
|
42
102
|
server_url: str | None = None, # Use domain instead
|
|
@@ -73,13 +133,14 @@ class Docent:
|
|
|
73
133
|
self._domain = domain
|
|
74
134
|
|
|
75
135
|
# Set server URL; server_url takes precedence over domain
|
|
76
|
-
|
|
136
|
+
prefix = "https://" if use_https else "http://"
|
|
137
|
+
server_url = (server_url or f"{prefix}api.{domain}").rstrip("/")
|
|
77
138
|
if not server_url.endswith("/rest"):
|
|
78
139
|
server_url = f"{server_url}/rest"
|
|
79
140
|
self._server_url = server_url
|
|
80
141
|
|
|
81
142
|
# Set web URL; web_url takes precedence over domain
|
|
82
|
-
self._web_url = (web_url or f"
|
|
143
|
+
self._web_url = (web_url or f"{prefix}{domain}").rstrip("/")
|
|
83
144
|
|
|
84
145
|
# Use requests.Session for connection pooling and persistent headers
|
|
85
146
|
self._session = requests.Session()
|
|
@@ -192,41 +253,199 @@ class Docent:
|
|
|
192
253
|
logger.info(f"Successfully updated Collection '{collection_id}'")
|
|
193
254
|
|
|
194
255
|
def add_agent_runs(
|
|
195
|
-
self,
|
|
256
|
+
self,
|
|
257
|
+
collection_id: str,
|
|
258
|
+
agent_runs: list[AgentRun],
|
|
259
|
+
*,
|
|
260
|
+
compression: Literal["gzip", "none"] = "gzip",
|
|
261
|
+
wait: bool = True,
|
|
262
|
+
poll_interval: float = 1.0,
|
|
263
|
+
# Deprecated
|
|
264
|
+
batch_size: int | None = None,
|
|
196
265
|
) -> dict[str, Any]:
|
|
197
266
|
"""Adds agent runs to a Collection.
|
|
198
267
|
|
|
199
268
|
Agent runs represent execution traces that can be visualized and analyzed.
|
|
200
|
-
|
|
269
|
+
Requests are automatically chunked to stay under the backend's payload limit.
|
|
201
270
|
|
|
202
271
|
Args:
|
|
203
272
|
collection_id: ID of the Collection.
|
|
204
273
|
agent_runs: List of AgentRun objects to add.
|
|
274
|
+
compression: Compression algorithm for request bodies. Defaults to gzip.
|
|
275
|
+
Set to "none" to retain legacy behavior.
|
|
276
|
+
wait: If True (default), wait for all ingestion jobs to complete before returning.
|
|
277
|
+
If False, return immediately after enqueuing jobs.
|
|
278
|
+
poll_interval: Seconds between status checks when wait=True. Defaults to 1.0.
|
|
205
279
|
|
|
206
280
|
Returns:
|
|
207
|
-
dict: API response data
|
|
281
|
+
dict: API response data containing:
|
|
282
|
+
- status: "success" if all jobs completed, "enqueued" if wait=False
|
|
283
|
+
- total_runs_added: Number of agent runs submitted
|
|
284
|
+
- job_ids: List of job IDs for tracking
|
|
208
285
|
|
|
209
286
|
Raises:
|
|
287
|
+
ValueError: If any single agent run exceeds the maximum payload size.
|
|
210
288
|
requests.exceptions.HTTPError: If the API request fails.
|
|
289
|
+
RuntimeError: If any job fails during processing (when wait=True).
|
|
211
290
|
"""
|
|
212
|
-
|
|
291
|
+
|
|
292
|
+
if batch_size is not None:
|
|
293
|
+
logger.warning(
|
|
294
|
+
"The 'batch_size' parameter is deprecated and will be removed in a future version. "
|
|
295
|
+
"We have transitioned to a new batching strategy based on the size of the payload."
|
|
296
|
+
)
|
|
213
297
|
|
|
214
298
|
url = f"{self._server_url}/{collection_id}/agent_runs"
|
|
215
299
|
total_runs = len(agent_runs)
|
|
300
|
+
job_ids: list[str] = []
|
|
216
301
|
|
|
217
302
|
# Process agent runs in batches
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
303
|
+
desc = f"Uploading agent runs (compression={compression})"
|
|
304
|
+
with tqdm(total=total_runs, desc=desc, unit="runs") as pbar:
|
|
305
|
+
for batch_size, payload_bytes in _yield_agent_run_batches_by_size(
|
|
306
|
+
agent_runs, MAX_AGENT_RUN_PAYLOAD_BYTES
|
|
307
|
+
):
|
|
308
|
+
request_kwargs: dict[str, Any] = {}
|
|
309
|
+
if compression == "none":
|
|
310
|
+
request_kwargs["data"] = payload_bytes
|
|
311
|
+
request_kwargs["headers"] = {"Content-Type": "application/json"}
|
|
312
|
+
elif compression == "gzip":
|
|
313
|
+
request_kwargs["data"] = gzip.compress(payload_bytes)
|
|
314
|
+
request_kwargs["headers"] = {
|
|
315
|
+
"Content-Type": "application/json",
|
|
316
|
+
"Content-Encoding": "gzip",
|
|
317
|
+
}
|
|
318
|
+
else:
|
|
319
|
+
raise ValueError(f"Unsupported compression '{compression}'")
|
|
320
|
+
|
|
321
|
+
response = self._session.post(url, **request_kwargs)
|
|
224
322
|
self._handle_response_errors(response)
|
|
225
323
|
|
|
226
|
-
|
|
324
|
+
# Server returns 202 with job_id for async processing
|
|
325
|
+
response_data = response.json()
|
|
326
|
+
job_id = response_data.get("job_id")
|
|
327
|
+
if job_id:
|
|
328
|
+
job_ids.append(job_id)
|
|
329
|
+
|
|
330
|
+
pbar.update(batch_size)
|
|
331
|
+
|
|
332
|
+
if not wait:
|
|
333
|
+
logger.info(
|
|
334
|
+
f"Enqueued {total_runs} agent runs to Collection '{collection_id}' "
|
|
335
|
+
f"({len(job_ids)} job(s)). Use get_agent_run_job_status() to check progress."
|
|
336
|
+
)
|
|
337
|
+
return {
|
|
338
|
+
"status": "enqueued",
|
|
339
|
+
"total_runs_added": total_runs,
|
|
340
|
+
"job_ids": job_ids,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# Wait for all jobs to complete
|
|
344
|
+
if job_ids:
|
|
345
|
+
logger.info(
|
|
346
|
+
f"Uploaded {total_runs} agent runs in {len(job_ids)} batch(es). "
|
|
347
|
+
f"Waiting for server-side processing to complete... "
|
|
348
|
+
f"(set wait=False to skip waiting)"
|
|
349
|
+
)
|
|
350
|
+
self._wait_for_jobs(collection_id, job_ids, poll_interval)
|
|
351
|
+
|
|
352
|
+
logger.info(
|
|
353
|
+
f"Successfully added {total_runs} agent runs to Collection '{collection_id}'. "
|
|
354
|
+
f"All {len(job_ids)} job(s) completed."
|
|
355
|
+
)
|
|
356
|
+
return {"status": "success", "total_runs_added": total_runs, "job_ids": job_ids}
|
|
357
|
+
|
|
358
|
+
def _wait_for_jobs(
|
|
359
|
+
self,
|
|
360
|
+
collection_id: str,
|
|
361
|
+
job_ids: list[str],
|
|
362
|
+
poll_interval: float = 1.0,
|
|
363
|
+
) -> None:
|
|
364
|
+
"""Wait for all jobs to complete, showing progress.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
collection_id: ID of the Collection.
|
|
368
|
+
job_ids: List of job IDs to wait for.
|
|
369
|
+
poll_interval: Seconds between status checks.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
RuntimeError: If any job fails or is canceled.
|
|
373
|
+
"""
|
|
374
|
+
pending_jobs = set(job_ids)
|
|
375
|
+
failed_jobs: dict[str, str] = {}
|
|
376
|
+
|
|
377
|
+
with tqdm(total=len(job_ids), desc="Waiting for server processing", unit="jobs") as pbar:
|
|
378
|
+
while pending_jobs:
|
|
379
|
+
statuses = self.get_agent_run_job_statuses(collection_id, list(pending_jobs))
|
|
380
|
+
|
|
381
|
+
for job_status in statuses:
|
|
382
|
+
job_id = job_status["job_id"]
|
|
383
|
+
status = job_status["status"]
|
|
384
|
+
|
|
385
|
+
if status == "completed":
|
|
386
|
+
pending_jobs.discard(job_id)
|
|
387
|
+
pbar.update(1)
|
|
388
|
+
elif status == "canceled":
|
|
389
|
+
pending_jobs.discard(job_id)
|
|
390
|
+
failed_jobs[job_id] = "Job was canceled"
|
|
391
|
+
pbar.update(1)
|
|
392
|
+
|
|
393
|
+
if pending_jobs:
|
|
394
|
+
time.sleep(poll_interval)
|
|
395
|
+
|
|
396
|
+
if failed_jobs:
|
|
397
|
+
failed_msg = ", ".join(f"{k}: {v}" for k, v in failed_jobs.items())
|
|
398
|
+
raise RuntimeError(f"Some jobs failed: {failed_msg}")
|
|
399
|
+
|
|
400
|
+
def get_agent_run_job_statuses(
|
|
401
|
+
self, collection_id: str, job_ids: list[str]
|
|
402
|
+
) -> list[dict[str, Any]]:
|
|
403
|
+
"""Get the status of multiple agent run ingestion jobs.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
collection_id: ID of the Collection.
|
|
407
|
+
job_ids: List of job IDs to check (max 100).
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
list: List of job status dictionaries, each containing:
|
|
411
|
+
- job_id: The job ID
|
|
412
|
+
- status: One of "pending", "running", "completed", "canceled"
|
|
413
|
+
- type: The job type
|
|
414
|
+
- created_at: ISO timestamp of job creation
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
ValueError: If more than 100 job IDs are provided.
|
|
418
|
+
requests.exceptions.HTTPError: If the API request fails.
|
|
419
|
+
"""
|
|
420
|
+
if len(job_ids) > 100:
|
|
421
|
+
raise ValueError("Cannot request more than 100 job IDs at once")
|
|
422
|
+
|
|
423
|
+
url = f"{self._server_url}/{collection_id}/agent_runs/jobs/batch_status"
|
|
424
|
+
response = self._session.post(url, json={"job_ids": job_ids})
|
|
425
|
+
self._handle_response_errors(response)
|
|
426
|
+
return response.json()["jobs"]
|
|
427
|
+
|
|
428
|
+
def get_agent_run_job_status(self, collection_id: str, job_id: str) -> dict[str, Any]:
|
|
429
|
+
"""Get the status of an agent run ingestion job.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
collection_id: ID of the Collection.
|
|
433
|
+
job_id: The ID of the job to check.
|
|
227
434
|
|
|
228
|
-
|
|
229
|
-
|
|
435
|
+
Returns:
|
|
436
|
+
dict: Job status information including:
|
|
437
|
+
- job_id: The job ID
|
|
438
|
+
- status: One of "pending", "running", "completed", "canceled"
|
|
439
|
+
- type: The job type
|
|
440
|
+
- created_at: ISO timestamp of job creation
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
requests.exceptions.HTTPError: If the API request fails.
|
|
444
|
+
"""
|
|
445
|
+
url = f"{self._server_url}/{collection_id}/agent_runs/jobs/{job_id}"
|
|
446
|
+
response = self._session.get(url)
|
|
447
|
+
self._handle_response_errors(response)
|
|
448
|
+
return response.json()
|
|
230
449
|
|
|
231
450
|
def list_collections(self) -> list[Collection]:
|
|
232
451
|
"""Lists all available Collections.
|
|
@@ -459,6 +678,43 @@ class Docent:
|
|
|
459
678
|
self._handle_response_errors(response)
|
|
460
679
|
return response.json()
|
|
461
680
|
|
|
681
|
+
def tag_transcript(self, collection_id: str, agent_run_id: str, value: str) -> None:
|
|
682
|
+
"""Add a tag to an agent run transcript.
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
collection_id: ID of the Collection.
|
|
686
|
+
agent_run_id: The agent run to tag.
|
|
687
|
+
value: The tag value (max length enforced by the server).
|
|
688
|
+
|
|
689
|
+
Raises:
|
|
690
|
+
requests.exceptions.HTTPError: If the API request fails.
|
|
691
|
+
"""
|
|
692
|
+
url = f"{self._server_url}/label/{collection_id}/tag"
|
|
693
|
+
payload = {"agent_run_id": agent_run_id, "value": value}
|
|
694
|
+
response = self._session.post(url, json=payload)
|
|
695
|
+
self._handle_response_errors(response)
|
|
696
|
+
|
|
697
|
+
def get_tags(self, collection_id: str, value: str | None = None) -> list[dict[str, Any]]:
|
|
698
|
+
"""Get all tags in a collection, optionally filtered by value."""
|
|
699
|
+
url = f"{self._server_url}/label/{collection_id}/tags"
|
|
700
|
+
params = {"value": value} if value is not None else None
|
|
701
|
+
response = self._session.get(url, params=params)
|
|
702
|
+
self._handle_response_errors(response)
|
|
703
|
+
return response.json()
|
|
704
|
+
|
|
705
|
+
def get_tags_for_agent_run(self, collection_id: str, agent_run_id: str) -> list[dict[str, Any]]:
|
|
706
|
+
"""Get all tags attached to a specific agent run."""
|
|
707
|
+
url = f"{self._server_url}/label/{collection_id}/agent_run/{agent_run_id}/tags"
|
|
708
|
+
response = self._session.get(url)
|
|
709
|
+
self._handle_response_errors(response)
|
|
710
|
+
return response.json()
|
|
711
|
+
|
|
712
|
+
def delete_tag(self, collection_id: str, tag_id: str) -> None:
|
|
713
|
+
"""Delete a tag by ID."""
|
|
714
|
+
url = f"{self._server_url}/label/{collection_id}/tag/{tag_id}"
|
|
715
|
+
response = self._session.delete(url)
|
|
716
|
+
self._handle_response_errors(response)
|
|
717
|
+
|
|
462
718
|
def get_agent_run(self, collection_id: str, agent_run_id: str) -> AgentRun | None:
|
|
463
719
|
"""Get a specific agent run by its ID.
|
|
464
720
|
|
|
@@ -365,7 +365,9 @@ def _get_text_for_citation_target(target: CitationTarget, context: LLMContext) -
|
|
|
365
365
|
if transcript.id == item.transcript_id:
|
|
366
366
|
if 0 <= item.block_idx < len(transcript.messages):
|
|
367
367
|
message = transcript.messages[item.block_idx]
|
|
368
|
-
metadata_value =
|
|
368
|
+
metadata_value = (
|
|
369
|
+
message.metadata.get(item.metadata_key) if message.metadata else None
|
|
370
|
+
)
|
|
369
371
|
if metadata_value is not None:
|
|
370
372
|
return json.dumps(metadata_value)
|
|
371
373
|
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/data_models/exceptions.py
RENAMED
|
File without changes
|
{docent_python-0.1.38a0 → docent_python-0.1.40a0}/docent/_llm_util/data_models/llm_output.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
|
{docent_python-0.1.38a0 → docent_python-0.1.40a0}/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
|