docent-python 0.1.60a0__tar.gz → 0.1.61a0__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.
Files changed (87) hide show
  1. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/PKG-INFO +1 -1
  2. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/reading.py +40 -2
  3. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_base.py +1 -1
  4. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_dql.py +10 -3
  5. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_readings.py +109 -52
  6. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/reading.py +14 -1
  7. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/pyproject.toml +1 -1
  8. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/uv.lock +1 -1
  9. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/.gitignore +0 -0
  10. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/LICENSE.md +0 -0
  11. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/README.md +0 -0
  12. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/__init__.py +0 -0
  13. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/__init__.py +0 -0
  14. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/data_models/__init__.py +0 -0
  15. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/data_models/exceptions.py +0 -0
  16. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/data_models/llm_output.py +0 -0
  17. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/llm_cache.py +0 -0
  18. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/llm_svc.py +0 -0
  19. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/model_registry.py +0 -0
  20. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/__init__.py +0 -0
  21. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/anthropic.py +0 -0
  22. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/common.py +0 -0
  23. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/google.py +0 -0
  24. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/openai.py +0 -0
  25. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/openrouter.py +0 -0
  26. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/preference_types.py +0 -0
  27. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/provider_registry.py +0 -0
  28. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_log_util/__init__.py +0 -0
  29. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/_log_util/logger.py +0 -0
  30. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/__init__.py +0 -0
  31. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/_tiktoken_util.py +0 -0
  32. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/agent_run.py +0 -0
  33. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/chat/__init__.py +0 -0
  34. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/chat/content.py +0 -0
  35. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/chat/message.py +0 -0
  36. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/chat/response_format.py +0 -0
  37. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/chat/tool.py +0 -0
  38. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/citation.py +0 -0
  39. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/feedback.py +0 -0
  40. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/formatted_objects.py +0 -0
  41. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/judge.py +0 -0
  42. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/metadata_util.py +0 -0
  43. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/regex.py +0 -0
  44. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/transcript.py +0 -0
  45. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/data_models/util.py +0 -0
  46. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/__init__.py +0 -0
  47. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/analysis.py +0 -0
  48. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/impl.py +0 -0
  49. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/runner.py +0 -0
  50. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/stats.py +0 -0
  51. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/types.py +0 -0
  52. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/util/forgiving_json.py +0 -0
  53. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/util/meta_schema.json +0 -0
  54. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/util/meta_schema.py +0 -0
  55. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/util/parse_output.py +0 -0
  56. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/util/template_formatter.py +0 -0
  57. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/judges/util/voting.py +0 -0
  58. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/loaders/load_inspect.py +0 -0
  59. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/mcp/__init__.py +0 -0
  60. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/mcp/__main__.py +0 -0
  61. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/mcp/server.py +0 -0
  62. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/py.typed +0 -0
  63. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/samples/__init__.py +0 -0
  64. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/samples/load.py +0 -0
  65. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/samples/log.eval +0 -0
  66. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/samples/tb_airline.json +0 -0
  67. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/__init__.py +0 -0
  68. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_agent_runs.py +0 -0
  69. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_client_util.py +0 -0
  70. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_collections.py +0 -0
  71. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_feedback.py +0 -0
  72. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_labels.py +0 -0
  73. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_results.py +0 -0
  74. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_rubrics.py +0 -0
  75. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/_sharing.py +0 -0
  76. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/agent_run_writer.py +0 -0
  77. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/client.py +0 -0
  78. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/integrations/__init__.py +0 -0
  79. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/integrations/harbor.py +0 -0
  80. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/integrations/inspect.py +0 -0
  81. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/integrations/nemogym.py +0 -0
  82. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/integrations/util.py +0 -0
  83. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/llm_context.py +0 -0
  84. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/llm_request.py +0 -0
  85. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/sdk/util.py +0 -0
  86. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/trace.py +0 -0
  87. {docent_python-0.1.60a0 → docent_python-0.1.61a0}/docent/trace_temp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docent-python
3
- Version: 0.1.60a0
3
+ Version: 0.1.61a0
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
@@ -2,7 +2,7 @@ from datetime import datetime
2
2
  from typing import Annotated, Any, Literal, TypeAlias
3
3
  from uuid import uuid4
4
4
 
5
- from pydantic import BaseModel, Field
5
+ from pydantic import BaseModel, Field, model_validator
6
6
 
