langfun 0.1.2.dev202412090805__py3-none-any.whl → 0.1.2.dev202412130804__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.
@@ -15,7 +15,11 @@
15
15
 
16
16
  import abc
17
17
  import contextlib
18
- from typing import Annotated, Any, Iterable, Iterator, Optional, Type, Union
18
+ import datetime
19
+ import time
20
+
21
+ import typing
22
+ from typing import Annotated, Any, ContextManager, Iterable, Iterator, Optional, Type, Union
19
23
  import langfun.core as lf
20
24
  from langfun.core import structured as lf_structured
21
25
  import pyglove as pg
@@ -26,24 +30,413 @@ class Action(pg.Object):
26
30
 
27
31
  def _on_bound(self):
28
32
  super()._on_bound()
33
+ self._session = None
29
34
  self._result = None
35
+ self._metadata = {}
36
+
37
+ @property
38
+ def session(self) -> Optional['Session']:
39
+ """Returns the session started by this action."""
40
+ return self._session
30
41
 
31
42
  @property
32
43
  def result(self) -> Any:
33
44
  """Returns the result of the action."""
34
45
  return self._result
35
46
 
47
+ @property
48
+ def metadata(self) -> dict[str, Any] | None:
49
+ """Returns the metadata associated with the result from previous call."""
50
+ return self._metadata
51
+
36
52
  def __call__(
37
- self, session: Optional['Session'] = None, **kwargs) -> Any:
53
+ self,
54
+ session: Optional['Session'] = None,
55
+ *,
56
+ show_progress: bool = True,
57
+ **kwargs) -> Any:
38
58
  """Executes the action."""
39
- session = session or Session()
40
- with session.track(self):
41
- self._result = self.call(session=session, **kwargs)
59
+ new_session = session is None
60
+ if new_session:
61
+ session = Session()
62
+ if show_progress:
63
+ lf.console.display(pg.view(session, name='agent_session'))
64
+
65
+ with session.track_action(self):
66
+ result = self.call(session=session, **kwargs)
67
+
68
+ # For the top-level action, we store the session in the metadata.
69
+ if new_session:
70
+ self._session = session
71
+ self._result = result
72
+ self._metadata = session.current_action.metadata
42
73
  return self._result
43
74
 
44
75
  @abc.abstractmethod
45
76
  def call(self, session: 'Session', **kwargs) -> Any:
