docent-python 0.1.13a0__tar.gz → 0.1.15a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of docent-python might be problematic. Click here for more details.

Files changed (37) hide show
  1. docent_python-0.1.15a0/LICENSE.md +13 -0
  2. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/PKG-INFO +4 -2
  3. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/__init__.py +1 -1
  4. docent_python-0.1.15a0/docent/data_models/agent_run.py +468 -0
  5. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/remove_invalid_citation_ranges.py +3 -6
  6. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/transcript.py +59 -37
  7. docent_python-0.1.15a0/docent/data_models/yaml_util.py +12 -0
  8. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/loaders/load_inspect.py +15 -10
  9. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/sdk/client.py +90 -46
  10. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/trace.py +4 -2
  11. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/pyproject.toml +4 -2
  12. docent_python-0.1.15a0/uv.lock +2239 -0
  13. docent_python-0.1.13a0/LICENSE.md +0 -7
  14. docent_python-0.1.13a0/docent/data_models/agent_run.py +0 -299
  15. docent_python-0.1.13a0/uv.lock +0 -954
  16. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/.gitignore +0 -0
  17. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/README.md +0 -0
  18. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/_log_util/__init__.py +0 -0
  19. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/_log_util/logger.py +0 -0
  20. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/__init__.py +0 -0
  21. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/_tiktoken_util.py +0 -0
  22. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/chat/__init__.py +0 -0
  23. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/chat/content.py +0 -0
  24. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/chat/message.py +0 -0
  25. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/chat/tool.py +0 -0
  26. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/citation.py +0 -0
  27. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/metadata.py +0 -0
  28. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/regex.py +0 -0
  29. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/data_models/shared_types.py +0 -0
  30. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/py.typed +0 -0
  31. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/samples/__init__.py +0 -0
  32. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/samples/load.py +0 -0
  33. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/samples/log.eval +0 -0
  34. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/samples/tb_airline.json +0 -0
  35. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/sdk/__init__.py +0 -0
  36. {docent_python-0.1.13a0/docent → docent_python-0.1.15a0/docent/sdk}/agent_run_writer.py +0 -0
  37. {docent_python-0.1.13a0 → docent_python-0.1.15a0}/docent/trace_temp.py +0 -0
@@ -0,0 +1,13 @@
1
+ Copyright 2025 Clarity AI Research Inc., dba Transluce
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -1,14 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docent-python
3
- Version: 0.1.13a0
3
+ Version: 0.1.15a0
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
7
7
  Project-URL: Docs, https://transluce-docent.readthedocs-hosted.com/en/latest
8
8
  Author-email: Transluce <info@transluce.org>
9
- License-Expression: MIT
9
+ License-Expression: Apache-2.0
10
10
  License-File: LICENSE.md
11
11
  Requires-Python: >=3.11
12
+ Requires-Dist: backoff>=2.2.1
13
+ Requires-Dist: inspect-ai>=0.3.132
12
14
  Requires-Dist: opentelemetry-api>=1.34.1
13
15
  Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.34.1
14
16
  Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.34.1
@@ -1,4 +1,4 @@
1
1
  __all__ = ["Docent", "init"]
2
2
 
3
- from docent.agent_run_writer import init
3
+ from docent.sdk.agent_run_writer import init
4
4
  from docent.sdk.client import Docent