7
7
  from docent._llm_util.providers.preference_types import ModelOption
8
8
 
@@ -177,6 +177,7 @@ class ReadingStep(BaseModel):
177
177
  name: str | None = None
178
178
  reading_id: str | None = None
179
179
  dql_query: str | None = None
180
+ dql_step_alias: str | None = None
180
181
  prompt_template_segments: list[Any] | None = None
181
182
  context_config: dict[str, Any] | None = None
182
183
  model: ModelOption
@@ -191,7 +192,11 @@ class ReadingStep(BaseModel):
191
192
  def to_submission(self, *, dql_query: str | None = None) -> "ReadingStepSubmission":
192
193
  """Convert to a ReadingStepSubmission for resolve_reading_entry.
193
194
 
194
- Optionally overrides dql_query (e.g. after alias substitution).
195
+ Optionally overrides dql_query (e.g. after alias substitution). The
196
+ stored step's own dql_query may be None when dql_step_alias is set;
197
+ callers are expected to pass the resolved DQL explicitly in that case.
198
+ When a concrete DQL is supplied, clear dql_step_alias so the
199
+ submission continues to satisfy the "exactly one DQL source" contract.
195
200
  """
196
201
  return ReadingStepSubmission(
197
202
  alias=self.alias,
@@ -203,6 +208,7 @@ class ReadingStep(BaseModel):
203
208
  prompt_template_segments=self.prompt_template_segments,
204
209
  context_config=self.context_config,
205
210
  dql_query=dql_query if dql_query is not None else self.dql_query,
211
+ dql_step_alias=None if dql_query is not None else self.dql_step_alias,
206
212
  source_reading_preset_id=self.source_reading_preset_id,
207
213
  cache_mode=self.cache_mode,
208
214
  )
@@ -272,10 +278,29 @@ class ReadingStepSubmission(BaseModel):
272
278
  prompt_template_segments: list[Any] | None = None
273
279
  context_config: dict[str, ParameterContextConfig] | None = None
274
280
  dql_query: str | None = None
281
+ # References a DqlOnlyStep in the same plan whose rows feed this reading.
282
+ # Mutually exclusive with dql_query for template entries.
283
+ dql_step_alias: str | None = None
275
284
 
276
285
  # Scripted reading fields (mutually exclusive with template fields)
277
286
  requests: list[ScriptedRequest] | None = None
278
287
 
288
+ @model_validator(mode="after")
289
+ def _validate_dql_source(self) -> "ReadingStepSubmission":
290
+ if self.requests is not None:
291
+ if self.dql_query is not None or self.dql_step_alias is not None:
292
+ raise ValueError(
293
+ "Scripted reading submissions must not set dql_query or dql_step_alias"
294
+ )
295
+ return self
296
+ if self.dql_query is not None and self.dql_step_alias is not None:
297
+ raise ValueError("ReadingStepSubmission: set exactly one of dql_query / dql_step_alias")
298
+ if self.dql_query is None and self.dql_step_alias is None:
299
+ raise ValueError(
300
+ "ReadingStepSubmission: template entries must set one of dql_query / dql_step_alias"
301
+ )
302
+ return self
303
+
279
304
 
280
305
  class PresetReadingStepSubmission(BaseModel):
281
306
  entry_type: Literal["preset_reading"] = "preset_reading"
@@ -284,8 +309,21 @@ class PresetReadingStepSubmission(BaseModel):
284
309
  source_reading_preset_id: str
285
310
  user_metadata: dict[str, Any] | None = None
286
311
  dql_query: str | None = None
312
+ dql_step_alias: str | None = None
287
313
  cache_mode: ReadingCacheMode = "reading"
288
314
 
315
+ @model_validator(mode="after")
316
+ def _validate_dql_source(self) -> "PresetReadingStepSubmission":
317
+ if self.dql_query is not None and self.dql_step_alias is not None:
318
+ raise ValueError(
319
+ "PresetReadingStepSubmission: set exactly one of dql_query / dql_step_alias"
320
+ )
321
+ if self.dql_query is None and self.dql_step_alias is None:
322
+ raise ValueError(
323
+ "PresetReadingStepSubmission: must set one of dql_query / dql_step_alias"
324
+ )
325
+ return self
326
+
289
327
 
290
328
  class DqlOnlyStepSubmission(BaseModel):