46
- """Subclasses to implement."""
77
+ """Calls the action.
78
+
79
+ Args:
80
+ session: The session to use for the action.
81
+ **kwargs: Additional keyword arguments to pass to the action.
82
+
83
+ Returns:
84
+ The result of the action.
85
+ """
86
+
87
+
88
+ # Type definition for traced item during execution.
89
+ TracedItem = Union[
90
+ lf_structured.QueryInvocation,
91
+ 'ActionInvocation',
92
+ 'ExecutionTrace',
93
+ # NOTE(daiyip): Consider remove log entry once we migrate existing agents.
94
+ lf.logging.LogEntry,
95
+ ]
96
+
97
+
98
+ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
99
+ """Trace of the execution of an action."""
100
+
101
+ name: Annotated[
102
+ str | None,
103
+ (
104
+ 'The name of the execution trace. If None, the trace is unnamed, '
105
+ 'which is the case for the top-level trace of an action. An '
106
+ 'execution trace could have sub-traces, called phases, which are '
107
+ 'created and named by `session.phase()` context manager.'
108
+ )
109
+ ] = None
110
+
111
+ start_time: Annotated[
112
+ float | None,
113
+ 'The start time of the execution. If None, the execution is not started.'
114
+ ] = None
115
+
116
+ end_time: Annotated[
117
+ float | None,
118
+ 'The end time of the execution. If None, the execution is not ended.'
119
+ ] = None
120
+
121
+ items: Annotated[
122
+ list[TracedItem],
123
+ 'All tracked execution items in the sequence.'
124
+ ] = []
125
+
126
+ def _on_bound(self):
127
+ super()._on_bound()
128
+ self._usage_summary = lf.UsageSummary()
129
+ for item in self.items:
130
+ if hasattr(item, 'usage_summary'):
131
+ self._usage_summary.merge(item.usage_summary)
132
+
133
+ self._tab_control = None
134
+ self._time_badge = None
135
+
136
+ def start(self) -> None:
137
+ assert self.start_time is None, 'Execution already started.'
138
+ self.rebind(start_time=time.time(), skip_notification=True)
139
+ if self._time_badge is not None:
140
+ self._time_badge.update(
141
+ 'Starting',
142
+ add_class=['starting'],
143
+ remove_class=['not-started'],
144
+ )
145
+
146
+ def stop(self) -> None:
147
+ assert self.end_time is None, 'Execution already stopped.'
148
+ self.rebind(end_time=time.time(), skip_notification=True)
149
+ if self._time_badge is not None:
150
+ self._time_badge.update(
151
+ f'{int(self.elapse)} seconds',
152
+ tooltip=pg.format(self.execution_summary(), verbose=False),
153
+ add_class=['finished'],
154
+ remove_class=['running'],
155
+ )
156
+
157
+ def __len__(self) -> int:
158
+ return len(self.items)
159
+
160
+ @property
161
+ def has_started(self) -> bool:
162
+ return self.start_time is not None
163
+
164
+ @property
165
+ def has_stopped(self) -> bool:
166
+ return self.end_time is not None
167
+
168
+ @property
169
+ def elapse(self) -> float:
170
+ """Returns the elapsed time of the execution."""
171
+ if self.start_time is None:
172
+ return 0.0
173
+ if self.end_time is None:
174
+ return time.time() - self.start_time
175
+ return self.end_time - self.start_time
176
+
177
+ @property
178
+ def queries(self) -> list[lf_structured.QueryInvocation]:
179
+ """Returns queries from the sequence."""
180
+ return self._child_items(lf_structured.QueryInvocation)
181
+
182
+ @property
183
+ def actions(self) -> list['ActionInvocation']:
184
+ """Returns action invocations from the sequence."""
185
+ return self._child_items(ActionInvocation)
186
+
187
+ @property
188
+ def logs(self) -> list[lf.logging.LogEntry]:
189
+ """Returns logs from the sequence."""
190
+ return self._child_items(lf.logging.LogEntry)
191
+
192
+ @property
193
+ def all_queries(self) -> list[lf_structured.QueryInvocation]:
194
+ """Returns all queries from current trace and its child execution items."""
195
+ return self._all_child_items(lf_structured.QueryInvocation)
196
+
197
+ @property
198
+ def all_logs(self) -> list[lf.logging.LogEntry]:
199
+ """Returns all logs from current trace and its child execution items."""
200
+ return self._all_child_items(lf.logging.LogEntry)
201
+
202
+ def _child_items(self, item_cls: Type[Any]) -> list[Any]:
203
+ child_items = []
204
+ for item in self.items:
205
+ if isinstance(item, item_cls):
206
+ child_items.append(item)
207
+ elif isinstance(item, ExecutionTrace):
208
+ child_items.extend(item._child_items(item_cls)) # pylint: disable=protected-access
209
+ return child_items
210
+
211
+ def _all_child_items(self, item_cls: Type[Any]) -> list[Any]:
212
+ child_items = []
213
+ for item in self.items:
214
+ if isinstance(item, item_cls):
215
+ child_items.append(item)
216
+ elif isinstance(item, ActionInvocation):
217
+ child_items.extend(item.execution._all_child_items(item_cls)) # pylint: disable=protected-access
218
+ elif isinstance(item, ExecutionTrace):
219
+ child_items.extend(item._all_child_items(item_cls)) # pylint: disable=protected-access
220
+ return child_items
221
+
222
+ def append(self, item: TracedItem) -> None:
223
+ """Appends an item to the sequence."""
224
+ with pg.notify_on_change(False):
225
+ self.items.append(item)
226
+
227
+ if isinstance(item, lf_structured.QueryInvocation):
228
+ current_invocation = self
229
+ while current_invocation is not None:
230
+ current_invocation.usage_summary.merge(item.usage_summary)
231
+ current_invocation = typing.cast(
232
+ ExecutionTrace,
233
+ current_invocation.sym_ancestor(
234
+ lambda x: isinstance(x, ExecutionTrace)
235
+ )
236
+ )
237
+
238
+ if self._tab_control is not None:
239
+ self._tab_control.append(self._execution_item_tab(item))
240
+
241
+ if (self._time_badge is not None
242
+ and not isinstance(item, lf.logging.LogEntry)):
243
+ sub_task_label = self._execution_item_label(item)
244
+ self._time_badge.update(
245
+ text=sub_task_label.text,
246
+ tooltip=sub_task_label.tooltip.content,
247
+ add_class=['running'],
248
+ remove_class=['not-started'],
249
+ )
250
+
251
+ def extend(self, items: Iterable[TracedItem]) -> None:
252
+ """Extends the sequence with a list of items."""
253
+ for item in items:
254
+ self.append(item)
255
+
256
+ @property
257
+ def usage_summary(self) -> lf.UsageSummary:
258
+ """Returns the usage summary of the action."""
259
+ return self._usage_summary
260
+
261
+ def execution_summary(self) -> dict[str, Any]:
262
+ """Execution summary string."""
263
+ return pg.Dict(
264
+ num_queries=len(self.queries),
265
+ execution_breakdown=[
266
+ dict(
267
+ action=action.action.__class__.__name__,
268
+ usage=action.usage_summary.total,
269
+ execution_time=action.execution.elapse,
270
+ )
271
+ for action in self.actions
272
+ ]
273
+ )
274
+
275
+ #
276
+ # HTML views.
277
+ #
278
+
279
+ def _html_tree_view_summary(
280
+ self,
281
+ *,
282
+ name: str | None = None,
283
+ extra_flags: dict[str, Any] | None = None,
284
+ view: pg.views.html.HtmlTreeView, **kwargs
285
+ ):
286
+ return None
287
+
288
+ def _execution_badge(self, interactive: bool = True):
289
+ if not self.has_started:
290
+ label = '(Not started)'
291
+ tooltip = 'Execution not started.'
292
+ css_class = 'not-started'
293
+ elif not self.has_stopped:
294
+ label = 'Starting'
295
+ tooltip = 'Execution starting.'
296
+ css_class = 'running'
297
+ else:
298
+ label = f'{int(self.elapse)} seconds'
299
+ tooltip = pg.format(self.execution_summary(), verbose=False)
300
+ css_class = 'finished'
301
+ time_badge = pg.views.html.controls.Badge(
302
+ label,
303
+ tooltip=tooltip,
304
+ css_classes=['execution-time', css_class],
305
+ interactive=interactive,
306
+ )
307
+ if interactive:
308
+ self._time_badge = time_badge
309
+ return time_badge
310
+
311
+ def _html_tree_view_content(
312
+ self,
313
+ *,
314
+ extra_flags: dict[str, Any] | None = None,
315
+ **kwargs
316
+ ):
317
+ del kwargs
318
+ extra_flags = extra_flags or {}
319
+ interactive = extra_flags.get('interactive', True)
320
+ if interactive or self.items:
321
+ self._tab_control = pg.views.html.controls.TabControl(
322
+ [self._execution_item_tab(item) for item in self.items],
323
+ tab_position='left'
324
+ )
325
+ return self._tab_control.to_html()
326
+ return '(no tracked items)'
327
+
328
+ def _execution_item_tab(self, item: TracedItem) -> pg.views.html.controls.Tab:
329
+ if isinstance(item, ActionInvocation):
330
+ css_class = 'action'
331
+ elif isinstance(item, lf_structured.QueryInvocation):
332
+ css_class = 'query'
333
+ elif isinstance(item, lf.logging.LogEntry):
334
+ css_class = 'log'
335
+ elif isinstance(item, ExecutionTrace):
336
+ css_class = 'phase'
337
+ else:
338
+ raise ValueError(f'Unsupported item type: {type(item)}')
339
+
340
+ return pg.views.html.controls.Tab(
341
+ label=self._execution_item_label(item),
342
+ content=pg.view(item),
343
+ css_classes=[css_class]
344
+ )
345
+
346
+ def _execution_item_label(
347
+ self, item: TracedItem
348
+ ) -> pg.views.html.controls.Label:
349
+ if isinstance(item, ActionInvocation):
350
+ return pg.views.html.controls.Label(
351
+ item.action.__class__.__name__,
352
+ tooltip=pg.format(
353
+ item.action,
354
+ verbose=False,
355
+ hide_default_values=True,
356
+ max_str_len=80,
357
+ max_bytes_len=20,
358
+ ),
359
+ )
360
+ elif isinstance(item, lf_structured.QueryInvocation):
361
+ schema_title = 'str'
362
+ if item.schema:
363
+ schema_title = lf_structured.annotation(item.schema.spec)
364
+ return pg.views.html.controls.Label(
365
+ schema_title,
366
+ tooltip=(
367
+ pg.format(
368
+ item.input,
369
+ verbose=False,
370
+ hide_default_values=True,
371
+ max_str_len=80,
372
+ max_bytes_len=20,
373
+ )
374
+ ),
375
+ )
376
+ elif isinstance(item, lf.logging.LogEntry):
377
+ return pg.views.html.controls.Label(
378
+ 'Log',
379
+ tooltip=item.message,
380
+ )
381
+ elif isinstance(item, ExecutionTrace):
382
+ return pg.views.html.controls.Label(
383
+ item.name or 'Phase',
384
+ tooltip=f'Execution phase {item.name!r}.'
385
+ )
386
+ else:
387
+ raise ValueError(f'Unsupported item type: {type(item)}')
388
+
389
+ def _html_tree_view_css_styles(self) -> list[str]:
390
+ return super()._html_tree_view_css_styles() + [
391
+ """
392
+ .tab-button.action > ::before {
393
+ content: "A";
394
+ font-weight: bold;
395
+ color: red;
396
+ padding: 10px;
397
+ }
398
+ .tab-button.phase > ::before {
399
+ content: "P";
400
+ font-weight: bold;
401
+ color: purple;
402
+ padding: 10px;
403
+ }
404
+ .tab-button.query > ::before {
405
+ content: "Q";
406
+ font-weight: bold;
407
+ color: orange;
408
+ padding: 10px;
409
+ }
410
+ .tab-button.log > ::before {
411
+ content: "L";
412
+ font-weight: bold;
413
+ color: green;
414
+ padding: 10px;
415
+ }
416
+ .details.execution-trace, .details.action-invocation {
417
+ border: 1px solid #eee;
418
+ }
419
+ .execution-trace-title {
420
+ display: inline-block;
421
+ }
422
+ .badge.execution-time {
423
+ margin-left: 4px;
424
+ border-radius: 0px;
425
+ }
426
+ .execution-time.starting {
427
+ background-color: ghostwhite;
428
+ font-weight: normal;
429
+ }
430
+ .execution-time.running {
431
+ background-color: ghostwhite;
432
+ font-weight: normal;
433
+ }
434
+ .execution-time.finished {
435
+ background-color: aliceblue;
436
+ font-weight: bold;
437
+ }
438
+ """
439
+ ]
47
440
 