@@ -0,0 +1,468 @@
1
+ import sys
2
+ import textwrap
3
+ from datetime import datetime
4
+ from queue import Queue
5
+ from typing import Any, Literal, TypedDict, cast
6
+ from uuid import uuid4
7
+
8
+ import yaml
9
+ from pydantic import (
10
+ BaseModel,
11
+ Field,
12
+ PrivateAttr,
13
+ field_validator,
14
+ model_validator,
15
+ )
16
+ from pydantic_core import to_jsonable_python
17
+
18
+ from docent._log_util import get_logger
19
+ from docent.data_models._tiktoken_util import get_token_count, group_messages_into_ranges
20
+ from docent.data_models.transcript import Transcript, TranscriptGroup
21
+ from docent.data_models.yaml_util import yaml_dump_metadata
22
+
23
+ logger = get_logger(__name__)
24
+
25
+
26
+ class FilterableField(TypedDict):
27
+ name: str
28
+ type: Literal["str", "bool", "int", "float"]
29
+
30
+
31
+ class AgentRun(BaseModel):
32
+ """Represents a complete run of an agent with transcripts and metadata.
33
+
34
+ An AgentRun encapsulates the execution of an agent, storing all communication
35
+ transcripts and associated metadata. It must contain at least one transcript.
36
+
37
+ Attributes:
38
+ id: Unique identifier for the agent run, auto-generated by default.
39
+ name: Optional human-readable name for the agent run.
40
+ description: Optional description of the agent run.
41
+ transcripts: List of Transcript objects.
42
+ transcript_groups: List of TranscriptGroup objects.
43
+ metadata: Additional structured metadata about the agent run as a JSON-serializable dictionary.
44
+ """
45
+
46
+ id: str = Field(default_factory=lambda: str(uuid4()))
47
+ name: str | None = None
48
+ description: str | None = None
49
+
50
+ transcripts: list[Transcript]
51
+ transcript_groups: list[TranscriptGroup] = Field(default_factory=list)
52
+ metadata: dict[str, Any] = Field(default_factory=dict)
53
+
54
+ @field_validator("transcripts", mode="before")
55
+ @classmethod
56
+ def _validate_transcripts_type(cls, v: Any) -> Any:
57
+ if isinstance(v, dict):
58
+ logger.warning(
59
+ "dict[str, Transcript] for transcripts is deprecated. Use list[Transcript] instead."
60
+ )
61
+ v = cast(dict[str, Transcript], v)
62
+ return [Transcript.model_validate(t) for t in v.values()]
63
+ return v
64
+
65
+ @field_validator("transcript_groups", mode="before")
66
+ @classmethod
67
+ def _validate_transcript_groups_type(cls, v: Any) -> Any:
68
+ if isinstance(v, dict):
69
+ logger.warning(
70
+ "dict[str, TranscriptGroup] for transcript_groups is deprecated. Use list[TranscriptGroup] instead."
71
+ )
72
+ v = cast(dict[str, TranscriptGroup], v)
73
+ return [TranscriptGroup.model_validate(tg) for tg in v.values()]
74
+ return v
75
+
76
+ @model_validator(mode="after")
77
+ def _validate_transcripts_not_empty(self):
78
+ """Validates that the agent run contains at least one transcript.
79
+
80
+ Raises:
81
+ ValueError: If the transcripts list is empty.
82
+
83
+ Returns:
84
+ AgentRun: The validated AgentRun instance.
85
+ """
86
+ if len(self.transcripts) == 0:
87
+ raise ValueError("AgentRun must have at least one transcript")
88
+ return self
89
+
90
+ def get_filterable_fields(self, max_depth: int = 1) -> list[FilterableField]:
91
+ """Returns a list of all fields that can be used to filter the agent run,
92
+ by recursively exploring the model_dump() for singleton types in dictionaries.
93
+
94
+ Returns:
95
+ list[FilterableField]: A list of filterable fields, where each field is a
96
+ dictionary containing its 'name' (path) and 'type'.
97
+ """
98
+
99
+ result: list[FilterableField] = []
100
+
101
+ def _explore_dict(d: dict[str, Any], prefix: str, depth: int):
102
+ nonlocal result
103
+
104
+ if depth > max_depth:
105
+ return
106
+
107
+ for k, v in d.items():
108
+ if isinstance(v, (str, int, float, bool)):
109
+ result.append(
110
+ {
111
+ "name": f"{prefix}.{k}",
112
+ "type": cast(Literal["str", "bool", "int", "float"], type(v).__name__),
113
+ }
114
+ )
115
+ elif isinstance(v, dict):
116
+ _explore_dict(cast(dict[str, Any], v), f"{prefix}.{k}", depth + 1)
117
+
118
+ # Look at the agent run metadata
119
+ _explore_dict(to_jsonable_python(self.metadata), "metadata", 0)
120
+ # Look at the transcript metadata
121
+ # TODO(mengk): restore this later when we have the ability to integrate with SQL.
122
+ # for t_id, t in self.transcripts.items():
123
+ # _explore_dict(
124
+ # t.metadata.model_dump(strip_internal_fields=True), f"transcript.{t_id}.metadata", 0
125
+ # )
126
+
127
+ # Append the text field
128
+ result.append({"name": "text", "type": "str"})
129
+
130
+ return result
131
+
132
+ ######################
133
+ # Converting to text #
134
+ ######################
135
+
136
+ def _to_text_impl(self, token_limit: int = sys.maxsize, use_blocks: bool = False) -> list[str]:
137
+ """
138
+ Core implementation for converting agent run to text representation.
139
+
140
+ Args:
141
+ token_limit: Maximum tokens per returned string under the GPT-4 tokenization scheme
142
+ use_blocks: If True, use individual message blocks. If False, use action units.
143
+
144
+ Returns:
145
+ List of strings, each at most token_limit tokens
146
+ """
147
+ # Generate transcript strings using appropriate method
148
+ transcript_strs: list[str] = []
149
+ for i, t in enumerate(self.transcripts):
150
+ if use_blocks:
151
+ transcript_content = t.to_str_blocks_with_token_limit(
152
+ token_limit=sys.maxsize,
153
+ transcript_idx=i,
154
+ agent_run_idx=None,
155
+ )[0]
156
+ else:
157
+ transcript_content = t.to_str_with_token_limit(
158
+ token_limit=sys.maxsize,
159
+ transcript_idx=i,
160
+ agent_run_idx=None,
161
+ )[0]
162
+ transcript_strs.append(f"<transcript>\n{transcript_content}\n</transcript>")
163
+
164
+ transcripts_str = "\n\n".join(transcript_strs)
165
+
166
+ # Gather metadata
167
+ metadata_obj = to_jsonable_python(self.metadata)
168
+ if self.name is not None:
169
+ metadata_obj["name"] = self.name
170
+ if self.description is not None:
171
+ metadata_obj["description"] = self.description
172
+
173
+ yaml_width = float("inf")
174
+ transcripts_str = (
175
+ f"Here is a complete agent run for analysis purposes only:\n{transcripts_str}\n\n"
176
+ )
177
+ metadata_str = f"Metadata about the complete agent run:\n<agent run metadata>\n{yaml.dump(metadata_obj, width=yaml_width)}\n</agent run metadata>"
178
+
179
+ if token_limit == sys.maxsize:
180
+ return [f"{transcripts_str}" f"{metadata_str}"]
181
+
182
+ # Compute message length; if fits, return the full transcript and metadata
183
+ transcript_str_tokens = get_token_count(transcripts_str)
184
+ metadata_str_tokens = get_token_count(metadata_str)
185
+ if transcript_str_tokens + metadata_str_tokens <= token_limit:
186
+ return [f"{transcripts_str}" f"{metadata_str}"]
187
+
188
+ # Otherwise, split up the transcript and metadata into chunks
189
+ else:
190
+ results: list[str] = []
191
+ transcript_token_counts = [get_token_count(t) for t in transcript_strs]
192
+ ranges = group_messages_into_ranges(
193
+ transcript_token_counts, metadata_str_tokens, token_limit - 50
194
+ )
195
+ for msg_range in ranges:
196
+ if msg_range.include_metadata:
197
+ cur_transcript_str = "\n\n".join(
198
+ transcript_strs[msg_range.start : msg_range.end]
199
+ )
200
+ results.append(
201
+ f"Here is a partial agent run for analysis purposes only:\n{cur_transcript_str}"
202
+ f"{metadata_str}"
203
+ )
204
+ else:
205
+ assert (
206
+ msg_range.end == msg_range.start + 1
207
+ ), "Ranges without metadata should be a single message"
208
+ t = self.transcripts[msg_range.start]
209
+ if msg_range.num_tokens < token_limit - 50:
210
+ if use_blocks:
211
+ transcript = f"<transcript>\n{t.to_str_blocks_with_token_limit(token_limit=sys.maxsize)[0]}\n</transcript>"
212
+ else:
213
+ transcript = f"<transcript>\n{t.to_str_with_token_limit(token_limit=sys.maxsize)[0]}\n</transcript>"
214
+ result = (
215
+ f"Here is a partial agent run for analysis purposes only:\n{transcript}"
216
+ )
217
+ results.append(result)
218
+ else:
219
+ if use_blocks:
220
+ transcript_fragments = t.to_str_blocks_with_token_limit(
221
+ token_limit=token_limit - 50,
222
+ )
223
+ else:
224
+ transcript_fragments = t.to_str_with_token_limit(
225
+ token_limit=token_limit - 50,
226
+ )
227
+ for fragment in transcript_fragments:
228
+ result = f"<transcript>\n{fragment}\n</transcript>"
229
+ result = (
230
+ f"Here is a partial agent run for analysis purposes only:\n{result}"
231
+ )
232
+ results.append(result)
233
+ return results
234
+
235
+ def to_text(self, token_limit: int = sys.maxsize) -> list[str]:
236
+ """
237
+ Represents an agent run as a list of strings, each of which is at most token_limit tokens
238
+ under the GPT-4 tokenization scheme.
239
+
240
+ We'll try to split up long AgentRuns along transcript boundaries and include metadata.
241
+ For very long transcripts, we'll have to split them up further and remove metadata.
242
+ """
243
+ return self._to_text_impl(token_limit=token_limit, use_blocks=False)
244
+
245
+ def to_text_blocks(self, token_limit: int = sys.maxsize) -> list[str]:
246
+ """
247
+ Represents an agent run as a list of strings using individual message blocks,
248
+ each of which is at most token_limit tokens under the GPT-4 tokenization scheme.
249
+
250
+ Unlike to_text() which uses action units, this method formats each message
251
+ as an individual block.
252
+ """
253
+ return self._to_text_impl(token_limit=token_limit, use_blocks=True)
254
+
255
+ @property
256
+ def text(self) -> str:
257
+ """Concatenates all transcript texts with double newlines as separators.
258
+
259
+ Returns:
260
+ str: A string representation of all transcripts.
261
+ """
262
+ return self._to_text_impl(token_limit=sys.maxsize, use_blocks=False)[0]
263
+
264
+ @property
265
+ def text_blocks(self) -> str:
266
+ """Concatenates all transcript texts using individual blocks format.
267
+
268
+ Returns:
269
+ str: A string representation of all transcripts using individual message blocks.
270
+ """
271
+ return self._to_text_impl(token_limit=sys.maxsize, use_blocks=True)[0]
272
+
273
+ ##############################
274
+ # New text rendering methods #
275
+ ##############################
276
+
277
+ # Transcript ID -> Transcript
278
+ _transcript_dict: dict[str, Transcript] | None = PrivateAttr(default=None)
279
+ # Transcript Group ID -> Transcript Group
280
+ _transcript_group_dict: dict[str, TranscriptGroup] | None = PrivateAttr(default=None)
281
+ # Canonical tree cache keyed by full_tree flag
282
+ _canonical_tree_cache: dict[bool, dict[str | None, list[tuple[Literal["t", "tg"], str]]]] = (
283
+ PrivateAttr(default_factory=dict)
284
+ )
285
+ # Transcript IDs (depth-first) cache keyed by full_tree flag
286
+ _transcript_ids_ordered_cache: dict[bool, list[str]] = PrivateAttr(default_factory=dict)
287
+
288
+ @property
289
+ def transcript_dict(self) -> dict[str, Transcript]:
290
+ """Lazily compute and cache a mapping from transcript ID to Transcript."""
291
+ if self._transcript_dict is None:
292
+ self._transcript_dict = {t.id: t for t in self.transcripts}
293
+ return self._transcript_dict
294
+
295
+ @property
296
+ def transcript_group_dict(self) -> dict[str, TranscriptGroup]:
297
+ """Lazily compute and cache a mapping from transcript group ID to TranscriptGroup."""
298
+ if self._transcript_group_dict is None:
299
+ self._transcript_group_dict = {tg.id: tg for tg in self.transcript_groups}
300
+ return self._transcript_group_dict
301
+
302
+ def get_canonical_tree(
303
+ self, full_tree: bool = False
304
+ ) -> dict[str | None, list[tuple[Literal["t", "tg"], str]]]:
305
+ """Compute and cache the canonical, sorted transcript group tree.
306
+
307
+ Args:
308
+ full_tree: If True, include all transcript groups regardless of whether
309
+ they contain transcripts. If False, include only the minimal tree
310
+ that connects relevant groups and transcripts.
311
+
312
+ Returns:
313
+ Canonical tree mapping parent group id (or "__global_root") to a list of
314
+ children (type, id) tuples sorted by creation time.
315
+ """
316
+ if (
317
+ full_tree not in self._canonical_tree_cache
318
+ or full_tree not in self._transcript_ids_ordered_cache
319
+ ):
320
+ canonical_tree, transcript_idx_map = self._build_canonical_tree(full_tree=full_tree)
321
+ self._canonical_tree_cache[full_tree] = canonical_tree
322
+ self._transcript_ids_ordered_cache[full_tree] = list(transcript_idx_map.keys())
323
+ return self._canonical_tree_cache[full_tree]
324
+
325
+ def get_transcript_ids_ordered(self, full_tree: bool = False) -> list[str]:
326
+ """Compute and cache the depth-first transcript id ordering.
327
+
328
+ Args:
329
+ full_tree: Whether to compute based on the full tree or the minimal tree.
330
+
331
+ Returns:
332
+ List of transcript ids in depth-first order.
333
+ """
334
+ if (
335
+ full_tree not in self._transcript_ids_ordered_cache
336
+ or full_tree not in self._canonical_tree_cache
337
+ ):
338
+ canonical_tree, transcript_idx_map = self._build_canonical_tree(full_tree=full_tree)
339
+ self._canonical_tree_cache[full_tree] = canonical_tree
340
+ self._transcript_ids_ordered_cache[full_tree] = list(transcript_idx_map.keys())
341
+ return self._transcript_ids_ordered_cache[full_tree]
342
+
343
+ def _build_canonical_tree(self, full_tree: bool = False):
344
+ t_dict = self.transcript_dict
345
+ tg_dict = self.transcript_group_dict
346
+
347
+ # Find all transcript groups that have direct transcript children
348
+ # Also keep track of transcripts that are not in a group
349
+ tgs_to_transcripts: dict[str, set[str]] = {}
350
+ for transcript in t_dict.values():
351
+ if transcript.transcript_group_id is None:
352
+ tgs_to_transcripts.setdefault("__global_root", set()).add(transcript.id)
353
+ else:
354
+ tgs_to_transcripts.setdefault(transcript.transcript_group_id, set()).add(
355
+ transcript.id
356
+ )
357
+
358
+ # tg_tree maps from parent -> children. A child can be a group or a transcript.
359
+ # A parent must be a group (or None, for transcripts that are not in a group).
360
+ tg_tree: dict[str, set[tuple[Literal["t", "tg"], str]]] = {}
361
+
362
+ if full_tree:
363
+ for tg_id, tg in tg_dict.items():
364
+ tg_tree.setdefault(tg.parent_transcript_group_id or "__global_root", set()).add(
365
+ ("tg", tg_id)
366
+ )
367
+ for t_id in tgs_to_transcripts.get(tg_id, []):
368
+ tg_tree.setdefault(tg_id, set()).add(("t", t_id))
369
+ for t_id, t in t_dict.items():
370
+ tg_tree.setdefault(t.transcript_group_id or "__global_root", set()).add(("t", t_id))
371
+ else:
372
+ # Initialize q with "important" tgs
373
+ q, seen = Queue[str](), set[str]()
374
+ for tg_id in tgs_to_transcripts.keys():
375
+ q.put(tg_id)
376
+ seen.add(tg_id)
377
+
378
+ # Do an "upwards BFS" from leaves up to the root. Builds a tree of only relevant nodes.
379
+ while q.qsize() > 0:
380
+ u_id = q.get()
381
+ u = tg_dict.get(u_id) # None if __global_root
382
+
383
+ # Add the transcripts under this tg
384
+ for t_id in tgs_to_transcripts.get(u_id, []):
385
+ tg_tree.setdefault(u_id, set()).add(("t", t_id))
386
+
387
+ # Add an edge from the parent
388
+ if u is not None:
389
+ par_id = u.parent_transcript_group_id or "__global_root"
390
+ # Mark u as a child of par
391
+ tg_tree.setdefault(par_id, set()).add(("tg", u_id))
392
+ # If we haven't investigated the parent before, add to q
393
+ if par_id not in seen:
394
+ q.put(par_id)
395
+ seen.add(par_id)
396
+
397
+ # For each node, sort by created_at timestamp
398
+
399
+ def _cmp(element: tuple[Literal["t", "tg"], str]) -> datetime:
400
+ obj_type, obj_id = element
401
+ if obj_type == "tg":
402
+ return tg_dict[obj_id].created_at or datetime.max
403
+ else:
404
+ return t_dict[obj_id].created_at or datetime.max
405
+
406
+ c_tree: dict[str | None, list[tuple[Literal["t", "tg"], str]]] = {}
407
+ for tg_id in tg_tree:
408
+ children_ids = list(set(tg_tree[tg_id]))
409
+ sorted_children_ids = sorted(children_ids, key=_cmp)
410
+ c_tree[tg_id] = sorted_children_ids
411
+
412
+ # Compute transcript indices as the depth-first traversal index
413
+ transcript_idx_map: dict[str, int] = {}
414
+
415
+ def _assign_transcript_indices(cur_tg_id: str, next_idx: int) -> int:
416
+ children = c_tree.get(cur_tg_id, [])
417
+ for child_type, child_id in children:
418
+ if child_type == "tg":
419
+ next_idx = _assign_transcript_indices(child_id, next_idx)
420
+ else:
421
+ transcript_idx_map[child_id] = next_idx
422
+ next_idx += 1
423
+ return next_idx
424
+
425
+ _assign_transcript_indices("__global_root", 0)
426
+
427
+ return c_tree, transcript_idx_map
428
+
429
+ def to_text_new(self, indent: int = 0, full_tree: bool = False):
430
+ c_tree = self.get_canonical_tree(full_tree=full_tree)
431
+ t_ids_ordered = self.get_transcript_ids_ordered(full_tree=full_tree)
432
+ t_idx_map = {t_id: i for i, t_id in enumerate(t_ids_ordered)}
433
+ t_dict = self.transcript_dict
434
+ tg_dict = self.transcript_group_dict
435
+
436
+ # Traverse the tree and render the string
437
+ def _recurse(tg_id: str) -> str:
438
+ children_ids = c_tree.get(tg_id, [])
439
+ children_texts: list[str] = []
440
+ for child_type, child_id in children_ids:
441
+ if child_type == "tg":
442
+ children_texts.append(_recurse(child_id))
443
+ else:
444
+ cur_text = t_dict[child_id].to_text_new(
445
+ transcript_idx=t_idx_map[child_id],
446
+ indent=indent,
447
+ )
448
+ children_texts.append(cur_text)
449
+ children_text = "\n".join(children_texts)
450
+
451
+ # No wrapper for global root
452
+ if tg_id == "__global_root":
453
+ return children_text
454
+ # Delegate rendering to TranscriptGroup
455
+ else:
456
+ tg = tg_dict[tg_id]
457
+ return tg.to_text_new(children_text=children_text, indent=indent)
458
+
459
+ text = _recurse("__global_root")
460
+
461
+ # Append agent run metadata below the full content
462
+ yaml_text = yaml_dump_metadata(self.metadata)
463
+ if yaml_text is not None:
464
+ if indent > 0:
465
+ yaml_text = textwrap.indent(yaml_text, " " * indent)
466
+ text += f"\n<|agent run metadata|>\n{yaml_text}\n</|agent run metadata|>"
467
+
468
+ return text
@@ -66,16 +66,13 @@ def get_transcript_text_for_citation(agent_run: AgentRun, citation: Citation) ->
66
66
  return None
67
67
 
68
68
  try:
69
- transcript_keys = list(agent_run.transcripts.keys())
70
- if citation.transcript_idx >= len(transcript_keys):
69
+ if citation.transcript_idx >= len(agent_run.get_transcript_ids_ordered()):
71
70
  return None
71
+ transcript_id = agent_run.get_transcript_ids_ordered()[citation.transcript_idx]
72
+ transcript = agent_run.transcript_dict[transcript_id]
72
73
 
73
- transcript_key = transcript_keys[citation.transcript_idx]
74
-
75
- transcript = agent_run.transcripts[transcript_key]
76
74
  if citation.block_idx >= len(transcript.messages):
77
75
  return None
78
-
79
76
  message = transcript.messages[citation.block_idx]
80
77
 
81
78
  # Use the same formatting function that generates content for LLMs