291
329
  entry_type: Literal["dql_only"] = "dql_only"
@@ -174,7 +174,7 @@ class DocentBase:
174
174
  self._plan_id: str | None = None
175
175
  self._flushed_collection_id: str | None = None
176
176
  self._pending: list[PendingEntry] = []
177
- self._alias_counter: int = 0
177
+ self._alias_counter: int = 1
178
178
  self._auto_flush: bool = True
179
179
  self._atexit_registered: bool = False
180
180
  self._crash_detected: bool = False
@@ -156,20 +156,27 @@ class DocentDqlMixin(DocentBase):
156
156
 
157
157
  return agent_run_ids
158
158
 
159
- def list_agent_run_ids(self, collection_id: str) -> list[str]:
159
+ def list_agent_run_ids(
160
+ self,
161
+ collection_id: str,
162
+ *,
163
+ apply_base_filter: bool = False,
164
+ ) -> list[str]:
160
165
  """Get all agent run IDs for a collection.
161
166
 
162
167
  Args:
163
168
  collection_id: ID of the Collection.
169
+ apply_base_filter: Whether to apply the collection view's base filter.
164
170
 
165
171
  Returns:
166
- str: JSON string containing the list of agent run IDs.
172
+ list[str]: Agent run IDs for the collection.
167
173
 
168
174
  Raises:
169
175
  requests.exceptions.HTTPError: If the API request fails.
170
176
  """
171
177
  url = f"{self._api_url}/{collection_id}/agent_run_ids"
172
- response = self._session.get(url)
178
+ params = {"apply_base_filter": "true"} if apply_base_filter else None
179
+ response = self._session.get(url, params=params)
173
180
  self._handle_response_errors(response)
174
181
  return response.json()
175
182
 
@@ -158,8 +158,8 @@ class DocentReadingsMixin(DocentBase):
158
158
 
159
159
  # ────────────────────────── Reading Plans ──────────────────────────
160
160
 
161
- def _next_alias(self, prefix: str = "r") -> str:
162
- alias = f"{prefix}{self._alias_counter}"
161
+ def _next_alias(self) -> str:
162
+ alias = str(self._alias_counter)
163
163
  self._alias_counter += 1
164
164
  return alias
165
165
 
@@ -222,17 +222,39 @@ class DocentReadingsMixin(DocentBase):
222
222
  return Path(main_file).name
223
223
  return None
224
224
 
225
- def query(self, collection_id: str, dql: str) -> QueryResult:
226
- """Create a lazy DQL query handle.
225
+ def query(
226
+ self,
227
+ collection_id: str,
228
+ dql: str,
229
+ *,
230
+ name: str | None = None,
231
+ ) -> QueryResult:
232
+ """Create a lazy DQL query handle backed by a plan step.
233
+
234
+ A corresponding ``dql_only`` step is appended to the pending plan
235
+ immediately so the query is visible in the plan UI, even if the
236
+ QueryResult is never consumed by a reading.
227
237
 
228
238
  Args:
229
239
  collection_id: Collection ID.
230
240
  dql: DQL query string.
241
+ name: Optional display name for the dql-only step.
231
242
 
232
243
  Returns:
233
244
  QueryResult handle. Access attributes to get ColumnRef objects.