48
441
 
49
442
  class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
@@ -55,131 +448,216 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
55
448
  'The result of the action.'
56
449
  ] = None
57
450
 
451
+ metadata: Annotated[
452
+ dict[str, Any],
453
+ 'The metadata returned by the action.'
454
+ ] = {}
455
+
58
456
  execution: Annotated[
59
- list[
60
- Union[
61
- lf_structured.QueryInvocation,
62
- 'ActionInvocation',
63
- lf.logging.LogEntry
64
- ]
65
- ],
66
- 'Execution execution.'
67
- ] = []
457
+ ExecutionTrace,
458
+ 'The execution sequence of the action.'
459
+ ] = ExecutionTrace()
68
460
 
69
461
  # Allow symbolic assignment without `rebind`.
70
462
  allow_symbolic_assignment = True
71
463
 
464
+ def _on_bound(self):
465
+ super()._on_bound()
466
+ self._current_phase = self.execution
467
+ self._tab_control = None
468
+
469
+ @property
470
+ def current_phase(self) -> ExecutionTrace:
471
+ """Returns the current execution phase."""
472
+ return self._current_phase
473
+
474
+ @contextlib.contextmanager
475
+ def phase(self, name: str) -> Iterator[ExecutionTrace]:
476
+ """Context manager for starting a new execution phase."""
477
+ phase = ExecutionTrace(name=name)
478
+ phase.start()
479
+ parent_phase = self._current_phase
480
+ self._current_phase.append(phase)
481
+ self._current_phase = phase
482
+ try:
483
+ yield phase
484
+ finally:
485
+ phase.stop()
486
+ self._current_phase = parent_phase
487
+
72
488
  @property
