docent-python 0.1.59a0__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 (88) hide show
  1. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/PKG-INFO +1 -1
  2. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/reading.py +178 -4
  3. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/mcp/server.py +13 -3
  4. docent_python-0.1.61a0/docent/sdk/_agent_runs.py +217 -0
  5. docent_python-0.1.61a0/docent/sdk/_base.py +299 -0
  6. docent_python-0.1.61a0/docent/sdk/_client_util.py +141 -0
  7. docent_python-0.1.61a0/docent/sdk/_collections.py +421 -0
  8. docent_python-0.1.61a0/docent/sdk/_dql.py +304 -0
  9. docent_python-0.1.61a0/docent/sdk/_feedback.py +157 -0
  10. docent_python-0.1.61a0/docent/sdk/_labels.py +225 -0
  11. docent_python-0.1.61a0/docent/sdk/_readings.py +1178 -0
  12. docent_python-0.1.61a0/docent/sdk/_results.py +311 -0
  13. docent_python-0.1.61a0/docent/sdk/_rubrics.py +320 -0
  14. docent_python-0.1.61a0/docent/sdk/_sharing.py +229 -0
  15. docent_python-0.1.61a0/docent/sdk/client.py +45 -0
  16. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/reading.py +20 -5
  17. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/pyproject.toml +1 -1
  18. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/uv.lock +1 -1
  19. docent_python-0.1.59a0/docent/sdk/client.py +0 -3281
  20. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/.gitignore +0 -0
  21. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/LICENSE.md +0 -0
  22. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/README.md +0 -0
  23. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/__init__.py +0 -0
  24. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/__init__.py +0 -0
  25. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/data_models/__init__.py +0 -0
  26. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/data_models/exceptions.py +0 -0
  27. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/data_models/llm_output.py +0 -0
  28. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/llm_cache.py +0 -0
  29. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/llm_svc.py +0 -0
  30. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/model_registry.py +0 -0
  31. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/__init__.py +0 -0
  32. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/anthropic.py +0 -0
  33. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/common.py +0 -0
  34. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/google.py +0 -0
  35. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/openai.py +0 -0
  36. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/openrouter.py +0 -0
  37. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/preference_types.py +0 -0
  38. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_llm_util/providers/provider_registry.py +0 -0
  39. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_log_util/__init__.py +0 -0
  40. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/_log_util/logger.py +0 -0
  41. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/__init__.py +0 -0
  42. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/_tiktoken_util.py +0 -0
  43. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/agent_run.py +0 -0
  44. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/chat/__init__.py +0 -0
  45. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/chat/content.py +0 -0
  46. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/chat/message.py +0 -0
  47. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/chat/response_format.py +0 -0
  48. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/chat/tool.py +0 -0
  49. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/citation.py +0 -0
  50. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/feedback.py +0 -0
  51. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/formatted_objects.py +0 -0
  52. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/judge.py +0 -0
  53. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/metadata_util.py +0 -0
  54. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/regex.py +0 -0
  55. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/transcript.py +0 -0
  56. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/data_models/util.py +0 -0
  57. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/__init__.py +0 -0
  58. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/analysis.py +0 -0
  59. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/impl.py +0 -0
  60. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/runner.py +0 -0
  61. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/stats.py +0 -0
  62. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/types.py +0 -0
  63. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/util/forgiving_json.py +0 -0
  64. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/util/meta_schema.json +0 -0
  65. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/util/meta_schema.py +0 -0
  66. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/util/parse_output.py +0 -0
  67. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/util/template_formatter.py +0 -0
  68. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/judges/util/voting.py +0 -0
  69. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/loaders/load_inspect.py +0 -0
  70. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/mcp/__init__.py +0 -0
  71. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/mcp/__main__.py +0 -0
  72. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/py.typed +0 -0
  73. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/samples/__init__.py +0 -0
  74. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/samples/load.py +0 -0
  75. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/samples/log.eval +0 -0
  76. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/samples/tb_airline.json +0 -0
  77. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/__init__.py +0 -0
  78. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/agent_run_writer.py +0 -0
  79. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/integrations/__init__.py +0 -0
  80. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/integrations/harbor.py +0 -0
  81. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/integrations/inspect.py +0 -0
  82. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/integrations/nemogym.py +0 -0
  83. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/integrations/util.py +0 -0
  84. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/llm_context.py +0 -0
  85. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/llm_request.py +0 -0
  86. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/sdk/util.py +0 -0
  87. {docent_python-0.1.59a0 → docent_python-0.1.61a0}/docent/trace.py +0 -0
  88. {docent_python-0.1.59a0 → 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.59a0
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
@@ -1,8 +1,8 @@
1
1
  from datetime import datetime
2
- from typing import Any, Literal, TypeAlias
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
 
@@ -41,6 +41,7 @@ prompt segments, model config, output schema, and user-supplied arguments.
41
41
  re-evaluation.
42
42
  """
43
43
  ReadingCacheMode = Literal["reading", "results", "none"]
44
+ ReadingStatus = Literal["completed", "failed", "pending", "cached", "needs_approval", "unresolved"]
44
45
 
45
46
 
46
47
  class ContextFilterSection(BaseModel):
@@ -73,6 +74,7 @@ class ReadingPreset(BaseModel):
73
74
  collection_id: str
74
75
  name: str
75
76
  created_at: datetime | None = None
77
+ created_by: str | None = None
76
78
  updated_at: datetime | None = None
77
79
 
78
80
 
@@ -117,6 +119,7 @@ class Reading(BaseModel):
117
119
  user_metadata: dict[str, Any] | None = None
118
120
  source_reading_preset_id: str | None = None
119
121
  created_at: datetime | None = None
122
+ created_by: str | None = None
120
123
 
121
124
 
122
125
  class ReadingResult(BaseModel):
@@ -174,6 +177,7 @@ class ReadingStep(BaseModel):
174
177
  name: str | None = None
175
178
  reading_id: str | None = None
176
179
  dql_query: str | None = None
180
+ dql_step_alias: str | None = None
177
181
  prompt_template_segments: list[Any] | None = None
178
182
  context_config: dict[str, Any] | None = None
179
183
  model: ModelOption
@@ -188,7 +192,11 @@ class ReadingStep(BaseModel):
188
192
  def to_submission(self, *, dql_query: str | None = None) -> "ReadingStepSubmission":
189
193
  """Convert to a ReadingStepSubmission for resolve_reading_entry.
190
194
 
191
- 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.
192
200
  """
193
201
  return ReadingStepSubmission(
194
202
  alias=self.alias,
@@ -200,6 +208,7 @@ class ReadingStep(BaseModel):
200
208
  prompt_template_segments=self.prompt_template_segments,
201
209
  context_config=self.context_config,
202
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,
203
212
  source_reading_preset_id=self.source_reading_preset_id,
204
213
  cache_mode=self.cache_mode,
205
214
  )
@@ -226,6 +235,7 @@ class ReadingPlan(BaseModel):
226
235
  name: str | None = None
227
236
  steps: list[PlanStep] = Field(default_factory=list) # type: ignore[reportUnknownVariableType]
228
237
  created_at: datetime | None = None
238
+ created_by: str | None = None
229
239
  updated_at: datetime | None = None
230
240
 
231
241
 
@@ -268,10 +278,29 @@ class ReadingStepSubmission(BaseModel):
268
278
  prompt_template_segments: list[Any] | None = None
269
279
  context_config: dict[str, ParameterContextConfig] | None = None
270
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
271
284
 
272
285
  # Scripted reading fields (mutually exclusive with template fields)
273
286
  requests: list[ScriptedRequest] | None = None
274
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
+
275
304
 
276
305
  class PresetReadingStepSubmission(BaseModel):
277
306
  entry_type: Literal["preset_reading"] = "preset_reading"
@@ -280,8 +309,21 @@ class PresetReadingStepSubmission(BaseModel):
280
309
  source_reading_preset_id: str
281
310
  user_metadata: dict[str, Any] | None = None
282
311
  dql_query: str | None = None
312
+ dql_step_alias: str | None = None
283
313
  cache_mode: ReadingCacheMode = "reading"
284
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
+
285
327
 
286
328
  class DqlOnlyStepSubmission(BaseModel):
287
329
  entry_type: Literal["dql_only"] = "dql_only"
@@ -306,23 +348,140 @@ class PlanSubmissionRequest(BaseModel):
306
348
  entries: list[PlanStepSubmission]
307
349
 
308
350
 
351
+ class DqlPreview(BaseModel):
352
+ columns: list[str]
353
+ rows: list[list[Any]]
354
+ truncated: bool
355
+ row_count: int
356
+
357
+
358
+ class ReadingResultPreview(BaseModel):
359
+ id: str
360
+ output: dict[str, Any] | None = None
361
+ error: dict[str, Any] | None = None
362
+
363
+
309
364
  class PlanStepSubmissionStatus(BaseModel):
310
365
  alias: str
311
- status: Literal["cached", "needs_approval", "unresolved"]
366
+ entry_type: str
367
+ status: ReadingStatus
312
368
  reading_id: str | None = None
369
+ result_count: int | None = None
370
+ dql_preview: DqlPreview | None = None
371
+ result_preview: list[ReadingResultPreview] | None = None
313
372
 
314
373
 
315
374
  class PlanSubmissionResponse(BaseModel):
316
375
  plan_id: str
376
+ plan_name: str | None = None
377
+ previous_latest_plan_id: str | None = None
378
+ has_active_listeners: bool = False
317
379
  entry_statuses: list[PlanStepSubmissionStatus]
318
380
 
319
381
 
382
+ # ── Plan SSE stream events (server → SDK) ────────────────────────────
383
+
384
+
385
+ class PlanStreamStepStatus(BaseModel):
386
+ """Minimal step shape carried inside a snapshot event."""
387
+
388
+ alias: str
389
+ reading_id: str | None = None
390
+ derived_status: str
391
+
392
+
393
+ class PlanSnapshotEvent(BaseModel):
394
+ type: Literal["snapshot"] = "snapshot"
395
+ steps: list[PlanStreamStepStatus]
396
+
397
+
398
+ class PlanStepError(BaseModel):
399
+ message: str
400
+
401
+
402
+ class PlanStepStartedEvent(BaseModel):
403
+ type: Literal["step_started"] = "step_started"
404
+ plan_id: str
405
+ step_alias: str
406
+ job_id: str
407
+ reading_id: str
408
+
409
+
410
+ class PlanStepCompletedEvent(BaseModel):
411
+ type: Literal["step_completed"] = "step_completed"
412
+ plan_id: str
413
+ step_alias: str
414
+ job_id: str
415
+ reading_id: str
416
+ result_count: int | None = None
417
+
418
+
419
+ class PlanStepFailedEvent(BaseModel):
420
+ type: Literal["step_failed"] = "step_failed"
421
+ plan_id: str
422
+ step_alias: str
423
+ job_id: str
424
+ error: PlanStepError | None = None
425
+
426
+
427
+ class PlanStepsUpdatedEvent(BaseModel):
428
+ type: Literal["steps_updated"] = "steps_updated"
429
+ plan_id: str
430
+
431
+
432
+ class PlanJobStartedEvent(BaseModel):
433
+ type: Literal["job_started"] = "job_started"
434
+ plan_id: str
435
+ job_id: str
436
+
437
+
438
+ class PlanJobCompletedEvent(BaseModel):
439
+ type: Literal["job_completed"] = "job_completed"
440
+ plan_id: str
441
+ job_id: str
442
+
443
+
444
+ class PlanJobFailedEvent(BaseModel):
445
+ type: Literal["job_failed"] = "job_failed"
446
+ plan_id: str
447
+ job_id: str
448
+ error: PlanStepError | None = None
449
+
450
+
451
+ class PlanSupersededEvent(BaseModel):
452
+ type: Literal["plan_superseded"] = "plan_superseded"
453
+ plan_id: str
454
+ superseded_by_plan_id: str
455
+ name: str | None = None
456
+
457
+
458
+ class PlanJobCancelledEvent(BaseModel):
459
+ type: Literal["job_cancelled"] = "job_cancelled"
460
+ plan_id: str
461
+
462
+
463
+ PlanStreamEvent: TypeAlias = Annotated[
464
+ PlanSnapshotEvent
465
+ | PlanStepStartedEvent
466
+ | PlanStepCompletedEvent
467
+ | PlanStepFailedEvent
468
+ | PlanStepsUpdatedEvent
469
+ | PlanJobStartedEvent
470
+ | PlanJobCompletedEvent
471
+ | PlanJobFailedEvent
472
+ | PlanJobCancelledEvent
473
+ | PlanSupersededEvent,
474
+ Field(discriminator="type"),
475
+ ]
476
+
477
+
320
478
  __all__ = [
321
479
  "AnnotatableReadingParamType",
322
480
  "BeginGroupStep",
323
481
  "ContextFilterSection",
324
482
  "DqlOnlyStep",
325
483
  "DqlOnlyStepSubmission",
484
+ "DqlPreview",
326
485
  "EndGroupStep",
327
486
  "EndStepGroupSubmission",
328
487
  "ScriptedRequest",
@@ -335,6 +494,8 @@ __all__ = [
335
494
  "ReadingCacheMode",
336
495
  "ReadingParamPlaceholder",
337
496
  "ReadingParamType",
497
+ "ReadingResultPreview",
498
+ "ReadingStatus",
338
499
  "ReadingStep",
339
500
  "ReadingStepSubmission",
340
501
  "ReadingTemplateSegment",
@@ -346,4 +507,17 @@ __all__ = [
346
507
  "ReadingResult",
347
508
  "StepGroupSubmission",
348
509
  "PresetReadingStepSubmission",
510
+ "PlanStreamEvent",
511
+ "PlanStreamStepStatus",
512
+ "PlanSnapshotEvent",
513
+ "PlanStepStartedEvent",
514
+ "PlanStepCompletedEvent",
515
+ "PlanStepError",
516
+ "PlanStepFailedEvent",
517
+ "PlanStepsUpdatedEvent",
518
+ "PlanJobStartedEvent",
519
+ "PlanJobCompletedEvent",
520
+ "PlanJobFailedEvent",
521
+ "PlanJobCancelledEvent",
522
+ "PlanSupersededEvent",
349
523
  ]
@@ -33,10 +33,13 @@ def get_metadata_fields(collection_id: str) -> str:
33
33
  """
34
34
  client = get_client()
35
35
  try:
36
- fields = client.get_metadata_fields(
36
+ response = client.get_metadata_fields(
37
37
  collection_id, include_sample_values=True, sample_limit=10
38
38
  )
39
39
 
40
+ fields = response.get("fields", [])
41
+ total_runs = response.get("total_runs")
42
+
40
43
  if not fields:
41
44
  return f"No metadata fields found for collection {collection_id}"
42
45
 
@@ -71,7 +74,10 @@ def get_metadata_fields(collection_id: str) -> str:
71
74
  lines.append(line)
72
75
 
73
76
  field_list = "\n".join(lines)
74
- return f"Metadata fields for collection {collection_id}:\n{field_list}"
77
+ tool_output = f"Metadata fields for collection {collection_id}:\n{field_list}"
78
+ if total_runs is not None:
79
+ tool_output += f"\n\nTotal runs: {total_runs}"
80
+ return tool_output
75
81
  except Exception as e:
76
82
  error_msg = str(e)
77
83
  if "404" in error_msg:
@@ -321,7 +327,11 @@ def get_reading_plan_results(
321
327
  for step in steps:
322
328
  if step.get("type") == "reading" and step.get("reading_id"):
323
329
  try:
324
- results = client.get_reading_results(collection_id, step["reading_id"])
330
+ results = client.get_reading_results(
331
+ collection_id,
332
+ step["reading_id"],
333
+ include_output=False,
334
+ )
325
335
  result_counts[step["alias"]] = len(results)
326
336
  except Exception:
327
337
  pass
@@ -0,0 +1,217 @@
1
+ """Agent run fetch, metadata, transcript groups, and chat sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from docent.data_models.agent_run import AgentRun
8
+ from docent.sdk._base import DocentBase
9
+
10
+
11
+ class DocentAgentRunsMixin(DocentBase):
12
+ """Agent run and transcript-group operations."""
13
+
14
+ def get_agent_run(self, collection_id: str, agent_run_id: str) -> AgentRun | None:
15
+ """Get a specific agent run by its ID.
16
+
17
+ Args:
18
+ collection_id: ID of the Collection.
19
+ agent_run_id: The ID of the agent run to retrieve.
20
+
21
+ Returns:
22
+ dict: Dictionary containing the agent run information.
23
+
24
+ Raises:
25
+ requests.exceptions.HTTPError: If the API request fails.
26
+ """
27
+ url = f"{self._api_url}/{collection_id}/agent_run"
28
+ response = self._session.get(url, params={"agent_run_id": agent_run_id})
29
+ self._handle_response_errors(response)
30
+ if response.json() is None:
31
+ return None
32
+ else:
33
+ # We do this to avoid metadata validation failing
34
+ # TODO(mengk): kinda hacky
35
+ return AgentRun.model_validate(response.json())
36
+
37
+ def update_agent_run_metadata(
38
+ self,
39
+ collection_id: str,
40
+ agent_run_id: str,
41
+ metadata: dict[str, Any],
42
+ ) -> dict[str, Any]:
43
+ """Merge metadata into an agent run's existing metadata.
44
+
45
+ Uses a deep merge: nested dictionaries are merged recursively so
46
+ existing keys are preserved, while non-dict values are overwritten.
47
+ Keys not present in ``metadata`` are left unchanged.
48
+
49
+ Requires WRITE permission on the collection.
50
+
51
+ Args:
52
+ collection_id: ID of the Collection containing the agent run.
53
+ agent_run_id: ID of the agent run to update.
54
+ metadata: Dictionary of metadata fields to merge.
55
+
56
+ Returns:
57
+ The full merged metadata dictionary after the update.
58
+
59
+ Raises:
60
+ requests.exceptions.HTTPError: If the API request fails (e.g., 404 if the agent run is not found).
61
+ """
62
+ url = f"{self._server_url}/{collection_id}/agent_run/{agent_run_id}/metadata"
63
+ response = self._session.put(url, json={"metadata": metadata})
64
+ self._handle_response_errors(response)
65
+ data: dict[str, Any] = response.json()
66
+ return data
67
+
68
+ def delete_agent_run_metadata_keys(
69
+ self,
70
+ collection_id: str,
71
+ agent_run_id: str,
72
+ keys: list[str],
73
+ ) -> tuple[dict[str, Any], list[str]]:
74
+ """Remove keys from an agent run's metadata.
75
+
76
+ Supports dot-delimited paths for nested deletion. For example,
77
+ ``"config.model"`` removes the ``model`` key inside ``config``
78
+ without affecting other keys in that dict.
79
+
80
+ Requires WRITE permission on the collection.
81
+
82
+ Args:
83
+ collection_id: ID of the Collection containing the agent run.
84
+ agent_run_id: ID of the agent run to modify.
85
+ keys: Metadata keys to remove. Use dot-delimited paths for nested
86
+ keys (e.g. ``["top_level_key", "nested.child_key"]``).
87
+
88
+ Returns:
89
+ A tuple of (metadata after deletion, list of keys that were not found).
90
+
91
+ Raises:
92
+ requests.exceptions.HTTPError: If the API request fails (e.g., 404 if the agent run is not found).
93
+ """
94
+ url = f"{self._server_url}/{collection_id}/agent_run/{agent_run_id}/metadata/delete"
95
+ response = self._session.post(url, json={"keys": keys})
96
+ self._handle_response_errors(response)
97
+ data: dict[str, Any] = response.json()
98
+ metadata: dict[str, Any] = data["metadata"]
99
+ not_found: list[str] = data["not_found"]
100
+ return metadata, not_found
101
+
102
+ def get_agent_run_metadata(
103
+ self,
104
+ collection_id: str,
105
+ agent_run_id: str,
106
+ ) -> dict[str, Any]:
107
+ """Get an agent run's metadata.
108
+
109
+ Args:
110
+ collection_id: ID of the Collection containing the agent run.
111
+ agent_run_id: ID of the agent run.
112
+
113
+ Returns:
114
+ The agent run's metadata dict.
115
+
116
+ Raises:
117
+ requests.exceptions.HTTPError: If the API request fails.
118
+ """
119
+ url = f"{self._server_url}/{collection_id}/agent_run/{agent_run_id}/metadata"
120
+ response = self._session.get(url)
121
+ self._handle_response_errors(response)
122
+ return response.json()
123
+
124
+ # ──────────────────────────────────────────
125
+ # Transcript group metadata
126
+ # ──────────────────────────────────────────
127
+
128
+ def get_transcript_group_metadata(
129
+ self,
130
+ collection_id: str,
131
+ transcript_group_id: str,
132
+ ) -> dict[str, Any]:
133
+ """Get a transcript group's metadata.
134
+
135
+ Args:
136
+ collection_id: ID of the Collection containing the transcript group.
137
+ transcript_group_id: ID of the transcript group.
138
+
139
+ Returns:
140
+ The transcript group's metadata dict.
141
+
142
+ Raises:
143
+ requests.exceptions.HTTPError: If the API request fails.
144
+ """
145
+ url = f"{self._server_url}/{collection_id}/transcript_group/{transcript_group_id}/metadata"
146
+ response = self._session.get(url)
147
+ self._handle_response_errors(response)
148
+ return response.json()
149
+
150
+ def update_transcript_group_metadata(
151
+ self,
152
+ collection_id: str,
153
+ transcript_group_id: str,
154
+ metadata: dict[str, Any],
155
+ ) -> dict[str, Any]:
156
+ """Deep-merge metadata into a transcript group's existing metadata.
157
+
158
+ Args:
159
+ collection_id: ID of the Collection containing the transcript group.
160
+ transcript_group_id: ID of the transcript group.
161
+ metadata: Metadata dict to merge into the existing metadata.
162
+
163
+ Returns:
164
+ The full merged metadata dict.
165
+
166
+ Raises:
167
+ requests.exceptions.HTTPError: If the API request fails.
168
+ """
169
+ url = f"{self._server_url}/{collection_id}/transcript_group/{transcript_group_id}/metadata"
170
+ response = self._session.put(url, json={"metadata": metadata})
171
+ self._handle_response_errors(response)
172
+ return response.json()
173
+
174
+ def delete_transcript_group_metadata_keys(
175
+ self,
176
+ collection_id: str,
177
+ transcript_group_id: str,
178
+ keys: list[str],
179
+ ) -> tuple[dict[str, Any], list[str]]:
180
+ """Remove keys from a transcript group's metadata.
181
+
182
+ Supports dot-delimited paths for nested deletion.
183
+
184
+ Args:
185
+ collection_id: ID of the Collection containing the transcript group.
186
+ transcript_group_id: ID of the transcript group.
187
+ keys: Keys to remove. Use dot notation for nested keys.
188
+
189
+ Returns:
190
+ Tuple of (metadata after deletion, list of keys that were not found).
191
+
192
+ Raises:
193
+ requests.exceptions.HTTPError: If the API request fails.
194
+ """
195
+ url = f"{self._server_url}/{collection_id}/transcript_group/{transcript_group_id}/metadata/delete"
196
+ response = self._session.post(url, json={"keys": keys})
197
+ self._handle_response_errors(response)
198
+ data = response.json()
199
+ return data["metadata"], data["not_found"]
200
+
201
+ def get_chat_sessions(self, collection_id: str, agent_run_id: str) -> list[dict[str, Any]]:
202
+ """Get all chat sessions for an agent run, excluding judge result sessions.
203
+
204
+ Args:
205
+ collection_id: ID of the Collection.
206
+ agent_run_id: The ID of the agent run to retrieve chat sessions for.
207
+
208
+ Returns:
209
+ list: List of chat session dictionaries.
210
+
211
+ Raises:
212
+ requests.exceptions.HTTPError: If the API request fails.
213
+ """
214
+ url = f"{self._api_url}/chat/{collection_id}/{agent_run_id}/sessions"
215
+ response = self._session.get(url)
216
+ self._handle_response_errors(response)
217
+ return response.json()