234
245
  """
235
- return QueryResult(collection_id, dedent(dql).strip())
246
+ alias = self._next_alias()
247
+ resolved_dql = dedent(dql).strip()
248
+ self._enqueue_pending(
249
+ _PendingDqlOnlyStep(
250
+ alias=alias,
251
+ collection_id=collection_id,
252
+ dql_query=resolved_dql,
253
+ name=name,
254
+ )
255
+ )
256
+ self._register_atexit()
257
+ return QueryResult(collection_id, resolved_dql, plan_alias=alias)
236
258
 
237
259
  def read(
238
260
  self,
@@ -289,11 +311,11 @@ class DocentReadingsMixin(DocentBase):
289
311
  provider=provider, model_name=model_name, reasoning_effort=reasoning_effort
290
312
  )
291
313
 
292
- alias = self._next_alias("r")
314
+ alias = self._next_alias()
293
315
  handle = Reading(client=self, alias=alias)
294
316
 
295
317
  if prompt_template is not None:
296
- template_segments, dql_query, ctx_config, inferred_cid = (
318
+ template_segments, dql_step_alias, ctx_config, inferred_cid = (
297
319
  self._serialize_template_prompt(prompt_template, context_config)
298
320
  )
299
321
  resolved_cid = collection_id or inferred_cid
@@ -310,7 +332,8 @@ class DocentReadingsMixin(DocentBase):
310
332
  cache_mode=cache_mode,
311
333
  prompt_template_segments=template_segments,
312
334
  context_config=ctx_config,
313
- dql_query=dql_query,
335
+ dql_query=None,
336
+ dql_step_alias=dql_step_alias,
314
337
  requests=None,
315
338
  )
316
339
  else:
@@ -330,6 +353,7 @@ class DocentReadingsMixin(DocentBase):
330
353
  prompt_template_segments=None,
331
354
  context_config=None,
332
355
  dql_query=None,
356
+ dql_step_alias=None,
333
357
  requests=scripted_reqs,
334
358
  )
335
359
 
@@ -344,28 +368,27 @@ class DocentReadingsMixin(DocentBase):
344
368
  ) -> tuple[list[Any], str | None, dict[str, ParameterContextConfig] | None, str | None]:
345
369
  """Convert prompt segments into template format.
346
370
 
347
- Returns (segments_json, dql_query, context_config_dict, collection_id).
371
+ Returns (segments_json, dql_step_alias, context_config_dict, collection_id).
372
+ The reading is bound to the single QueryResult used in the prompt via
373
+ its plan alias; if none is present the returned alias is ``None``.
348
374
  """
349
375
  segments: list[Any] = []
350
- dql_query: str | None = None
376
+ seen_qr: QueryResult | None = None
351
377
  inferred_collection_id: str | None = None
352
378
  param_configs: dict[str, ParameterContextConfig] = {}
353
- seen_query_results: dict[int, QueryResult] = {}
354
379
 
355
380
  for seg in prompt:
356
381
  if isinstance(seg, str):
357
382
  segments.append(dedent(seg))
358
383
  elif isinstance(seg, ColumnRef):
359
384
  qr = seg.query_result
360
- qr_id = id(qr)
361
- if qr_id not in seen_query_results:
362
- seen_query_results[qr_id] = qr
363
- if dql_query is not None and dql_query != qr.dql:
364
- raise ValueError(
365
- "All ColumnRefs in a prompt must reference the same QueryResult"
366
- )
367
- dql_query = qr.dql
385
+ if seen_qr is None:
386
+ seen_qr = qr
368
387
  inferred_collection_id = qr.collection_id
388
+ elif qr is not seen_qr:
389
+ raise ValueError(
390
+ "All ColumnRefs in a prompt must reference the same QueryResult"
391
+ )
369
392
 
370
393
  param_name = seg.column_name
371
394
  param_type = seg.type_annotation or "unknown"
@@ -392,7 +415,13 @@ class DocentReadingsMixin(DocentBase):
392
415
  segments[i] = segments[i].rstrip()
393
416
  break
394
417
 
395
- return segments, dql_query, param_configs if param_configs else None, inferred_collection_id
418
+ dql_step_alias = seen_qr.plan_alias if seen_qr is not None else None
419
+ return (
420
+ segments,
421
+ dql_step_alias,
422
+ param_configs if param_configs else None,
423
+ inferred_collection_id,
424
+ )
396
425
 
397
426
  def _serialize_scripted_requests(
398
427
  self,
@@ -434,29 +463,32 @@ class DocentReadingsMixin(DocentBase):
434
463
  return result
435
464
 
436
465
  def show_query_result(self, query_result: QueryResult, *, name: str | None = None) -> None:
437
- """Register a DQL-only preview step.
438
-
439
- The DQL results will be shown in the UI but not persisted.
466
+ """Deprecated: `client.query(...)` now auto-registers a dql-only step.
440
467
 
441
- Args:
442
- query_result: A QueryResult handle from client.query().
443
- name: Optional display name.
468
+ Retained as a no-op for backwards compatibility. If ``name`` is
469
+ provided and the query's dql-only step has no name yet, we update
470
+ it so the preview still gets a human-readable label.
444
471
  """