73
489
  def logs(self) -> list[lf.logging.LogEntry]:
74
- """Returns logs from execution sequence."""
75
- return [v for v in self.execution if isinstance(v, lf.logging.LogEntry)]
490
+ """Returns immediate child logs from execution sequence."""
491
+ return self.execution.logs
76
492
 
77
493
  @property
78
- def child_invocations(self) -> list['ActionInvocation']:
79
- """Returns child action invocations."""
80
- return [v for v in self.execution if isinstance(v, ActionInvocation)]
494
+ def actions(self) -> list['ActionInvocation']:
495
+ """Returns immediate child action invocations."""
496
+ return self.execution.actions
81
497
 
82
- def queries(
83
- self,
84
- include_children: bool = False
85
- ) -> Iterable[lf_structured.QueryInvocation]:
86
- """Iterates over queries from the current invocation."""
87
- for v in self.execution:
88
- if isinstance(v, lf_structured.QueryInvocation):
89
- yield v
90
- elif isinstance(v, ActionInvocation):
91
- if include_children:
92
- yield from v.queries(include_children=True)
498
+ @property
499
+ def queries(self) -> list[lf_structured.QueryInvocation]:
500
+ """Returns immediate queries made by the action."""
501
+ return self.execution.queries
502
+
503
+ @property
504
+ def all_queries(self) -> list[lf_structured.QueryInvocation]:
505
+ """Returns all queries made by the action and its child execution items."""
506
+ return self.execution.all_queries
507
+
508
+ @property
509
+ def all_logs(self) -> list[lf.logging.LogEntry]:
510
+ """Returns all logs made by the action and its child execution items."""
511
+ return self.execution.all_logs
512
+
513
+ @property
514
+ def usage_summary(self) -> lf.UsageSummary:
515
+ """Returns the usage summary of the action."""
516
+ return self.execution.usage_summary
517
+
518
+ def start(self) -> None:
519
+ """Starts the execution of the action."""
520
+ self.execution.start()
521
+
522
+ def end(self, result: Any, metadata: dict[str, Any]) -> None:
523
+ """Ends the execution of the action with result and metadata."""
524
+ self.rebind(
525
+ result=result,
526
+ metadata=metadata,
527
+ skip_notification=True,
528
+ raise_on_no_change=False
529
+ )
530
+ self.execution.stop()
531
+ if self._tab_control is not None:
532
+ if self.metadata:
533
+ self._tab_control.insert(
534
+ 1,
535
+ pg.views.html.controls.Tab(
536
+ 'metadata',
537
+ pg.view(
538
+ self.metadata,
539
+ collapse_level=None,
540
+ enable_summary_tooltip=False
541
+ ),
542
+ name='metadata',
543
+ )
544
+ )
545
+ self._tab_control.insert(
546
+ 1,
547
+ pg.views.html.controls.Tab(
548
+ 'result',
549
+ pg.view(
550
+ self.result,
551
+ collapse_level=None,
552
+ enable_summary_tooltip=False
553
+ ),
554
+ name='result',
555
+ ),
556
+ )
557
+ self._tab_control.select(['metadata', 'result'])
558
+
559
+ #
560
+ # HTML views.
561
+ #
93
562
 
