docent-python 0.1.12a0__py3-none-any.whl → 0.1.13a0__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/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
- __all__ = ["Docent"]
1
+ __all__ = ["Docent", "init"]
2
2
 
3
+ from docent.agent_run_writer import init
3
4
  from docent.sdk.client import Docent
@@ -0,0 +1,266 @@
1
+ import atexit
2
+ import os
3
+ import queue
4
+ import signal
5
+ import threading
6
+ import time
7
+ from typing import Any, Callable, Coroutine, Optional
8
+
9
+ import anyio
10
+ import backoff
11
+ import httpx
12
+ from backoff.types import Details
13
+
14
+ from docent._log_util.logger import get_logger
15
+ from docent.data_models.agent_run import AgentRun
16
+ from docent.sdk.client import Docent
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ def _giveup(exc: BaseException) -> bool:
22
+ """Give up on client errors."""
23
+
24
+ if isinstance(exc, httpx.HTTPStatusError):
25
+ status = exc.response.status_code
26
+ return status < 500 and status != 429
27
+ return False
28
+
29
+
30
+ def _print_backoff_message(e: Details):
31
+ logger.warning(
32
+ f"AgentRunWriter backing off for {e['wait']:.2f}s due to {e['exception'].__class__.__name__}" # type: ignore
33
+ )
34
+
35
+
36
+ class AgentRunWriter:
37
+ """Background thread for logging agent runs.
38
+
39
+ Args:
40
+ api_key (str): API key for the Docent API.
41
+ collection_id (str): ID of the collection to log agent runs to.
42
+ server_url (str): URL of the Docent server.
43
+ num_workers (int): Max number of concurrent tasks to run,
44
+ managed by anyio.CapacityLimiter.
45
+ queue_maxsize (int): Maximum size of the queue.
46
+ If maxsize is <= 0, the queue size is infinite.
47
+ request_timeout (float): Timeout for the HTTP request.
48
+ flush_interval (float): Interval to flush the queue.
49
+ batch_size (int): Number of agent runs to batch together.
50
+ max_retries (int): Maximum number of retries for the HTTP request.
51
+ shutdown_timeout (int): Timeout to wait for the background thread to finish
52
+ after the main thread has requested shutdown.
53
+ """
54
+
55
+ _instance: Optional["AgentRunWriter"] = None
56
+ _instance_lock = threading.Lock()
57
+
58
+ def __init__(
59
+ self,
60
+ api_key: str,
61
+ collection_id: str,
62
+ server_url: str = "https://api.docent.transluce.org",
63
+ num_workers: int = 2,
64
+ queue_maxsize: int = 20_000,
65
+ request_timeout: float = 30.0,
66
+ flush_interval: float = 1.0,
67
+ batch_size: int = 1_000,
68
+ max_retries: int = 5,
69
+ shutdown_timeout: int = 60,
70
+ ) -> None:
71
+ with self._instance_lock:
72
+ if AgentRunWriter._instance is not None:
73
+ return
74
+ AgentRunWriter._instance = self
75
+
76
+ # Request parameters
77
+ self._headers = {"Authorization": f"Bearer {api_key}"}
78
+ self._base_url = server_url.rstrip("/") + "/rest"
79
+ self._endpoint = f"{collection_id}/agent_runs"
80
+
81
+ self._num_workers = num_workers
82
+ self._request_timeout = request_timeout
83
+ self._flush_interval = flush_interval
84
+ self._batch_size = batch_size
85
+ self._max_retries = max_retries
86
+ self._shutdown_timeout = shutdown_timeout
87
+
88
+ self._queue: queue.Queue[AgentRun] = queue.Queue(maxsize=queue_maxsize)
89
+ self._cancel_event = threading.Event()
90
+
91
+ # Start background thread
92
+ self._thread = threading.Thread(
93
+ target=lambda: anyio.run(self._async_main),
94
+ name="AgentRunWriterThread",
95
+ daemon=True,
96
+ )
97
+ self._thread.start()
98
+ logger.info("AgentRunWriter thread started")
99
+
100
+ self._register_shutdown_hooks()
101
+
102
+ def _register_shutdown_hooks(self) -> None:
103
+ """Register shutdown hooks for atexit and signals."""
104
+
105
+ # Register shutdown hooks
106
+ atexit.register(self.finish)
107
+
108
+ # Register signal handlers for graceful shutdown
109
+ signal.signal(signal.SIGINT, lambda s, f: self._shutdown()) # Ctrl+C
110
+ signal.signal(signal.SIGTERM, lambda s, f: self._shutdown()) # Kill signal
111
+
112
+ def log_agent_runs(self, agent_runs: list[AgentRun]) -> None:
113
+ """Put a list of AgentRun objects into the queue.
114
+
115
+ If the queue is full, the method will block until the queue has space.
116
+
117
+ Args:
118
+ agent_runs (list[AgentRun]): List of AgentRun objects to put into the queue.
119
+ """
120
+
121
+ p_full = (
122
+ (self._queue.qsize() + len(agent_runs)) / self._queue.maxsize
123
+ if self._queue.maxsize > 0
124
+ else 0
125
+ )
126
+ if p_full >= 0.9:
127
+ logger.warning("AgentRunWriter queue is almost full (>=90%).")
128
+
129
+ for run in agent_runs:
130
+ try:
131
+ self._queue.put_nowait(run)
132
+ except queue.Full:
133
+ logger.warning("AgentRunWriter queue is full, blocking...")
134
+ self._queue.put(run, block=True)
135
+
136
+ def finish(self, force: bool = False) -> None:
137
+ """Request shutdown and wait up to timeout for pending tasks to complete.
138
+
139
+ Args:
140
+ force (bool): If True, shut down immediately. If False, wait for pending tasks to complete.
141
+ """
142
+ if not force:
143
+ # Wait for background thread to finish up to timeout
144
+ logger.info("Waiting for pending tasks to complete")
145
+
146
+ for i in range(0, self._shutdown_timeout, 5):
147
+ if not self._thread.is_alive():
148
+ break
149
+
150
+ if self._queue.empty():
151
+ break
152
+
153
+ logger.info(
154
+ f"Waiting for pending tasks to complete " f"({i}/{self._shutdown_timeout})s"
155
+ )
156
+ time.sleep(5)
157
+
158
+ self._shutdown()
159
+
160
+ def _shutdown(self) -> None:
161
+ """Shutdown the AgentRunWriter thread."""
162
+ if self._thread.is_alive():
163
+ logger.info("Cancelling pending tasks...")
164
+ self._cancel_event.set()
165
+ n_pending = self._queue.qsize()
166
+ logger.info(f"Cancelled ~{n_pending} pending tasks")
167
+
168
+ # Give a brief moment to exit
169
+ logger.info("Waiting for thread to exit...")
170
+ self._thread.join(timeout=1.0)
171
+
172
+ def get_post_batch_fcn(
173
+ self, client: httpx.AsyncClient
174
+ ) -> Callable[[list[AgentRun], anyio.CapacityLimiter], Coroutine[Any, Any, None]]:
175
+ """Return a function that will post a batch of agent runs to the API."""
176
+
177
+ @backoff.on_exception(
178
+ backoff.expo,
179
+ exception=httpx.HTTPError,
180
+ giveup=_giveup,
181
+ max_tries=self._max_retries,
182
+ on_backoff=_print_backoff_message,
183
+ )
184
+ async def _post_batch(batch: list[AgentRun], limiter: anyio.CapacityLimiter) -> None:
185
+ async with limiter:
186
+ payload = {"agent_runs": [ar.model_dump(mode="json") for ar in batch]}
187
+ resp = await client.post(
188
+ self._endpoint, json=payload, timeout=self._request_timeout
189
+ )
190
+ resp.raise_for_status()
191
+
192
+ return _post_batch
193
+
194
+ async def _async_main(self) -> None:
195
+ """Main async function for the AgentRunWriter thread."""
196
+
197
+ limiter = anyio.CapacityLimiter(self._num_workers)
198
+
199
+ async with httpx.AsyncClient(base_url=self._base_url, headers=self._headers) as client:
200
+ async with anyio.create_task_group() as tg:
201
+ _post_batch = self.get_post_batch_fcn(client)
202
+
203
+ async def batch_loop() -> None:
204
+ while not self._cancel_event.is_set():
205
+ batch = await self._gather_next_batch_from_queue()
206
+ if not batch:
207
+ continue
208
+
209
+ tg.start_soon(_post_batch, batch, limiter)
210
+
211
+ tg.start_soon(batch_loop)
212
+
213
+ async def _gather_next_batch_from_queue(self) -> list[AgentRun]:
214
+ """Gather a batch of agent runs from the queue.
215
+
216
+ Fetches items from the queue until the batch is full or the timeout expires.
217
+ """
218
+ batch: list[AgentRun] = []
219
+ with anyio.move_on_after(self._flush_interval):
220
+ while len(batch) < self._batch_size:
221
+ try:
222
+ item = self._queue.get_nowait()
223
+ batch.append(item)
224
+ except queue.Empty:
225
+ await anyio.sleep(0.1)
226
+
227
+ return batch
228
+
229
+
230
+ def init(
231
+ collection_name: str = "Agent Run Collection",
232
+ collection_id: str | None = None,
233
+ server_url: str = "https://api.docent.transluce.org",
234
+ web_url: str = "https://docent.transluce.org",
235
+ api_key: str | None = None,
236
+ ):
237
+ """Initialize the AgentRunWriter thread.
238
+
239
+ Args:
240
+ collection_name (str): Name of the agent run collection.
241
+ collection_id (str): ID of the agent run collection.
242
+ server_url (str): URL of the Docent server.
243
+ web_url (str): URL of the Docent web UI.
244
+ api_key (str): API key for the Docent API.
245
+ """
246
+ api_key = api_key or os.getenv("DOCENT_API_KEY")
247
+
248
+ if api_key is None:
249
+ raise ValueError(
250
+ "api_key is required. Please provide an "
251
+ "api_key or set the DOCENT_API_KEY environment variable."
252
+ )
253
+
254
+ sdk = Docent(
255
+ server_url=server_url,
256
+ web_url=web_url,
257
+ api_key=api_key,
258
+ )
259
+
260
+ collection_id = collection_id or sdk.create_collection(name=collection_name)
261
+
262
+ return AgentRunWriter(
263
+ api_key=api_key,
264
+ collection_id=collection_id,
265
+ server_url=server_url,
266
+ )
@@ -20,9 +20,9 @@ class ToolCall:
20
20
  """
21
21
 
22
22
  id: str
23
- type: Literal["function"] | None
24
23
  function: str
25
24
  arguments: dict[str, Any]
25
+ type: Literal["function"] | None = None
26
26
  parse_error: str | None = None
27
27
  view: ToolCallContent | None = None
28
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docent-python
3
- Version: 0.1.12a0
3
+ Version: 0.1.13a0
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,4 +1,5 @@
1
- docent/__init__.py,sha256=J2BbO6rzilfw9WXRUeolr439EGFezqbMU_kCpCCryRA,59
1
+ docent/__init__.py,sha256=KY_gsq7iKn5tZODvwbsDFMxAZukniec5nBoYawdhglo,108
2
+ docent/agent_run_writer.py,sha256=QNCV4m36c9BuhzWCyuzs0wH9ql8uubzcQUXMhc3XVug,9135
2
3
  docent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
4
  docent/trace.py,sha256=C8oO5NuChSgyHIB5OI6qOfXWaIld7jdvlEqImK56a1E,66761
4
5
  docent/trace_temp.py,sha256=Z0lAPwVzXjFvxpiU-CuvfWIslq9Q4alNkZMoQ77Xudk,40711
@@ -16,7 +17,7 @@ docent/data_models/transcript.py,sha256=Gmy4lYdlvC5SXzpnerFJ83lIMPPiYUPgjOUbwg6a
16
17
  docent/data_models/chat/__init__.py,sha256=GleyRzYqKRkwwSRm_tQJw5BudCbgu9WRSa71Fntz0L0,610
17
18
  docent/data_models/chat/content.py,sha256=Co-jO8frQa_DSP11wJuhPX0s-GpJk8yqtKqPeiAIZ_U,1672
18
19
  docent/data_models/chat/message.py,sha256=xGt09keA6HRxw40xB_toNzEqA9ip7k53dnhXrEbKGO8,4157
19
- docent/data_models/chat/tool.py,sha256=x7NKINswPe0Kqvcx4ubjHzB-n0-i4DbFodvaBb2vitk,3042
20
+ docent/data_models/chat/tool.py,sha256=MMglNHzkwHqUoK0xDWqs2FtelPsgHqwVpGpI1F8KZyw,3049
20
21
  docent/loaders/load_inspect.py,sha256=_cK2Qd6gyLQuJVzOlsvEZz7TrqzNmH6ZsLTkSCWAPqQ,6628
21
22
  docent/samples/__init__.py,sha256=roDFnU6515l9Q8v17Es_SpWyY9jbm5d6X9lV01V0MZo,143
22
23
  docent/samples/load.py,sha256=ZGE07r83GBNO4A0QBh5aQ18WAu3mTWA1vxUoHd90nrM,207
@@ -24,7 +25,7 @@ docent/samples/log.eval,sha256=orrW__9WBfANq7NwKsPSq9oTsQRcG6KohG5tMr_X_XY,39770
24
25
  docent/samples/tb_airline.json,sha256=eR2jFFRtOw06xqbEglh6-dPewjifOk-cuxJq67Dtu5I,47028
25
26
  docent/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
27
  docent/sdk/client.py,sha256=rvOFXvyAr9QxCijN0_CWENbm8y3YQvR1msfFSBDZvOw,13309
27
- docent_python-0.1.12a0.dist-info/METADATA,sha256=OnxdikeOy69TZy_HGoWUxi7VDu20k8nwrikLcIPLXho,1038
28
- docent_python-0.1.12a0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- docent_python-0.1.12a0.dist-info/licenses/LICENSE.md,sha256=vOHzq3K4Ndu0UV9hPrtXvlD7pHOjyDQmGjHuLSIkRQY,1087
30
- docent_python-0.1.12a0.dist-info/RECORD,,
28
+ docent_python-0.1.13a0.dist-info/METADATA,sha256=NOSOTU8xJ5BHt_lA1Hq749RNknOBHMC2v7cQbF5ssEs,1038
29
+ docent_python-0.1.13a0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ docent_python-0.1.13a0.dist-info/licenses/LICENSE.md,sha256=vOHzq3K4Ndu0UV9hPrtXvlD7pHOjyDQmGjHuLSIkRQY,1087
31
+ docent_python-0.1.13a0.dist-info/RECORD,,