445
- alias = self._next_alias("d")
446
- self._enqueue_pending(
447
- _PendingDqlOnlyStep(
448
- alias=alias,
449
- collection_id=query_result.collection_id,
450
- dql_query=query_result.dql,
451
- name=name,
452
- )
472
+ import warnings
473
+
474
+ warnings.warn(
475
+ "show_query_result() is deprecated; client.query() already registers a "
476
+ "dql-only plan step. Pass name= to client.query() to label it.",
477
+ DeprecationWarning,
478
+ stacklevel=2,
453
479
  )
454
- self._register_atexit()
480
+ if name is None or query_result.plan_alias is None:
481
+ return
482
+ for p in self._pending:
483
+ if isinstance(p, _PendingDqlOnlyStep) and p.alias == query_result.plan_alias:
484
+ if p.name is None:
485
+ p.name = name
486
+ break
455
487
 
456
488
  def preset_reading(
457
489
  self,
458
490
  preset_id: str,
459
- query_result: QueryResult | None = None,
491
+ query_result: QueryResult | None,
460
492
  *,
461
493
  name: str | None = None,
462
494
  user_metadata: dict[str, Any] | None = None,
@@ -468,16 +500,19 @@ class DocentReadingsMixin(DocentBase):
468
500
 
469
501
  Args:
470
502
  preset_id: The reading preset ID to use.
471
- query_result: Optional QueryResult for the DQL query.
503
+ query_result: QueryResult supplying the preset's input rows.
472
504
  name: Optional display name for the step.
473
505
  user_metadata: Optional metadata stored on the reading row and used in reading deduplication.
474
506
 
475
507
  Returns:
476
508
  A Reading handle that will be populated with results after flush().
477
509
  """
478
- alias = self._next_alias("r")
479
- collection_id = query_result.collection_id if query_result else None
480
- dql_query = query_result.dql if query_result else None
510
+ if query_result is None:
511
+ raise ValueError("preset_reading requires a QueryResult")
512
+
513
+ alias = self._next_alias()
514
+ collection_id = query_result.collection_id
515
+ dql_step_alias = query_result.plan_alias
481
516
  handle = Reading(client=self, alias=alias)
482
517
  self._enqueue_pending(
483
518
  _PendingPresetReading(
@@ -487,7 +522,8 @@ class DocentReadingsMixin(DocentBase):
487
522
  name=name,
488
523
  source_reading_preset_id=preset_id,
489
524
  user_metadata=user_metadata,
490
- dql_query=dql_query,
525
+ dql_query=None,
526
+ dql_step_alias=dql_step_alias,
491
527
  cache_mode=cache_mode,
492
528
  )
493
529
  )
@@ -507,7 +543,7 @@ class DocentReadingsMixin(DocentBase):
507
543
  Args:
508
544
  label: The group label.
509
545
  """
510
- alias = self._next_alias("g")
546
+ alias = self._next_alias()
511
547
  self._pending.append(_PendingStepGroup(alias=alias, label=label))
512
548
  return StepGroupContext(self._pending, alias)
513
549
 
@@ -552,6 +588,7 @@ class DocentReadingsMixin(DocentBase):
552
588
  prompt_template_segments=p.prompt_template_segments,
553
589
  context_config=p.context_config,
554
590
  dql_query=p.dql_query,
591
+ dql_step_alias=p.dql_step_alias,
555
592
  requests=[ScriptedRequest(**r) for r in p.requests] if p.requests else None,
556
593
  )
557
594
  entries.append(entry)
@@ -565,6 +602,7 @@ class DocentReadingsMixin(DocentBase):
565
602
  source_reading_preset_id=p.source_reading_preset_id,
566
603
  user_metadata=p.user_metadata,
567
604
  dql_query=p.dql_query,
605
+ dql_step_alias=p.dql_step_alias,
568
606
  cache_mode=p.cache_mode,
569
607
  )
570
608
  )
@@ -675,6 +713,13 @@ class DocentReadingsMixin(DocentBase):
675
713
 
676
714
  _FLUSH_TIMEOUT_SECONDS = 600
677
715
 