94
563
  def _html_tree_view_summary(
95
564
  self, *, view: pg.views.html.HtmlTreeView, **kwargs
96
565
  ):
97
- if isinstance(self.action, RootAction):
98
- return None
99
- kwargs.pop('title')
100
- return view.summary(
101
- self,
102
- title=view.render(
103
- self.action, name='action', collapse_level=0,
104
- css_classes='invocation-title',
105
- ),
106
- **kwargs
107
- )
566
+ return None
108
567
 
109
568
  def _html_tree_view_content(
110
569
  self,
111
570
  *,
112
- root_path: pg.KeyPath | None = None,
113
- collapse_level: int | None = None,
114
571
  view: pg.views.html.HtmlTreeView,
572
+ extra_flags: dict[str, Any] | None = None,
115
573
  **kwargs
116
574
  ):
117
- prepare_phase = []
118
- current_phase = prepare_phase
119
- action_phases = []
120
- for item in self.execution:
121
- if isinstance(item, ActionInvocation):
122
- current_phase = []
123
- action_phases.append(current_phase)
124
- current_phase.append(item)
125
-
126
- def _render_phase(
127
- phase: list[ActionInvocation | lf.logging.LogEntry]
128
- ) -> pg.Html.WritableTypes:
129
- return pg.Html.element(
130
- 'div',
131
- [
132
- view.render(item) for item in phase
133
- ]
575
+ extra_flags = extra_flags or {}
576
+ interactive = extra_flags.get('interactive', True)
577
+ if (isinstance(self.action, RootAction)
578
+ and self.execution.has_stopped
579
+ and len(self.execution) == 1):
580
+ return view.content(self.execution.items[0], extra_flags=extra_flags)
581
+
582
+ tabs = []
583
+ if not isinstance(self.action, RootAction):
584
+ tabs.append(
585
+ pg.views.html.controls.Tab(
586
+ 'action',
587
+ view.render( # pylint: disable=g-long-ternary
588
+ self.action,
589
+ collapse_level=None,
590
+ root_path=self.action.sym_path,
591
+ enable_summary_tooltip=False,
592
+ ),
593
+ name='action',
594
+ )
134
595
  )
135
-
136
- def _render_action_phases(
137
- phases: list[list[ActionInvocation | lf.logging.LogEntry]]
138
- ) -> pg.Html.WritableTypes:
139
- if len(phases) == 1:
140
- return _render_phase(phases[0])
141
- return pg.views.html.controls.TabControl(
142
- [
143
- pg.views.html.controls.Tab(
144
- label=f'Step {i + 1}',
145
- content=_render_phase(phase),
146
- )
147
- for i, phase in enumerate(phases)
148
- ],
596
+ if self.execution.has_stopped:
597
+ tabs.append(
598
+ pg.views.html.controls.Tab(
599
+ 'result',
600
+ view.render(
601
+ self.result,
602
+ collapse_level=None,
603
+ enable_summary_tooltip=False
604
+ ),
605
+ name='result'
606
+ )
149
607
  )
608
+ if self.metadata:
609
+ tabs.append(
610
+ pg.views.html.controls.Tab(
611
+ 'metadata',
612
+ view.render(
613
+ self.metadata,
614
+ collapse_level=None,
615
+ enable_summary_tooltip=False
616
+ ),
617
+ name='metadata'
618
+ )
619
+ )
150
620
 
151
- result_name = 'final_result' if isinstance(
152
- self.action, RootAction) else 'result'
153
- return pg.Html.element(
154
- 'div',
155
- [
156
- view.render(
157
- self.result,
158
- name=result_name,
159
- css_classes=[
160
- f'invocation-{result_name}'.replace('_', '-')
161
- ]
621
+ tabs.append(
622
+ pg.views.html.controls.Tab(
623
+ pg.Html.element(
624
+ 'span',
625
+ [
626
+ 'execution',
627
+ self.execution._execution_badge(interactive), # pylint: disable=protected-access
628
+ (
629
+ self.usage_summary.to_html( # pylint: disable=g-long-ternary
630
+ extra_flags=dict(as_badge=True)
631
+ )
632
+ if (interactive
633
+ or self.usage_summary.total.num_requests > 0)
634
+ else None
635
+ ),
636
+ ],
637
+ css_classes=['execution-tab-title']
162
638
  ),
163
- _render_phase(prepare_phase) if prepare_phase else None,
164
- _render_action_phases(action_phases)
165
- ]
639
+ view.render(self.execution, extra_flags=extra_flags),
640
+ name='execution',
641
+ )
166
642
  )
643
+ tab_control = pg.views.html.controls.TabControl(tabs)
644
+ # Select the tab following a priority: metadata, result, action, execution.
645
+ tab_control.select(['metadata', 'result', 'action', 'execution'])
646
+ if interactive:
647
+ self._tab_control = tab_control
648
+ return tab_control
167
649
 
168
650
  @classmethod
169
651
  def _html_tree_view_css_styles(cls) -> list[str]:
170
652
  return super()._html_tree_view_css_styles() + [
171
653
  """
172
- details.invocation-title {
173
- display: inline-block;
174
- background-color: #b1f0ff;
175
- border: 1px solid white;
654
+ .execution-tab-title {
655
+ text-align: left;
176
656
  }
177
- details.invocation-result {
178
- border: 1px solid #eee;
179
- }
180
- details.invocation-final-result {
181
- border: 1px solid #eee;
182
- background-color: #fef78f;
657
+ .execution-tab-title .usage-summary.label {
658
+ border-radius: 0px;
659
+ font-weight: normal;
660
+ color: #AAA;
183
661
  }
184
662
  """
185
663
  ]
@@ -192,52 +670,80 @@ class RootAction(Action):
192
670
  raise NotImplementedError('Shall not be called.')
193
671
 
194
672
 
195
- class Session(pg.Object):
673
+ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
196
674
  """Session for performing an agentic task."""
197
675
 
198
- root_invocation: ActionInvocation = ActionInvocation(RootAction())
676
+ root: ActionInvocation = ActionInvocation(RootAction())
199
677
 
200
678
  def _on_bound(self):
201
679
  super()._on_bound()
202
- self._invocation_stack = [self.root_invocation]
680
+ self._current_action = self.root
203
681
 
204
682
  @property
205
683
  def final_result(self) -> Any:
206
684
  """Returns the final result of the session."""
207
- return self.root_invocation.result
685
+ return self.root.result
208
686
 
209
687
  @property
210
- def current_invocation(self) -> ActionInvocation:
688
+ def current_action(self) -> ActionInvocation:
211
689
  """Returns the current invocation."""
212
- assert self._invocation_stack
213
- return self._invocation_stack[-1]
690
+ return self._current_action
691
+
692
+ def add_metadata(self, **kwargs: Any) -> None:
693
+ """Adds metadata to the current invocation."""
694
+ with pg.notify_on_change(False):
695
+ self._current_action.metadata.update(kwargs)
696
+
697
+ def phase(self, name: str) -> ContextManager[ExecutionTrace]:
698
+ """Context manager for starting a new execution phase."""
699
+ return self.current_action.phase(name)
214
700
 
215
701
  @contextlib.contextmanager
216
- def track(self, action: Action) -> Iterator[ActionInvocation]:
702
+ def track_action(self, action: Action) -> Iterator[ActionInvocation]:
217
703
  """Track the execution of an action."""
218
- new_invocation = ActionInvocation(pg.maybe_ref(action))
219
- with pg.notify_on_change(False):
220
- self.current_invocation.execution.append(new_invocation)
221
- self._invocation_stack.append(new_invocation)
704
+ if not self.root.execution.has_started:
705
+ self.root.start()
706
+
707
+ invocation = ActionInvocation(pg.maybe_ref(action))
708
+ parent_action = self._current_action
709
+ parent_action.current_phase.append(invocation)
222
710
 
223
711
  try:
224
- yield new_invocation
712
+ self._current_action = invocation
713
+ # Start the execution of the current action.
714
+ self._current_action.start()
715
+ yield invocation
225
716
  finally:
226
- assert self._invocation_stack
227
- invocation = self._invocation_stack.pop(-1)
228
- invocation.rebind(
229
- result=action.result, skip_notification=True, raise_on_no_change=False
230
- )
231
- assert invocation.action is action, (invocation.action, action)
232
- assert self._invocation_stack, self._invocation_stack
233
-
234
- if len(self._invocation_stack) == 1:
235
- self.root_invocation.rebind(
236
- result=invocation.result,
237
- skip_notification=True,
238
- raise_on_no_change=False
717
+ # Stop the execution of the current action.
718
+ self._current_action.end(action.result, action.metadata)
719
+ self._current_action = parent_action
720
+ if parent_action is self.root:
721
+ parent_action.end(
722
+ result=action.result, metadata=action.metadata,
239
723
  )
240
724
 
725
+ @contextlib.contextmanager
726
+ def track_queries(
727
+ self,
728
+ phase: str | None = None
729
+ ) -> Iterator[list[lf_structured.QueryInvocation]]:
730
+ """Tracks `lf.query` made within the context.
731
+
732
+ Args:
733
+ phase: The name of a new phase to track the queries in. If not provided,
734
+ the queries will be tracked in the parent phase.
735
+
736
+ Yields:
737
+ A list of `lf.QueryInvocation` objects, each for a single `lf.query`
738
+ call.
739
+ """
740
+ with self.phase(phase) if phase else contextlib.nullcontext():
741
+ with lf_structured.track_queries(include_child_scopes=False) as queries:
742
+ try:
743
+ yield queries
744
+ finally:
745
+ self._current_action.current_phase.extend(queries)
746
+
241
747
  def query(
242
748
  self,
243
749
  prompt: Union[str, lf.Template, Any],
@@ -250,9 +756,37 @@ class Session(pg.Object):
250
756
  examples: list[lf_structured.MappingExample] | None = None,
251
757
  **kwargs
252
758
  ) -> Any:
253
- """Calls `lf.query` and associates it with the current invocation."""
254
- with lf_structured.track_queries() as queries:
255
- output = lf_structured.query(
759
+ """Calls `lf.query` and associates it with the current invocation.
760
+
761
+ The following code are equivalent:
762
+
763
+ Code 1:
764
+ ```
765
+ session.query(...)
766
+ ```
767
+
768
+ Code 2:
769
+ ```
770
+ with session.track_queries() as queries:
771
+ output = lf.query(...)
772
+ ```
773
+ The former is preferred when `lf.query` is directly called by the action.
774
+ If `lf.query` is called by a function that does not have access to the
775
+ session, the latter should be used.
776
+
777
+ Args:
778
+ prompt: The prompt to query.
779
+ schema: The schema to use for the query.
780
+ default: The default value to return if the query fails.
781
+ lm: The language model to use for the query.
782
+ examples: The examples to use for the query.
783
+ **kwargs: Additional keyword arguments to pass to `lf.query`.
784
+
785
+ Returns:
786
+ The result of the query.
787
+ """
788
+ with self.track_queries():
789
+ return lf_structured.query(
256
790
  prompt,
257
791
  schema=schema,
258
792
  default=default,
@@ -260,17 +794,16 @@ class Session(pg.Object):
260
794
  examples=examples,
261
795
  **kwargs
262
796
  )
263
- with pg.notify_on_change(False):
264
- self.current_invocation.execution.extend(queries)
265
- return output
266
797
 
267
798
  def _log(self, level: lf.logging.LogLevel, message: str, **kwargs):
268
- with pg.notify_on_change(False):
269
- self.current_invocation.execution.append(
270
- lf.logging.log(
271
- level, message, indent=len(self._invocation_stack) - 1, **kwargs
272
- )
273
- )
799
+ self._current_action.current_phase.append(
800
+ lf.logging.LogEntry(
801
+ level=level,
802
+ time=datetime.datetime.now(),
803
+ message=message,
804
+ metadata=kwargs,
805
+ )
806
+ )
274
807
 
275
808
  def debug(self, message: str, **kwargs):
276
809
  """Logs a debug message to the session."""
@@ -296,5 +829,26 @@ class Session(pg.Object):
296
829
  """Returns the session as a message."""
297
830
  return lf.AIMessage(
298
831
  'Agentic task session.',
299
- result=self.root_invocation
832
+ result=self.root
833
+ )
834
+
835
+ #
836
+ # HTML views.
837
+ #
838
+
839
+ def _html_tree_view_content(
840
+ self,
841
+ *,
842
+ view: pg.views.html.HtmlTreeView,
843
+ **kwargs
844
+ ):
845
+ return view.content(self.root, **kwargs)
846
+
847
+ @classmethod
848
+ def _html_tree_view_config(cls):
849
+ config = super()._html_tree_view_config()
850
+ config.update(
851
+ enable_summary_tooltip=False,
852
+ enable_key_tooltip=False,
300
853
  )
854
+ return config