langfun 0.1.2.dev202510230805__py3-none-any.whl → 0.1.2.dev202511270805__py3-none-any.whl

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

Potentially problematic release.


This version of langfun might be problematic. Click here for more details.

Files changed (155) hide show
  1. langfun/core/__init__.py +2 -0
  2. langfun/core/agentic/__init__.py +4 -1
  3. langfun/core/agentic/action.py +447 -29
  4. langfun/core/agentic/action_eval.py +9 -2
  5. langfun/core/agentic/action_test.py +149 -21
  6. langfun/core/async_support.py +32 -3
  7. langfun/core/coding/python/correction.py +19 -9
  8. langfun/core/coding/python/execution.py +14 -12
  9. langfun/core/coding/python/generation.py +21 -16
  10. langfun/core/coding/python/sandboxing.py +23 -3
  11. langfun/core/component.py +42 -3
  12. langfun/core/concurrent.py +70 -6
  13. langfun/core/concurrent_test.py +1 -0
  14. langfun/core/console.py +1 -1
  15. langfun/core/data/conversion/anthropic.py +12 -3
  16. langfun/core/data/conversion/anthropic_test.py +8 -6
  17. langfun/core/data/conversion/gemini.py +9 -2
  18. langfun/core/data/conversion/gemini_test.py +12 -9
  19. langfun/core/data/conversion/openai.py +145 -31
  20. langfun/core/data/conversion/openai_test.py +161 -17
  21. langfun/core/eval/base.py +47 -43
  22. langfun/core/eval/base_test.py +5 -5
  23. langfun/core/eval/matching.py +5 -2
  24. langfun/core/eval/patching.py +3 -3
  25. langfun/core/eval/scoring.py +4 -3
  26. langfun/core/eval/v2/__init__.py +1 -0
  27. langfun/core/eval/v2/checkpointing.py +64 -6
  28. langfun/core/eval/v2/checkpointing_test.py +9 -2
  29. langfun/core/eval/v2/eval_test_helper.py +103 -2
  30. langfun/core/eval/v2/evaluation.py +91 -16
  31. langfun/core/eval/v2/evaluation_test.py +9 -3
  32. langfun/core/eval/v2/example.py +50 -40
  33. langfun/core/eval/v2/example_test.py +16 -8
  34. langfun/core/eval/v2/experiment.py +74 -8
  35. langfun/core/eval/v2/experiment_test.py +19 -0
  36. langfun/core/eval/v2/metric_values.py +31 -3
  37. langfun/core/eval/v2/metric_values_test.py +32 -0
  38. langfun/core/eval/v2/metrics.py +157 -44
  39. langfun/core/eval/v2/metrics_test.py +39 -18
  40. langfun/core/eval/v2/progress.py +30 -1
  41. langfun/core/eval/v2/progress_test.py +27 -0
  42. langfun/core/eval/v2/progress_tracking.py +12 -3
  43. langfun/core/eval/v2/progress_tracking_test.py +6 -1
  44. langfun/core/eval/v2/reporting.py +90 -71
  45. langfun/core/eval/v2/reporting_test.py +24 -6
  46. langfun/core/eval/v2/runners/__init__.py +30 -0
  47. langfun/core/eval/v2/{runners.py → runners/base.py} +59 -142
  48. langfun/core/eval/v2/runners/beam.py +341 -0
  49. langfun/core/eval/v2/runners/beam_test.py +131 -0
  50. langfun/core/eval/v2/runners/ckpt_monitor.py +294 -0
  51. langfun/core/eval/v2/runners/ckpt_monitor_test.py +162 -0
  52. langfun/core/eval/v2/runners/debug.py +40 -0
  53. langfun/core/eval/v2/runners/debug_test.py +76 -0
  54. langfun/core/eval/v2/runners/parallel.py +100 -0
  55. langfun/core/eval/v2/runners/parallel_test.py +95 -0
  56. langfun/core/eval/v2/runners/sequential.py +47 -0
  57. langfun/core/eval/v2/runners/sequential_test.py +172 -0
  58. langfun/core/langfunc.py +45 -130
  59. langfun/core/langfunc_test.py +7 -5
  60. langfun/core/language_model.py +141 -21
  61. langfun/core/language_model_test.py +54 -3
  62. langfun/core/llms/__init__.py +9 -1
  63. langfun/core/llms/anthropic.py +157 -2
  64. langfun/core/llms/azure_openai.py +29 -17
  65. langfun/core/llms/cache/base.py +25 -3
  66. langfun/core/llms/cache/in_memory.py +48 -7
  67. langfun/core/llms/cache/in_memory_test.py +14 -4
  68. langfun/core/llms/compositional.py +25 -1
  69. langfun/core/llms/deepseek.py +30 -2
  70. langfun/core/llms/fake.py +32 -1
  71. langfun/core/llms/gemini.py +55 -17
  72. langfun/core/llms/gemini_test.py +84 -0
  73. langfun/core/llms/google_genai.py +34 -1
  74. langfun/core/llms/groq.py +28 -3
  75. langfun/core/llms/llama_cpp.py +23 -4
  76. langfun/core/llms/openai.py +36 -3
  77. langfun/core/llms/openai_compatible.py +148 -27
  78. langfun/core/llms/openai_compatible_test.py +207 -20
  79. langfun/core/llms/openai_test.py +0 -2
  80. langfun/core/llms/rest.py +12 -1
  81. langfun/core/llms/vertexai.py +58 -8
  82. langfun/core/logging.py +1 -1
  83. langfun/core/mcp/client.py +77 -22
  84. langfun/core/mcp/client_test.py +8 -35
  85. langfun/core/mcp/session.py +94 -29
  86. langfun/core/mcp/session_test.py +54 -0
  87. langfun/core/mcp/tool.py +151 -22
  88. langfun/core/mcp/tool_test.py +197 -0
  89. langfun/core/memory.py +1 -0
  90. langfun/core/message.py +160 -55
  91. langfun/core/message_test.py +65 -81
  92. langfun/core/modalities/__init__.py +8 -0
  93. langfun/core/modalities/audio.py +21 -1
  94. langfun/core/modalities/image.py +19 -1
  95. langfun/core/modalities/mime.py +64 -3
  96. langfun/core/modalities/mime_test.py +11 -0
  97. langfun/core/modalities/pdf.py +19 -1
  98. langfun/core/modalities/video.py +21 -1
  99. langfun/core/modality.py +167 -29
  100. langfun/core/modality_test.py +42 -12
  101. langfun/core/natural_language.py +1 -1
  102. langfun/core/sampling.py +4 -4
  103. langfun/core/sampling_test.py +20 -4
  104. langfun/core/structured/__init__.py +2 -24
  105. langfun/core/structured/completion.py +34 -44
  106. langfun/core/structured/completion_test.py +23 -43
  107. langfun/core/structured/description.py +54 -50
  108. langfun/core/structured/function_generation.py +29 -12
  109. langfun/core/structured/mapping.py +81 -37
  110. langfun/core/structured/parsing.py +95 -79
  111. langfun/core/structured/parsing_test.py +0 -3
  112. langfun/core/structured/querying.py +215 -142
  113. langfun/core/structured/querying_test.py +65 -29
  114. langfun/core/structured/schema/__init__.py +49 -0
  115. langfun/core/structured/schema/base.py +664 -0
  116. langfun/core/structured/schema/base_test.py +531 -0
  117. langfun/core/structured/schema/json.py +174 -0
  118. langfun/core/structured/schema/json_test.py +121 -0
  119. langfun/core/structured/schema/python.py +316 -0
  120. langfun/core/structured/schema/python_test.py +410 -0
  121. langfun/core/structured/schema_generation.py +33 -14
  122. langfun/core/structured/scoring.py +47 -36
  123. langfun/core/structured/tokenization.py +26 -11
  124. langfun/core/subscription.py +2 -2
  125. langfun/core/template.py +174 -49
  126. langfun/core/template_test.py +123 -17
  127. langfun/env/__init__.py +8 -2
  128. langfun/env/base_environment.py +320 -128
  129. langfun/env/base_environment_test.py +473 -0
  130. langfun/env/base_feature.py +92 -15
  131. langfun/env/base_feature_test.py +228 -0
  132. langfun/env/base_sandbox.py +84 -361
  133. langfun/env/base_sandbox_test.py +1235 -0
  134. langfun/env/event_handlers/__init__.py +1 -1
  135. langfun/env/event_handlers/chain.py +233 -0
  136. langfun/env/event_handlers/chain_test.py +253 -0
  137. langfun/env/event_handlers/event_logger.py +95 -98
  138. langfun/env/event_handlers/event_logger_test.py +21 -21
  139. langfun/env/event_handlers/metric_writer.py +225 -140
  140. langfun/env/event_handlers/metric_writer_test.py +23 -6
  141. langfun/env/interface.py +854 -40
  142. langfun/env/interface_test.py +112 -2
  143. langfun/env/load_balancers_test.py +23 -2
  144. langfun/env/test_utils.py +126 -84
  145. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/METADATA +1 -1
  146. langfun-0.1.2.dev202511270805.dist-info/RECORD +215 -0
  147. langfun/core/eval/v2/runners_test.py +0 -343
  148. langfun/core/structured/schema.py +0 -987
  149. langfun/core/structured/schema_test.py +0 -982
  150. langfun/env/base_test.py +0 -1481
  151. langfun/env/event_handlers/base.py +0 -350
  152. langfun-0.1.2.dev202510230805.dist-info/RECORD +0 -195
  153. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/WHEEL +0 -0
  154. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/licenses/LICENSE +0 -0
  155. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/top_level.txt +0 -0