716
+ @staticmethod
717
+ def _format_step_label(name: str | None, alias: str, default: str) -> str:
718
+ """Format a step label that always includes the alias."""
719
+ if name:
720
+ return f"{name} [${alias}]"
721
+ return f"{default} [${alias}]"
722
+
678
723
  def _preview_and_wait(
679
724
  self,
680
725
  *,
@@ -690,14 +735,17 @@ class DocentReadingsMixin(DocentBase):
690
735
  for es in submit_response.entry_statuses:
691
736
  if es.entry_type == "dql_only":
692
737
  if es.status == "cached" and es.dql_preview is not None:
693
- self._log_dql_preview(pending_names.get(es.alias), es.dql_preview)
738
+ self._log_dql_preview(pending_names.get(es.alias), es.alias, es.dql_preview)
694
739
  elif es.status != "cached":
695
740
  unsettled_dql_aliases[es.alias] = es
696
741
 
697
742
  elif es.entry_type == "reading":
698
743
  if es.status == "cached":
699
744
  self._log_reading_preview(
700
- pending_names.get(es.alias), es.result_count, es.result_preview
745
+ pending_names.get(es.alias),
746
+ es.alias,
747
+ es.result_count,
748
+ es.result_preview,
701
749
  )
702
750
  else:
703
751
  unsettled_reading_aliases.add(es.alias)
@@ -776,11 +824,15 @@ class DocentReadingsMixin(DocentBase):
776
824
  collection_id,
777
825
  step.reading_id,
778
826
  pending_names.get(step.alias),
827
+ step.alias,
779
828
  reading_handles.get(step.alias),
780
829
  )
781
830
  elif step.alias in pending and step.derived_status == "failed":
782
831
  self._logger.warning(
783
- "Step %s failed", pending_names.get(step.alias) or step.alias
832
+ "Step %s failed",
833
+ self._format_step_label(
834
+ pending_names.get(step.alias), step.alias, "Reading"
835
+ ),
784
836
  )
785
837
  pending.discard(step.alias)
786
838
  if not pending:
@@ -796,6 +848,7 @@ class DocentReadingsMixin(DocentBase):
796
848
  collection_id,
797
849
  event.reading_id,
798
850
  pending_names.get(event.step_alias),
851
+ event.step_alias,
799
852
  reading_handles.get(event.step_alias),
800
853
  result_count=event.result_count,
801
854
  )
@@ -809,9 +862,11 @@ class DocentReadingsMixin(DocentBase):
809
862
  )
810
863
 
811
864
  elif isinstance(event, PlanStepFailedEvent) and event.step_alias in pending:
812
- name = pending_names.get(event.step_alias, event.step_alias)
865
+ label = self._format_step_label(
866
+ pending_names.get(event.step_alias), event.step_alias, "Reading"
867
+ )
813
868
  msg = event.error.message if event.error else "unknown error"
814
- self._logger.warning("Step %s failed: %s", name or event.step_alias, msg)
869
+ self._logger.warning("Step %s failed: %s", label, msg)
815
870
  pending.discard(event.step_alias)
816
871
 
817
872
  elif isinstance(event, PlanJobCancelledEvent):
@@ -873,7 +928,7 @@ class DocentReadingsMixin(DocentBase):
873
928
  truncated=result.get("truncated", False),
874
929
  row_count=result.get("row_count", 0),
875
930
  )
876
- self._log_dql_preview(name, preview)
931
+ self._log_dql_preview(name, alias, preview)
877
932
  return result
878
933
  break
879
934
  except Exception:
@@ -885,11 +940,12 @@ class DocentReadingsMixin(DocentBase):
885
940
  collection_id: str,
886
941
  reading_id: str | None,
887
942
  name: str | None,
943
+ alias: str,
888
944
  handle: Reading | None,
889
945
  result_count: int | None = None,
890
946
  ) -> None:
891
947
  """Fetch results for a just-completed step, log preview, populate handle."""
892
- label = name or "Reading"
948
+ label = self._format_step_label(name, alias, "Reading")
893
949
  if reading_id is None:
894
950
  self._logger.info("%s: completed", label)
895
951
  return
@@ -913,14 +969,14 @@ class DocentReadingsMixin(DocentBase):
913
969
  else:
914
970
  self._logger.info("%s: completed%s", label, count_str)
915
971
 
916
- def _log_dql_preview(self, name: str | None, dql_preview: Any) -> None:
972
+ def _log_dql_preview(self, name: str | None, alias: str, dql_preview: Any) -> None:
917
973
  """Log a DQL result preview to stdout."""
918
974
  from docent.data_models.reading import DqlPreview
919
975
 
920
976
  if isinstance(dql_preview, dict):
921
977
  dql_preview = DqlPreview.model_validate(dql_preview)