@@ -14,9 +14,11 @@
14
14
  """Base classes for agentic actions."""
15
15
 
16
16
  import abc
17
+ import collections
17
18
  import contextlib
18
19
  import dataclasses
19
20
  import functools
21
+ import itertools
20
22
  import threading
21
23
  import time
22
24
  import typing
@@ -36,7 +38,12 @@ class ActionTimeoutError(ActionError):
36
38
 
37
39
 
38
40
  class Action(pg.Object):
39
- """Base class for Langfun's agentic actions.
41
+ """Base class for agentic actions.
42
+
43
+ An `Action` represents a single, executable step or task that an agent can
44
+ perform, such as calling a tool, querying a language model, or returning a
45
+ final answer. Actions are designed to be composable and trackable within a
46
+ `Session`.
40
47
 
41
48
  # Developing Actions
42
49
 
@@ -149,7 +156,7 @@ class Action(pg.Object):
149
156
 
150
157
  # Explicitly create and pass a session.
151
158
  with lf.Session(id='my_agent_session') as session:
152
- result = calc(session=session) # Pass the session explicitly
159
+ result = calc(session=session) # Pass the session explicitly
153
160
  print(result)
154
161
  ```
155
162
 
@@ -308,19 +315,190 @@ class Action(pg.Object):
308
315
  """
309
316
 
310
317
 
318
+ #
319
+ # Execution tracking.
320
+ #
321
+
322
+
323
+ class ExecutionUnit(pg.Object):
324
+ """Base class for execution units in an agentic trajectory.
325
+
326
+ An `ExecutionUnit` represents a logical step or container in the agent's
327
+ execution flow. It serves as the common interface for top-level executable
328
+ items.
329
+
330
+ The concrete subclasses of `ExecutionUnit` are typically:
331
+ * **`ActionInvocation`**: Represents a single, specific action executed by
332
+ the agent.
333
+ * **`ParallelExecutions`**: Represents a container for a group of
334
+ `ExecutionUnits` that were executed concurrently.
335
+
336
+ Users can retrieve the immediate child execution units from an `ExecutionUnit`
337
+ object. To access the leaf nodes of the execution tree, use `all_actions`,
338
+ `all_queries`, and `all_logs` instead.
339
+
340
+ Each unit exposes a **`position`** property to reveal its specific location
341
+ within the execution hierarchy (e.g., '1.2.3').
342
+
343
+ Users could use **`parent_execution_unit`** to get the parent execution unit
344
+ of the current execution unit.
345
+ """
346
+
347
+ @dataclasses.dataclass
348
+ class Position:
349
+ """The position of an executed unit under current session."""
350
+
351
+ parent: Optional['ExecutionUnit.Position'] = None
352
+ index: int = 0
353
+
354
+ def indices(self) -> tuple[int, ...]:
355
+ """Returns the indices from root to current execution unit."""
356
+ # A deque is efficient for adding items to the front.
357
+ path = collections.deque()
358
+ current_pos = self
359
+
360
+ # Traverse up from the current position to the root.
361
+ while current_pos.parent is not None:
362
+ path.appendleft(current_pos.index)
363
+ current_pos = current_pos.parent
364
+
365
+ path.appendleft(current_pos.index)
366
+ return tuple(path)
367
+
368
+ def to_str(
369
+ self,
370
+ *,
371
+ index_base: int = 1,
372
+ separator: str = '.',
373
+ **kwargs
374
+ ) -> str:
375
+ """Returns a string description of the position."""
376
+ # For root action, we return empty string as it's position descriptor.
377
+ if self.parent is None:
378
+ return ''
379
+ parent_descriptor = self.parent.to_str(
380
+ index_base=index_base,
381
+ separator=separator,
382
+ **kwargs
383
+ )
384
+ if not parent_descriptor:
385
+ return f'{self.index + index_base}'
386
+ return parent_descriptor + separator + f'{self.index + index_base}'
387
+
388
+ def __repr__(self) -> str:
389
+ return f'Position({", ".join(str(x) for x in self.indices())})'
390
+
391
+ def __str__(self) -> str:
392
+ return self.to_str()
393
+
394
+ def __eq__(self, other: 'ExecutionUnit.Position') -> bool:
395
+ if isinstance(other, ExecutionUnit.Position):
396
+ return self.indices() == other.indices()
397
+ if isinstance(other, tuple):
398
+ return self.indices() == other
399
+ if isinstance(other, str):
400
+ return str(self) == other
401
+ return False
402
+
403
+ def __ne__(self, other: 'ExecutionUnit.Position') -> bool:
404
+ return not self == other
405
+
406
+ def __hash__(self) -> int:
407
+ return hash(self.indices())
408
+
409
+ def __lt__(self, other: 'ExecutionUnit.Position') -> bool:
410
+ return self.indices() < other.indices()
411
+
412
+ def __gt__(self, other: 'ExecutionUnit.Position') -> bool:
413
+ return self.indices() > other.indices()
414
+
415
+ def _on_parent_change(self, *args, **kwargs):
416
+ super()._on_parent_change(*args, **kwargs)
417
+ self.__dict__.pop('parent_execution_unit', None)
418
+ self.__dict__.pop('position', None)
419
+
420
+ @functools.cached_property
421
+ def parent_execution_unit(self) -> Optional['ExecutionUnit']:
422
+ """Returns the parent execution unit of the current execution unit."""
423
+ parent_trace = self.sym_ancestor(lambda x: isinstance(x, ExecutionTrace))
424
+ assert isinstance(parent_trace, ExecutionTrace), (
425
+ 'Execution unit is not associated with any `ExecutionTrace`: '
426
+ f'{self}'
427
+ )
428
+ return parent_trace.parent_execution_unit
429
+
430
+ @functools.cached_property
431
+ def position(self) -> Position:
432
+ """Returns the execution position of the action."""
433
+ parent_trace = self.sym_ancestor(lambda x: isinstance(x, ExecutionTrace))
434
+ while parent_trace is not None:
435
+ parent_position = parent_trace.position
436
+ if parent_position is not None:
437
+ return ExecutionUnit.Position(
438
+ parent_position, parent_trace.indexof(self, ExecutionUnit)
439
+ )
440
+ parent_trace = parent_trace.sym_ancestor(
441
+ lambda x: isinstance(x, ExecutionTrace)
442
+ )
443
+ return ExecutionUnit.Position(None, 0)
444
+
445
+ @property
446
+ @abc.abstractmethod
447
+ def execution_units(
448
+ self,
449
+ ) -> list['ExecutionUnit']:
450
+ """Returns immediate child execution items."""
451
+
452
+ @property
453
+ @abc.abstractmethod
454
+ def queries(self) -> list[lf_structured.QueryInvocation]:
455
+ """Returns queries issued by the execution item."""
456
+
457
+ @property
458
+ @abc.abstractmethod
459
+ def actions(self) -> list['ActionInvocation']:
460
+ """Returns immediate child action invocations."""
461
+
462
+ @property
463
+ @abc.abstractmethod
464
+ def logs(self) -> list[lf.logging.LogEntry]:
465
+ """Returns immediate logs under current execution item."""
466
+
467
+ @property
468
+ @abc.abstractmethod
469
+ def all_queries(self) -> list[lf_structured.QueryInvocation]:
470
+ """Returns all queries from the subtree."""
471
+
472
+ @property
473
+ @abc.abstractmethod
474
+ def all_actions(self) -> list['ActionInvocation']:
475
+ """Returns all action invocations from the subtree."""
476
+
477
+ @property
478
+ @abc.abstractmethod
479
+ def all_logs(self) -> list[lf.logging.LogEntry]:
480
+ """Returns all logs from the subtree."""
481
+
482
+
311
483
  # Type definition for traced item during execution.
312
484
  TracedItem = Union[
485
+ ExecutionUnit,
313
486
  lf_structured.QueryInvocation,
314
- 'ActionInvocation',
315
487
  'ExecutionTrace',
316
- 'ParallelExecutions',
317
488
  # NOTE(daiyip): Consider remove log entry once we migrate existing agents.
318
489
  lf.logging.LogEntry,
319
490
  ]
320
491
 
321
492
 
322
493
  class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
323
- """Trace of the execution of an action."""
494
+ """Trace of an execution, containing queries, logs, and sub-actions.
495
+
496
+ `ExecutionTrace` records the sequence of operations performed during an
497
+ action's execution or within a specific phase of execution (demarcated by
498
+ `session.track_phase`). It captures `lf.query` calls, log entries, and
499
+ nested `ActionInvocation` objects in the order they occurred. It also
500
+ aggregates LLM usage summaries from its child items.
501
+ """
324
502
 
325
503
  name: Annotated[
326
504
  str | None,
@@ -328,7 +506,7 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
328
506
  'The name of the execution trace. If None, the trace is unnamed, '
329
507
  'which is the case for the top-level trace of an action. An '
330
508
  'execution trace could have sub-traces, called phases, which are '
331
- 'created and named by `session.phase()` context manager.'
509
+ 'created and named by `session.track_phase()` context manager.'
332
510
  )
333
511
  ] = None
334
512
 
@@ -360,9 +538,15 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
360
538
  def _on_parent_change(self, *args, **kwargs):
361
539
  super()._on_parent_change(*args, **kwargs)
362
540
  self.__dict__.pop('id', None)
541
+ self.__dict__.pop('parent_execution_unit', None)
542
+ self.__dict__.pop('position', None)
363
543
 
364
- def indexof(self, item: TracedItem, count_item_cls: Type[Any]) -> int:
365
- """Returns the index of the child items of given type."""
544
+ def indexof(
545
+ self,
546
+ item: TracedItem,
547
+ count_item_cls: Type[Any] | tuple[Type[Any], ...]
548
+ ) -> int:
549
+ """Returns the index of the child item of given type."""
366
550
  pos = 0
367
551
  for x in self._iter_children(count_item_cls):
368
552
  if x is item:
@@ -370,6 +554,52 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
370
554
  pos += 1
371
555
  return -1
372
556
 
557
+ @functools.cached_property
558
+ def parent_execution_unit(self) -> ExecutionUnit:
559
+ """Returns the parent execution unit of the current execution trace."""
560
+ parent = self.sym_parent
561
+ if isinstance(parent, ActionInvocation):
562
+ # Current execution trace is the body of an action.
563
+ return parent
564
+ elif isinstance(parent, pg.List):
565
+ container = parent.sym_parent
566
+ if isinstance(container, ParallelExecutions):
567
+ return container
568
+ elif isinstance(container, ExecutionTrace):
569
+ return container.parent_execution_unit
570
+ assert False, (
571
+ 'Execution trace is not associated with any `ActionInvocation` or '
572
+ f'`ParallelExecutions`: {self}'
573
+ )
574
+
575
+ @functools.cached_property
576
+ def position(self) -> ExecutionUnit.Position | None:
577
+ """Returns the execution position of the execution trace.
578
+
579
+ Returns:
580
+ The execution position of the execution trace, or None if the execution
581
+ trace is either not associated with an execution unit or the execution
582
+ trace is a phase under another execution trace.
583
+ """
584
+ parent = self.sym_parent
585
+ if isinstance(parent, ActionInvocation):
586
+ # Current execution trace is the body of an action.
587
+ return parent.position
588
+ elif isinstance(parent, pg.List):
589
+ container = parent.sym_parent
590
+ if isinstance(container, ParallelExecutions):
591
+ return ExecutionUnit.Position(
592
+ container.position, self.sym_path.key
593
+ )
594
+ elif isinstance(container, ExecutionTrace):
595
+ # When execution trace is a phase under another execution trace,
596
+ # we return None as the position.
597
+ return None
598
+ assert False, (
599
+ 'Execution trace is not associated with any `ActionInvocation` or '
600
+ f'`ParallelExecutions`: {self}'
601
+ )
602
+
373
603
  @functools.cached_property
374
604
  def id(self) -> str:
375
605
  parent = self.sym_parent
@@ -441,6 +671,11 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
441
671
  """Returns action invocations from the sequence."""
442
672
  return list(self._iter_children(ActionInvocation))
443
673
 
674
+ @property
675
+ def execution_units(self) -> list[ExecutionUnit]:
676
+ """Returns parallel executions from the sequence."""
677
+ return list(self._iter_children(ExecutionUnit))
678
+
444
679
  @property
445
680
  def logs(self) -> list[lf.logging.LogEntry]:
446
681
  """Returns logs from the sequence."""
@@ -461,7 +696,9 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
461
696
  """Returns all logs from current trace and its child execution items."""
462
697
  return list(self._iter_subtree(lf.logging.LogEntry))
463
698
 
464
- def _iter_children(self, item_cls: Type[Any]) -> Iterator[TracedItem]:
699
+ def _iter_children(
700
+ self, item_cls: Type[Any] | tuple[Type[Any], ...]
701
+ ) -> Iterator[TracedItem]:
465
702
  for item in self.items:
466
703
  if isinstance(item, item_cls):
467
704
  yield item
@@ -473,7 +710,10 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
473
710
  for x in branch._iter_children(item_cls): # pylint: disable=protected-access
474
711
  yield x
475
712
 
476
- def _iter_subtree(self, item_cls: Type[Any]) -> Iterator[TracedItem]:
713
+ def _iter_subtree(
714
+ self,
715
+ item_cls: Type[Any] | tuple[Type[Any], ...]
716
+ ) -> Iterator[TracedItem]:
477
717
  for item in self.items:
478
718
  if isinstance(item, item_cls):
479
719
  yield item
@@ -538,6 +778,18 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
538
778
  remove_class=['not-started'],
539
779
  )
540
780
 
781
+ def remove(self, item: TracedItem) -> None:
782
+ """Removes an item from the sequence."""
783
+ index = self.items.index(item)
784
+ if index == -1:
785
+ raise ValueError(f'Item not found in execution trace: {item!r}')
786
+
787
+ with pg.notify_on_change(False):
788
+ self.items.pop(index)
789
+
790
+ if self._tab_control is not None:
791
+ self._tab_control.remove(index)
792
+
541
793
  def extend(self, items: Iterable[TracedItem]) -> None:
542
794
  """Extends the sequence with a list of items."""
543
795
  for item in items:
@@ -774,8 +1026,13 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
774
1026
  ]
775
1027
 
776
1028
 
777
- class ParallelExecutions(pg.Object, pg.views.html.HtmlTreeView.Extension):
778
- """A class for encapsulating parallel execution traces."""
1029
+ class ParallelExecutions(ExecutionUnit, pg.views.html.HtmlTreeView.Extension):
1030
+ """A container for multiple parallel execution traces.
1031
+
1032
+ When `session.concurrent_map` is used, it creates a `ParallelExecutions`
1033
+ object to hold an `ExecutionTrace` for each parallel branch of execution,
1034
+ allowing inspection of parallel workflows.
1035
+ """
779
1036
 
780
1037
  name: Annotated[
781
1038
  str | None,
@@ -821,8 +1078,64 @@ class ParallelExecutions(pg.Object, pg.views.html.HtmlTreeView.Extension):
821
1078
  self.branches.append(branch)
822
1079
  if self._tab_control is not None:
823
1080
  self._tab_control.append(self._branch_tab(branch))
1081
+
1082
+ # Invalidate cached properties.
1083
+ self.__dict__.pop('all_queries', None)
1084
+ self.__dict__.pop('all_actions', None)
1085
+ self.__dict__.pop('all_logs', None)
824
1086
  return branch
825
1087
 
1088
+ #
1089
+ # ExecutionUnit interface.
1090
+ #
1091
+
1092
+ @property
1093
+ def execution_units(self) -> list[ExecutionUnit]:
1094
+ """Returns immediate child execution items from execution sequence."""
1095
+ return []
1096
+
1097
+ @property
1098
+ def queries(self) -> list[lf_structured.QueryInvocation]:
1099
+ """Returns immediate queries made by the parallel execution."""
1100
+ return []
1101
+
1102
+ @property
1103
+ def actions(self) -> list['ActionInvocation']:
1104
+ """Returns immediate child action invocations."""
1105
+ return []
1106
+
1107
+ @property
1108
+ def logs(self) -> list[lf.logging.LogEntry]:
1109
+ """Returns immediate child logs from execution sequence."""
1110
+ return []
1111
+
1112
+ @functools.cached_property
1113
+ def all_queries(self) -> list[lf_structured.QueryInvocation]:
1114
+ """Returns all queries made by the action and its child execution items."""
1115
+ return list(
1116
+ itertools.chain.from_iterable(
1117
+ branch.all_queries for branch in self.branches
1118
+ )
1119
+ )
1120
+
1121
+ @functools.cached_property
1122
+ def all_actions(self) -> list['ActionInvocation']:
1123
+ """Returns all actions made by the action and its child execution items."""
1124
+ return list(
1125
+ itertools.chain.from_iterable(
1126
+ branch.all_actions for branch in self.branches
1127
+ )
1128
+ )
1129
+
1130
+ @property
1131
+ def all_logs(self) -> list[lf.logging.LogEntry]:
1132
+ """Returns all logs made by the action and its child execution items."""
1133
+ return list(
1134
+ itertools.chain.from_iterable(
1135
+ branch.all_logs for branch in self.branches
1136
+ )
1137
+ )
1138
+
826
1139
  #
827
1140
  # HTML views.
828
1141
  #
@@ -863,8 +1176,15 @@ class ParallelExecutions(pg.Object, pg.views.html.HtmlTreeView.Extension):
863
1176
  )
864
1177
 
865
1178
 
866
- class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
867
- """A class for capturing the invocation of an action."""
1179
+ class ActionInvocation(ExecutionUnit, pg.views.html.HtmlTreeView.Extension):
1180
+ """An invocation of an action, capturing its execution and result.
1181
+
1182
+ `ActionInvocation` represents a single call to an `Action`. It contains
1183
+ the `Action` object itself, its result or error, associated metadata,
1184
+ and an `ExecutionTrace` detailing the steps taken during its execution
1185
+ (queries, logs, sub-actions). Invocations form a tree structure within a
1186
+ `Session`, reflecting the hierarchy of agentic operations.
1187
+ """
868
1188
 
869
1189
  action: Annotated[
870
1190
  Action,
@@ -917,6 +1237,7 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
917
1237
  def _on_parent_change(self, *args, **kwargs):
918
1238
  super()._on_parent_change(*args, **kwargs)
919
1239
  self.__dict__.pop('id', None)
1240
+ self.__dict__.pop('position', None)
920
1241
 
921
1242
  @property
922
1243
  def parent_action(self) -> Optional['ActionInvocation']:
@@ -953,6 +1274,15 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
953
1274
  """Returns the state of the action."""
954
1275
  return self.action.state
955
1276
 
1277
+ #
1278
+ # Implement `ExecutionUnit` interface.
1279
+ #
1280
+
1281
+ @property
1282
+ def execution_units(self) -> list[ExecutionUnit]:
1283
+ """Returns immediate child execution items from execution sequence."""
1284
+ return self.execution.execution_units
1285
+
956
1286
  @property
957
1287
  def logs(self) -> list[lf.logging.LogEntry]:
958
1288
  """Returns immediate child logs from execution sequence."""
@@ -1004,10 +1334,12 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
1004
1334
  metadata: dict[str, Any] | None = None,
1005
1335
  ) -> None:
1006
1336
  """Ends the execution of the action with result and metadata."""
1007
- rebind_dict = dict(result=result, error=error)
1008
- if metadata is not None:
1009
- rebind_dict['metadata'] = metadata
1010
- self.rebind(**rebind_dict, skip_notification=True, raise_on_no_change=False)
1337
+ with pg.notify_on_change(False):
1338
+ self.result = result
1339
+ self.error = error
1340
+ if metadata:
1341
+ self.metadata.update(metadata)
1342
+
1011
1343
  self.execution.stop()
1012
1344
  if self._tab_control is not None:
1013
1345
  if self.metadata:
@@ -1165,6 +1497,19 @@ class RootAction(Action):
1165
1497
  class SessionEventHandler:
1166
1498
  """Interface for handling session events."""
1167
1499
 
1500
+ def get(
1501
+ self,
1502
+ session_cls: type['SessionEventHandler']
1503
+ ) -> Optional['SessionEventHandler']:
1504
+ """Returns this or a child event handler for the given session class."""
1505
+ if isinstance(self, session_cls):
1506
+ return self
1507
+ elif isinstance(self, SessionEventHandlerChain):
1508
+ for handler in self.handlers:
1509
+ if v := handler.get(session_cls):
1510
+ return v
1511
+ return None
1512
+
1168
1513
  def on_session_start(
1169
1514
  self,
1170
1515
  session: 'Session'
@@ -1394,7 +1739,50 @@ class SessionLogging(SessionEventHandler):
1394
1739
 
1395
1740
 
1396
1741
  class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1397
- """Session for performing an agentic task."""
1742
+ """Manages the execution trajectory of agentic actions.
1743
+
1744
+ A `Session` tracks the execution of a root `Action` and all its
1745
+ sub-actions, including LLM queries (`lf.query`), logging messages,
1746
+ and nested actions. It provides a complete, hierarchical trace of an
1747
+ agent's workflow, which is important for debugging, analysis, and
1748
+ visualization.
1749
+
1750
+ Sessions can be created implicitly when an action is called without an
1751
+ active session, or explicitly for more control.
1752
+
1753
+ **1. Implicit Session:**
1754
+ When an action is called without a session, Langfun creates one automatically.
1755
+
1756
+ ```python
1757
+ action = MyAction()
1758
+ action()
1759
+ session = action.session # Access the implicit session
1760
+ ```
1761
+
1762
+ **2. Explicit Session:**
1763
+ Use a `with` statement to manage a session explicitly. This is useful for
1764
+ setting session IDs or capturing the trajectory of multiple top-level actions.
1765
+
1766
+ ```python
1767
+ with lf.Session(id='my-session') as session:
1768
+ action1()
1769
+ action2()
1770
+ ```
1771
+
1772
+ **3. Accessing Trajectory:**
1773
+ The `session.root` attribute provides access to the `ActionInvocation` tree.
1774
+
1775
+ ```python
1776
+ with lf.Session() as session:
1777
+ my_action()
1778
+
1779
+ # Get all queries in the session
1780
+ print(session.all_queries)
1781
+
1782
+ # Get all top-level action calls in the session
1783
+ print(session.root.actions)
1784
+ ```
1785
+ """
1398
1786
 
1399
1787
  root: Annotated[
1400
1788
  ActionInvocation,
@@ -1406,11 +1794,6 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1406
1794
  'An optional identifier for the session, which will be used for logging.'
1407
1795
  ]
1408
1796
 
1409
- event_handler: Annotated[
1410
- SessionEventHandler,
1411
- 'Event handler for the session.'
1412
- ]
1413
-
1414
1797
  @pg.explicit_method_override
1415
1798
  def __init__(
1416
1799
  self,
@@ -1424,14 +1807,33 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1424
1807
  super().__init__(
1425
1808
  id=id,
1426
1809
  root=root or ActionInvocation(RootAction()),
1427
- event_handler=event_handler or SessionLogging(verbose=verbose),
1428
1810
  **kwargs
1429
1811
  )
1812
+ self._event_handler = event_handler or SessionLogging(verbose=verbose)
1813
+
1814
+ @property
1815
+ def event_handler(self) -> SessionEventHandler:
1816
+ """Returns the event handler for the session."""
1817
+ return self._event_handler
1818
+
1819
+ def _sym_clone(self, deep: bool, memo: Any = None) -> 'Session':
1820
+ other = super()._sym_clone(deep=deep, memo=memo)
1821
+ if deep:
1822
+ event_handler = pg.clone(self.event_handler, deep=deep, memo=memo)
1823
+ else:
1824
+ event_handler = self.event_handler
1825
+ other._event_handler = event_handler # pylint: disable=protected-access
1826
+ return other
1430
1827
 
1431
1828
  #
1432
1829
  # Shortcut methods for accessing the root action invocation.
1433
1830
  #
1434
1831
 
1832
+ @property
1833
+ def metadata(self) -> dict[str, Any]:
1834
+ """Returns metadata associated with the root of the session."""
1835
+ return self.root.metadata
1836
+
1435
1837
  @property
1436
1838
  def all_queries(self) -> list[lf_structured.QueryInvocation]:
1437
1839
  """Returns all queries made by the session."""
@@ -1547,7 +1949,7 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1547
1949
  )
1548
1950
 
1549
1951
  def update_progress(self, title: str, **kwargs: Any) -> None:
1550
- """Update the progress of current action's execution.
1952
+ """Updates the progress of current action's execution.
1551
1953
 
1552
1954
  Args:
1553
1955
  title: The title of the progress update.
@@ -1648,13 +2050,20 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1648
2050
  @contextlib.contextmanager
1649
2051
  def track_queries(
1650
2052
  self,
1651
- phase: str | None = None
2053
+ phase: str | None = None,
2054
+ track_if: Callable[
2055
+ [lf_structured.QueryInvocation],
2056
+ bool
2057
+ ] | None = None,
1652
2058
  ) -> Iterator[list[lf_structured.QueryInvocation]]:
1653
2059
  """Tracks `lf.query` made within the context.
1654
2060
 
1655
2061
  Args:
1656
2062
  phase: The name of a new phase to track the queries in. If not provided,
1657
2063
  the queries will be tracked in the parent phase.
2064
+ track_if: A function that takes a `lf_structured.QueryInvocation` and
2065
+ returns True if the query should be included in the result. If None,
2066
+ all queries (including failed queries) will be included.
1658
2067
 
1659
2068
  Yields:
1660
2069
  A list of `lf.QueryInvocation` objects, each for a single `lf.query`
@@ -1673,6 +2082,11 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1673
2082
  self.event_handler.on_query_start(self, self._current_action, invocation)
1674
2083
 
1675
2084
  def _query_end(invocation: lf_structured.QueryInvocation):
2085
+ if track_if is not None and not track_if(invocation):
2086
+ self._current_execution.remove(invocation)
2087
+ # Even if the query is not included in the execution trace, we still
2088
+ # count the usage summary to the current execution and trigger the
2089
+ # event handler to log the query.
1676
2090
  self._current_execution.merge_usage_summary(invocation.usage_summary)
1677
2091
  self.event_handler.on_query_end(self, self._current_action, invocation)
1678
2092
 
@@ -1705,8 +2119,9 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1705
2119
  *,
1706
2120
  lm: lf.LanguageModel,
1707
2121
  examples: list[lf_structured.MappingExample] | None = None,
2122
+ track_if: Callable[[lf_structured.QueryInvocation], bool] | None = None,
1708
2123
  **kwargs
1709
- ) -> Any:
2124
+ ) -> Any:
1710
2125
  """Calls `lf.query` and associates it with the current invocation.
1711
2126
 
1712
2127
  The following code are equivalent:
@@ -1731,12 +2146,15 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1731
2146
  default: The default value to return if the query fails.
1732
2147
  lm: The language model to use for the query.
1733
2148
  examples: The examples to use for the query.
2149
+ track_if: A function that takes a `lf_structured.QueryInvocation`
2150
+ and returns True if the query should be tracked.
2151
+ If None, all queries (including failed queries) will be tracked.
1734
2152
  **kwargs: Additional keyword arguments to pass to `lf.query`.
1735
2153
 
1736
2154
  Returns:
1737
2155
  The result of the query.
1738
2156
  """
1739
- with self.track_queries():
2157
+ with self.track_queries(track_if=track_if):
1740
2158
  return lf_structured.query(
1741
2159
  prompt,
1742
2160
  schema=schema,