922
978
 
923
- label = name or "DQL"
979
+ label = self._format_step_label(name, alias, "DQL")
924
980
  cols = dql_preview.columns
925
981
  rows = dql_preview.rows
926
982
  truncated = dql_preview.truncated
@@ -963,11 +1019,12 @@ class DocentReadingsMixin(DocentBase):
963
1019
  def _log_reading_preview(
964
1020
  self,
965
1021
  name: str | None,
1022
+ alias: str,
966
1023
  result_count: int | None,
967
1024
  result_preview: list[Any] | None,
968
1025
  ) -> None:
969
1026
  """Log a reading result preview to stdout."""
970
- label = name or "Reading"
1027
+ label = self._format_step_label(name, alias, "Reading")
971
1028
  count_str = f" ({result_count} results)" if result_count is not None else ""
972
1029
  self._logger.info("%s: cached%s", label, count_str)
973
1030
  if result_preview:
@@ -76,11 +76,16 @@ class QueryResult:
76
76
  """Lazy handle for a DQL query, returned by client.query().
77
77
 
78
78
  Attribute access returns ColumnRef objects for use in prompt templates.
79
+ Each QueryResult is bound to a dql-only step in the plan (identified by
80
+ `_plan_alias`); when a ColumnRef backed by this result is used in a
81
+ reading's prompt_template, that reading's submission references the
82
+ dql-only step via `dql_step_alias` rather than embedding the DQL directly.
79
83
  """
80
84
 
81
- def __init__(self, collection_id: str, dql: str) -> None:
85
+ def __init__(self, collection_id: str, dql: str, *, plan_alias: str | None = None) -> None:
82
86
  self._collection_id = collection_id
83
87
  self._dql = dql
88
+ self._plan_alias = plan_alias
84
89
 
85
90
  @property
86
91
  def collection_id(self) -> str:
@@ -90,6 +95,10 @@ class QueryResult:
90
95
  def dql(self) -> str:
91
96
  return self._dql
92
97
 
98
+ @property
99
+ def plan_alias(self) -> str | None:
100
+ return self._plan_alias
101
+
93
102
  def __getattr__(self, name: str) -> ColumnRef:
94
103
  if name.startswith("_"):
95
104
  raise AttributeError(name)
@@ -204,6 +213,7 @@ class _PendingReading:
204
213
  prompt_template_segments: list[Any] | None,
205
214
  context_config: dict[str, ParameterContextConfig] | None,
206
215
  dql_query: str | None,
216
+ dql_step_alias: str | None = None,
207
217
  # Scripted path
208
218
  requests: list[dict[str, Any]] | None,
209
219
  ) -> None:
@@ -220,6 +230,7 @@ class _PendingReading:
220
230
  self.prompt_template_segments = prompt_template_segments
221
231
  self.context_config = context_config
222
232
  self.dql_query = dql_query
233
+ self.dql_step_alias = dql_step_alias
223
234
  self.requests = requests
224
235
 
225
236
 
@@ -248,6 +259,7 @@ class _PendingPresetReading:
248
259
  source_reading_preset_id: str,
249
260
  user_metadata: dict[str, Any] | None,
250
261
  dql_query: str | None,
262
+ dql_step_alias: str | None = None,
251
263
  cache_mode: ReadingCacheMode = "reading",
252
264
  ) -> None:
253
265
  self.alias = alias
@@ -257,6 +269,7 @@ class _PendingPresetReading:
257
269
  self.source_reading_preset_id = source_reading_preset_id
258
270
  self.user_metadata = user_metadata
259
271
  self.dql_query = dql_query
272
+ self.dql_step_alias = dql_step_alias
260
273
  self.cache_mode: ReadingCacheMode = cache_mode
261
274
 
262
275
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "docent-python"
3
3
  description = "Docent SDK"
4
- version = "0.1.60-alpha"
4
+ version = "0.1.61-alpha"
5
5
  authors = [
6
6
  { name="Transluce", email="info@transluce.org" },
7
7
  ]
@@ -553,7 +553,7 @@ wheels = [
553
553
 
554
554
  [[package]]
555
555
  name = "docent-python"
556
- version = "0.1.60a0"
556
+ version = "0.1.61a0"
557
557
  source = { editable = "." }
558
558
  dependencies = [
559
559
  { name = "anthropic" },