langfun 0.1.2.dev202504270803__py3-none-any.whl → 0.1.2.dev202504290805__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.

@@ -15,6 +15,7 @@
15
15
 
16
16
  import abc
17
17
  import contextlib
18
+ import functools
18
19
  import threading
19
20
  import time
20
21
  import typing
@@ -31,8 +32,7 @@ class Action(pg.Object):
31
32
  def _on_bound(self):
32
33
  super()._on_bound()
33
34
  self._session = None
34
- self._result = None
35
- self._metadata = {}
35
+ self._invocation: ActionInvocation | None = None
36
36
 
37
37
  @property
38
38
  def session(self) -> Optional['Session']:
@@ -42,12 +42,12 @@ class Action(pg.Object):
42
42
  @property
43
43
  def result(self) -> Any:
44
44
  """Returns the result of the action."""
45
- return self._result
45
+ return self._invocation.result if self._invocation else None
46
46
 
47
47
  @property
48
48
  def metadata(self) -> dict[str, Any] | None:
49
49
  """Returns the metadata associated with the result from previous call."""
50
- return self._metadata
50
+ return self._invocation.metadata if self._invocation else None
51
51
 
52
52
  def __call__(
53
53
  self,
@@ -58,31 +58,57 @@ class Action(pg.Object):
58
58
  **kwargs
59
59
  ) -> Any:
60
60
  """Executes the action."""
61
- new_session = session is None
62
- if new_session:
61
+ if session is None:
63
62
  session = Session()
64
63
  if show_progress:
65
64
  lf.console.display(pg.view(session, name='agent_session'))
66
65
 
67
- with session.track_action(self):
68
- if verbose:
69
- session.info(f'Executing action {self!r}...', keep=False)
70
-
71
- result = self.call(session=session, verbose=verbose, **kwargs)
66
+ # For the top-level action, we store the session in the metadata.
67
+ self._session = session
68
+ else:
69
+ self._session = None
72
70
 
71
+ with session.track_action(self) as invocation:
73
72
  if verbose:
74
- session.info(
75
- f'Action {self.__class__.__name__} completed successfully.',
76
- keep=False,
77
- result=result
78
- )
73
+ session.info('Action execution started.', keep=False, action=self)
79
74
 
80
- # For the top-level action, we store the session in the metadata.
81
- if new_session:
82
- self._session = session
83
- self._result = result
84
- self._metadata = session.current_action.metadata
85
- return self._result
75
+ try:
76
+ result = self.call(session=session, verbose=verbose, **kwargs)
77
+ self._invocation.end(result)
78
+ if verbose:
79
+ session.info(
80
+ (
81
+ f'Action execution succeeded in '
82
+ f'{self._invocation.execution.elapse:.2f} seconds.'
83
+ ),
84
+ keep=False,
85
+ result=result
86
+ )
87
+ except BaseException as e:
88
+ error = pg.utils.ErrorInfo.from_exception(e)
89
+ self._invocation.end(result=None, error=error)
90
+ if invocation.parent_action is session.root:
91
+ session.error(
92
+ (
93
+ f'Top-level action execution failed in '
94
+ f'{self._invocation.execution.elapse:.2f} seconds.'
95
+ ),
96
+ keep=True,
97
+ action=self,
98
+ error=error
99
+ )
100
+ else:
101
+ session.warning(
102
+ (
103
+ f'Action execution failed in '
104
+ f'{self._invocation.execution.elapse:.2f} seconds.'
105
+ ),
106
+ keep=False,
107
+ action=self,
108
+ error=error
109
+ )
110
+ raise
111
+ return result
86
112
 
87
113
  @abc.abstractmethod
88
114
  def call(self, session: 'Session', **kwargs) -> Any:
@@ -146,6 +172,38 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
146
172
  self._tab_control = None
147
173
  self._time_badge = None
148
174
 
175
+ def _on_parent_change(self, *args, **kwargs):
176
+ super()._on_parent_change(*args, **kwargs)
177
+ self.__dict__.pop('id', None)
178
+
179
+ def indexof(self, item: TracedItem, count_item_cls: Type[Any]) -> int:
180
+ """Returns the index of the child items of given type."""
181
+ pos = 0
182
+ for x in self._iter_children(count_item_cls):
183
+ if x is item:
184
+ return pos
185
+ pos += 1
186
+ return -1
187
+
188
+ @functools.cached_property
189
+ def id(self) -> str:
190
+ parent = self.sym_parent
191
+ if isinstance(parent, ActionInvocation):
192
+ # Current execution trace is the body of an action.
193
+ return parent.id
194
+ elif isinstance(parent, pg.List):
195
+ container = parent.sym_parent
196
+ if isinstance(container, ExecutionTrace):
197
+ # Current execution trace is a phase.
198
+ group_id = (
199
+ self.name or f'g{container.indexof(self, ExecutionTrace) + 1}'
200
+ )
201
+ return f'{container.id}/{group_id}'
202
+ elif isinstance(container, ParallelExecutions):
203
+ # Current execution trace is a parallel branch.
204
+ return f'{container.id}/b{self.sym_path.key + 1}'
205
+ return ''
206
+
149
207
  def reset(self) -> None:
150
208
  """Resets the execution trace."""
151
209
  self.rebind(items=[], skip_notification=True, raise_on_no_change=False)
@@ -194,58 +252,59 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
194
252
  @property
195
253
  def queries(self) -> list[lf_structured.QueryInvocation]:
196
254
  """Returns queries from the sequence."""
197
- return self._child_items(lf_structured.QueryInvocation)
255
+ return list(self._iter_children(lf_structured.QueryInvocation))
198
256
 
199
257
  @property
200
258
  def actions(self) -> list['ActionInvocation']:
201
259
  """Returns action invocations from the sequence."""
202
- return self._child_items(ActionInvocation)
260
+ return list(self._iter_children(ActionInvocation))
203
261
 
204
262
  @property
205
263
  def logs(self) -> list[lf.logging.LogEntry]:
206
264
  """Returns logs from the sequence."""
207
- return self._child_items(lf.logging.LogEntry)
265
+ return list(self._iter_children(lf.logging.LogEntry))
208
266
 
209
267
  @property
210
268
  def all_queries(self) -> list[lf_structured.QueryInvocation]:
211
269
  """Returns all queries from current trace and its child execution items."""
212
- return self._all_child_items(lf_structured.QueryInvocation)
270
+ return list(self._iter_subtree(lf_structured.QueryInvocation))
213
271
 
214
272
  @property
215
273
  def all_actions(self) -> list['ActionInvocation']:
216
274
  """Returns all actions from current trace and its child execution items."""
217
- return self._all_child_items(ActionInvocation)
275
+ return list(self._iter_subtree(ActionInvocation))
218
276
 
219
277
  @property
220
278
  def all_logs(self) -> list[lf.logging.LogEntry]:
221
279
  """Returns all logs from current trace and its child execution items."""
222
- return self._all_child_items(lf.logging.LogEntry)
280
+ return list(self._iter_subtree(lf.logging.LogEntry))
223
281
 
224
- def _child_items(self, item_cls: Type[Any]) -> list[Any]:
225
- child_items = []
282
+ def _iter_children(self, item_cls: Type[Any]) -> Iterator[TracedItem]:
226
283
  for item in self.items:
227
284
  if isinstance(item, item_cls):
228
- child_items.append(item)
285
+ yield item
229
286
  elif isinstance(item, ExecutionTrace):
230
- child_items.extend(item._child_items(item_cls)) # pylint: disable=protected-access
287
+ for x in item._iter_children(item_cls): # pylint: disable=protected-access
288
+ yield x
231
289
  elif isinstance(item, ParallelExecutions):
232
290
  for branch in item.branches:
233
- child_items.extend(branch._child_items(item_cls)) # pylint: disable=protected-access
234
- return child_items
291
+ for x in branch._iter_children(item_cls): # pylint: disable=protected-access
292
+ yield x
235
293
 
236
- def _all_child_items(self, item_cls: Type[Any]) -> list[Any]:
237
- child_items = []
294
+ def _iter_subtree(self, item_cls: Type[Any]) -> Iterator[TracedItem]:
238
295
  for item in self.items:
239
296
  if isinstance(item, item_cls):
240
- child_items.append(item)
297
+ yield item
241
298
  if isinstance(item, ActionInvocation):
242
- child_items.extend(item.execution._all_child_items(item_cls)) # pylint: disable=protected-access
299
+ for x in item.execution._iter_subtree(item_cls): # pylint: disable=protected-access
300
+ yield x
243
301
  elif isinstance(item, ExecutionTrace):
244
- child_items.extend(item._all_child_items(item_cls)) # pylint: disable=protected-access
302
+ for x in item._iter_subtree(item_cls): # pylint: disable=protected-access
303
+ yield x
245
304
  elif isinstance(item, ParallelExecutions):
246
305
  for branch in item.branches:
247
- child_items.extend(branch._all_child_items(item_cls)) # pylint: disable=protected-access
248
- return child_items
306
+ for x in branch._iter_subtree(item_cls): # pylint: disable=protected-access
307
+ yield x
249
308
 
250
309
  def append(self, item: TracedItem) -> None:
251
310
  """Appends an item to the sequence."""
@@ -359,7 +418,7 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
359
418
  elif isinstance(item, lf_structured.QueryInvocation):
360
419
  css_class = 'query'
361
420
  elif isinstance(item, lf.logging.LogEntry):
362
- css_class = 'log'
421
+ css_class = f'log-{item.level}'
363
422
  elif isinstance(item, ExecutionTrace):
364
423
  css_class = 'phase'
365
424
  elif isinstance(item, ParallelExecutions):
@@ -379,13 +438,7 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
379
438
  if isinstance(item, ActionInvocation):
380
439
  return pg.views.html.controls.Label(
381
440
  item.action.__class__.__name__,
382
- tooltip=pg.format(
383
- item.action,
384
- verbose=False,
385
- hide_default_values=True,
386
- max_str_len=80,
387
- max_bytes_len=20,
388
- ),
441
+ tooltip=f'[{item.id}] Action invocation',
389
442
  )
390
443
  elif isinstance(item, lf_structured.QueryInvocation):
391
444
  schema_title = 'str'
@@ -393,30 +446,22 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
393
446
  schema_title = lf_structured.annotation(item.schema.spec)
394
447
  return pg.views.html.controls.Label(
395
448
  schema_title,
396
- tooltip=(
397
- pg.format(
398
- item.input,
399
- verbose=False,
400
- hide_default_values=True,
401
- max_str_len=80,
402
- max_bytes_len=20,
403
- )
404
- ),
449
+ tooltip=f'[{item.id}] lf.Query invocation'
405
450
  )
406
451
  elif isinstance(item, lf.logging.LogEntry):
407
452
  return pg.views.html.controls.Label(
408
- 'Log',
453
+ item.level.title(),
409
454
  tooltip=item.message,
410
455
  )
411
456
  elif isinstance(item, ExecutionTrace):
412
457
  return pg.views.html.controls.Label(
413
458
  item.name or 'Phase',
414
- tooltip=f'Execution phase {item.name!r}.'
459
+ tooltip=f'[{item.id}] Execution group {item.name!r}'
415
460
  )
416
461
  elif isinstance(item, ParallelExecutions):
417
462
  return pg.views.html.controls.Label(
418
463
  item.name or 'Parallel',
419
- tooltip='Parallel executions.'
464
+ tooltip=f'[{item.id}] Parallel executions'
420
465
  )
421
466
  else:
422
467
  raise ValueError(f'Unsupported item type: {type(item)}')
@@ -431,13 +476,13 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
431
476
  padding: 10px;
432
477
  }
433
478
  .tab-button.phase > ::before {
434
- content: "P";
479
+ content: "G";
435
480
  font-weight: bold;
436
481
  color: purple;
437
482
  padding: 10px;
438
483
  }
439
484
  .tab-button.parallel > ::before {
440
- content: "||";
485
+ content: "P";
441
486
  font-weight: bold;
442
487
  color: blue;
443
488
  padding: 10px;
@@ -448,11 +493,26 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
448
493
  color: orange;
449
494
  padding: 10px;
450
495
  }
451
- .tab-button.log > ::before {
452
- content: "L";
453
- font-weight: bold;
454
- color: green;
455
- padding: 10px;
496
+ .tab-button.log-debug > ::before {
497
+ content: "🔍";
498
+ padding: 7px;
499
+ }
500
+ .tab-button.log-info > ::before {
501
+ content: "ⓘ";
502
+ color: blue;
503
+ padding: 7px;
504
+ }
505
+ .tab-button.log-warning > ::before {
506
+ content: "❗";
507
+ padding: 7px;
508
+ }
509
+ .tab-button.log-error > ::before {
510
+ content: "‼️";
511
+ padding: 7px;
512
+ }
513
+ .tab-button.log-fatal > ::before {
514
+ content: "❌";
515
+ padding: 7px;
456
516
  }
457
517
  .details.execution-trace, .details.action-invocation {
458
518
  border: 1px solid #eee;
@@ -504,6 +564,22 @@ class ParallelExecutions(pg.Object, pg.views.html.HtmlTreeView.Extension):
504
564
  self._tab_control = None
505
565
  self._lock = threading.Lock()
506
566
 
567
+ def _on_parent_change(self, *args, **kwargs):
568
+ super()._on_parent_change(*args, **kwargs)
569
+ self.__dict__.pop('id', None)
570
+
571
+ @functools.cached_property
572
+ def id(self) -> str:
573
+ parent = self.sym_parent
574
+ if isinstance(parent, pg.List):
575
+ container = parent.sym_parent
576
+ if isinstance(container, ExecutionTrace):
577
+ parallel_id = (
578
+ self.name or f'p{container.indexof(self, ParallelExecutions) + 1}'
579
+ )
580
+ return f'{container.id}/{parallel_id}'
581
+ return ''
582
+
507
583
  def add(self) -> ExecutionTrace:
508
584
  """Appends a branch to the parallel execution."""
509
585
  with self._lock, pg.notify_on_change(False):
@@ -547,7 +623,7 @@ class ParallelExecutions(pg.Object, pg.views.html.HtmlTreeView.Extension):
547
623
  return pg.views.html.controls.Tab(
548
624
  label=pg.views.html.controls.Label(
549
625
  branch.name,
550
- tooltip=f'Execution thread {branch.name!r}.'
626
+ tooltip=f'[{branch.id}] Branch {branch.name!r}'
551
627
  ),
552
628
  content=pg.view(branch),
553
629
  )
@@ -567,6 +643,11 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
567
643
  'The metadata returned by the action.'
568
644
  ] = {}
569
645
 
646
+ error: Annotated[
647
+ pg.utils.ErrorInfo | None,
648
+ 'Error from the action if failed.'
649
+ ] = None
650
+
570
651
  execution: Annotated[
571
652
  ExecutionTrace,
572
653
  'The execution sequence of the action.'
@@ -579,6 +660,33 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
579
660
  super()._on_bound()
580
661
  self._tab_control = None
581
662
 
663
+ def _on_parent_change(self, *args, **kwargs):
664
+ super()._on_parent_change(*args, **kwargs)
665
+ self.__dict__.pop('id', None)
666
+
667
+ @property
668
+ def parent_action(self) -> Optional['ActionInvocation']:
669
+ """Returns the parent action invocation."""
670
+ return self.sym_ancestor(lambda x: isinstance(x, ActionInvocation))
671
+
672
+ @functools.cached_property
673
+ def id(self) -> str:
674
+ """Returns the id of the action invocation."""
675
+ parent = self.sym_parent
676
+ if isinstance(parent, Session):
677
+ return f'{parent.id}:'
678
+ elif isinstance(parent, pg.List):
679
+ container = parent.sym_parent
680
+ if isinstance(container, ExecutionTrace):
681
+ action_id = f'a{container.indexof(self, ActionInvocation) + 1}'
682
+ return f'{container.id}/{action_id}'
683
+ return ''
684
+
685
+ @property
686
+ def has_error(self) -> bool:
687
+ """Returns True if the action invocation has an error."""
688
+ return self.error is not None
689
+
582
690
  @property
583
691
  def logs(self) -> list[lf.logging.LogEntry]:
584
692
  """Returns immediate child logs from execution sequence."""
@@ -618,14 +726,17 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
618
726
  """Starts the execution of the action."""
619
727
  self.execution.start()
620
728
 
621
- def end(self, result: Any, metadata: dict[str, Any]) -> None:
729
+ def end(
730
+ self,
731
+ result: Any,
732
+ error: pg.utils.ErrorInfo | None = None,
733
+ metadata: dict[str, Any] | None = None,
734
+ ) -> None:
622
735
  """Ends the execution of the action with result and metadata."""
623
- self.rebind(
624
- result=result,
625
- metadata=metadata,
626
- skip_notification=True,
627
- raise_on_no_change=False
628
- )
736
+ rebind_dict = dict(result=result, error=error)
737
+ if metadata is not None:
738
+ rebind_dict['metadata'] = metadata
739
+ self.rebind(**rebind_dict, skip_notification=True, raise_on_no_change=False)
629
740
  self.execution.stop()
630
741
  if self._tab_control is not None:
631
742
  if self.metadata:
@@ -641,19 +752,33 @@ class ActionInvocation(pg.Object, pg.views.html.HtmlTreeView.Extension):
641
752
  name='metadata',
642
753
  )
643
754
  )
644
- self._tab_control.insert(
645
- 1,
646
- pg.views.html.controls.Tab(
647
- 'result',
648
- pg.view(
649
- self.result,
650
- collapse_level=None,
651
- enable_summary_tooltip=False
652
- ),
653
- name='result',
654
- ),
655
- )
656
- self._tab_control.select(['metadata', 'result'])
755
+ if self.has_error:
756
+ self._tab_control.insert(
757
+ 1,
758
+ pg.views.html.controls.Tab(
759
+ 'error',
760
+ pg.view(
761
+ self.error,
762
+ collapse_level=None,
763
+ enable_summary_tooltip=False
764
+ ),
765
+ name='error',
766
+ )
767
+ )
768
+ else:
769
+ self._tab_control.insert(
770
+ 1,
771
+ pg.views.html.controls.Tab(
772
+ 'result',
773
+ pg.view(
774
+ self.result,
775
+ collapse_level=None,
776
+ enable_summary_tooltip=False
777
+ ),
778
+ name='result',
779
+ ),
780
+ )
781
+ self._tab_control.select(['error', 'metadata', 'result'])
657
782
 
658
783
  #
659
784
  # HTML views.
@@ -806,7 +931,7 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
806
931
  self._current_execution = self.root.execution
807
932
  if self.id is None:
808
933
  self.rebind(
809
- id=f'session@{uuid.uuid4().hex[-7:]}',
934
+ id=f'agent@{uuid.uuid4().hex[-7:]}',
810
935
  skip_notification=True
811
936
  )
812
937
 
@@ -821,6 +946,7 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
821
946
  self._current_execution.start()
822
947
 
823
948
  invocation = ActionInvocation(pg.maybe_ref(action))
949
+ action._invocation = invocation # pylint: disable=protected-access
824
950
  parent_action = self._current_action
825
951
  parent_execution = self._current_execution
826
952
  parent_execution.append(invocation)
@@ -832,16 +958,18 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
832
958
  self._current_action.start()
833
959
  yield invocation
834
960
  finally:
835
- # Stop the execution of the current action.
836
- invocation.end(action.result, action.metadata)
837
961
  self._current_execution = parent_execution
838
962
  self._current_action = parent_action
839
963
  if parent_action is self.root:
840
- parent_action.end(result=action.result, metadata=action.metadata)
964
+ parent_action.end(
965
+ result=invocation.result,
966
+ metadata=invocation.metadata,
967
+ error=invocation.error
968
+ )
841
969
 
842
970
  @contextlib.contextmanager
843
971
  def track_phase(self, name: str | None) -> Iterator[ExecutionTrace]:
844
- """Context manager for starting a new execution phase."""
972
+ """Context manager for starting a new execution phase (group)."""
845
973
  parent_execution = self._current_execution
846
974
  if name is None:
847
975
  phase = parent_execution
@@ -878,6 +1006,12 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
878
1006
  try:
879
1007
  yield queries
880
1008
  finally:
1009
+ for i, query in enumerate(queries):
1010
+ query.rebind(
1011
+ id=f'{execution.id}/q{len(execution.queries) + i + 1}',
1012
+ skip_notification=False,
1013
+ raise_on_no_change=False
1014
+ )
881
1015
  execution.extend(queries)
882
1016
 
883
1017
  #
@@ -897,7 +1031,7 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
897
1031
  ] = None,
898
1032
  default: Any = lf.RAISE_IF_HAS_ERROR,
899
1033
  *,
900
- lm: lf.LanguageModel | None = None,
1034
+ lm: lf.LanguageModel,
901
1035
  examples: list[lf_structured.MappingExample] | None = None,
902
1036
  **kwargs
903
1037
  ) -> Any:
@@ -931,14 +1065,24 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
931
1065
  The result of the query.
932
1066
  """
933
1067
  with self.track_queries():
934
- return lf_structured.query(
935
- prompt,
936
- schema=schema,
937
- default=default,
938
- lm=lm,
939
- examples=examples,
940
- **kwargs
941
- )
1068
+ start_time = time.time()
1069
+ try:
1070
+ return lf_structured.query(
1071
+ prompt,
1072
+ schema=schema,
1073
+ default=default,
1074
+ lm=lm,
1075
+ examples=examples,
1076
+ **kwargs
1077
+ )
1078
+ except BaseException as e:
1079
+ elapse = time.time() - start_time
1080
+ self.warning(
1081
+ f'Failed to query LLM ({lm.model_id}) in {elapse:.2f} seconds.',
1082
+ error=pg.utils.ErrorInfo.from_exception(e),
1083
+ keep=False,
1084
+ )
1085
+ raise
942
1086
 
943
1087
  def concurrent_map(
944
1088
  self,
@@ -1069,11 +1213,15 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1069
1213
  **kwargs
1070
1214
  ) -> None:
1071
1215
  """Logs a message to the session."""
1216
+ action_name = self._current_action.action.__class__.__name__
1217
+ execution = self._current_execution
1072
1218
  log_entry = lf.logging.log(
1073
- level, f'[{self.id}]: {message}]', **kwargs
1219
+ level,
1220
+ f'[{execution.id} ({action_name})]: {message}',
1221
+ **kwargs
1074
1222
  )
1075
1223
  if keep:
1076
- self._current_execution.append(log_entry)
1224
+ execution.append(log_entry)
1077
1225
 
1078
1226
  def debug(self, message: str, keep: bool = True, **kwargs):
1079
1227
  """Logs a debug message to the session."""
@@ -34,9 +34,11 @@ class ActionEval(lf.eval.v2.Evaluation):
34
34
  def process(self, example: lf.eval.v2.Example) -> tuple[str, dict[str, Any]]:
35
35
  example_input = example.input
36
36
  action = example_input.action
37
- session = action_lib.Session(id=str(example.id))
37
+ session = action_lib.Session(id=f'{self.id}#example-{example.id}')
38
38
  with lf.logging.use_log_level('fatal'):
39
- action(session=session, **self.action_args)
39
+ kwargs = self.action_args.copy()
40
+ kwargs.update(verbose=True)
41
+ action(session=session, **kwargs)
40
42
  return session.final_result, dict(session=session)
41
43
 
42
44
 
@@ -22,54 +22,108 @@ import langfun.core.structured as lf_structured
22
22
  import pyglove as pg
23
23
 
24
24
 
25
- class SessionTest(unittest.TestCase):
25
+ class Bar(action_lib.Action):
26
+ simulate_action_error: bool = False
27
+
28
+ def call(self, session, *, lm, **kwargs):
29
+ assert session.current_action.action is self
30
+ session.info('Begin Bar')
31
+ session.query('bar', lm=lm)
32
+ session.add_metadata(note='bar')
33
+ if self.simulate_action_error:
34
+ raise ValueError('Bar error')
35
+ return 2
36
+
37
+
38
+ class Foo(action_lib.Action):
39
+ x: int
40
+ simulate_action_error: bool = False
41
+ simulate_query_error: bool = False
42
+
43
+ def call(self, session, *, lm, **kwargs):
44
+ assert session.current_action.action is self
45
+ with session.track_phase('prepare'):
46
+ session.info('Begin Foo', x=1)
47
+ session.query(
48
+ 'foo',
49
+ schema=int if self.simulate_query_error else None,
50
+ lm=lm
51
+ )
52
+ with session.track_queries():
53
+ self.make_additional_query(lm)
54
+ session.add_metadata(note='foo')
55
+
56
+ def _sub_task(i):
57
+ session.add_metadata(**{f'subtask_{i}': i})
58
+ return lf_structured.query(f'subtask_{i}', lm=lm)
59
+
60
+ for i, output, error in session.concurrent_map(
61
+ _sub_task, range(3), max_workers=2, silence_on_errors=None,
62
+ ):
63
+ assert isinstance(i, int), i
64
+ assert isinstance(output, str), output
65
+ assert error is None, error
66
+ return self.x + Bar(
67
+ simulate_action_error=self.simulate_action_error
68
+ )(session, lm=lm)
69
+
70
+ def make_additional_query(self, lm):
71
+ lf_structured.query('additional query', lm=lm)
72
+
73
+
74
+ class ActionInvocationTest(unittest.TestCase):
75
+
76
+ def test_basics(self):
77
+ action_invocation = action_lib.ActionInvocation(
78
+ action=Foo(1)
79
+ )
80
+ self.assertEqual(action_invocation.id, '')
81
+ root = action_lib.ActionInvocation(action=action_lib.RootAction())
82
+ root.execution.append(action_invocation)
83
+ self.assertIs(action_invocation.parent_action, root)
84
+ self.assertEqual(action_invocation.id, '/a1')
85
+
86
+
87
+ class ExecutionTraceTest(unittest.TestCase):
26
88
 
27
89
  def test_basics(self):
28
- test = self
29
-
30
- class Bar(action_lib.Action):
31
-
32
- def call(self, session, *, lm, **kwargs):
33
- test.assertIs(session.current_action.action, self)
34
- session.info('Begin Bar')
35
- session.query('bar', lm=lm)
36
- session.add_metadata(note='bar')
37
- return 2
38
-
39
- class Foo(action_lib.Action):
40
- x: int
41
-
42
- def call(self, session, *, lm, **kwargs):
43
- test.assertIs(session.current_action.action, self)
44
- with session.track_phase('prepare'):
45
- session.info('Begin Foo', x=1)
46
- session.query('foo', lm=lm)
47
- with session.track_queries():
48
- self.make_additional_query(lm)
49
- session.add_metadata(note='foo')
50
-
51
- def _sub_task(i):
52
- session.add_metadata(**{f'subtask_{i}': i})
53
- return lf_structured.query(f'subtask_{i}', lm=lm)
54
-
55
- for i, output, error in session.concurrent_map(
56
- _sub_task, range(3), max_workers=2, silence_on_errors=None,
57
- ):
58
- assert isinstance(i, int), i
59
- assert isinstance(output, str), output
60
- assert error is None, error
61
- return self.x + Bar()(session, lm=lm)
62
-
63
- def make_additional_query(self, lm):
64
- lf_structured.query('additional query', lm=lm)
90
+ execution = action_lib.ExecutionTrace()
91
+ self.assertEqual(execution.id, '')
65
92
 
93
+ root = action_lib.ActionInvocation(action=action_lib.RootAction())
94
+ action_invocation = action_lib.ActionInvocation(
95
+ action=Foo(1)
96
+ )
97
+ root.execution.append(action_invocation)
98
+ self.assertEqual(action_invocation.execution.id, '/a1')
99
+
100
+ root.execution.reset()
101
+ self.assertEqual(len(root.execution), 0)
102
+
103
+
104
+ class SessionTest(unittest.TestCase):
105
+
106
+ def test_succeeded_trajectory(self):
66
107
  lm = fake.StaticResponse('lm response')
67
108
  foo = Foo(1)
68
- self.assertEqual(foo(lm=lm, verbose=True), 3)
109
+ self.assertIsNone(foo.session)
110
+ self.assertIsNone(foo.result)
111
+ self.assertIsNone(foo.metadata)
112
+
113
+ session = action_lib.Session(id='agent@1')
114
+ self.assertEqual(session.id, 'agent@1')
115
+
116
+ # Render HTML view to trigger dynamic update during execution.
117
+ _ = session.to_html()
118
+
119
+ self.assertEqual(foo(session, lm=lm, verbose=True), 3)
120
+
121
+ self.assertIsNone(foo.session)
122
+ self.assertEqual(foo.result, 3)
123
+ self.assertEqual(
124
+ foo.metadata, dict(note='foo', subtask_0=0, subtask_1=1, subtask_2=2)
125
+ )
69
126
 
70
- session = foo.session
71
- self.assertIn('session@', session.id)
72
- self.assertIsNotNone(session)
73
127
  self.assertIsInstance(session.root.action, action_lib.RootAction)
74
128
  self.assertIs(session.current_action, session.root)
75
129
 
@@ -78,6 +132,9 @@ class SessionTest(unittest.TestCase):
78
132
  #
79
133
 
80
134
  root = session.root
135
+ self.assertIsNone(root.parent_action)
136
+ self.assertEqual(root.id, 'agent@1:')
137
+ self.assertEqual(root.execution.id, 'agent@1:')
81
138
  self.assertEqual(len(root.execution.items), 1)
82
139
  self.assertIs(root.execution.items[0].action, foo)
83
140
 
@@ -104,33 +161,57 @@ class SessionTest(unittest.TestCase):
104
161
 
105
162
  # Inspecting the top-level action (Foo)
106
163
  foo_invocation = root.execution.items[0]
164
+ self.assertIs(foo_invocation.parent_action, root)
165
+ self.assertEqual(foo_invocation.id, 'agent@1:/a1')
166
+ self.assertEqual(foo_invocation.execution.id, 'agent@1:/a1')
107
167
  self.assertEqual(len(foo_invocation.execution.items), 4)
108
168
 
109
169
  # Prepare phase.
110
170
  prepare_phase = foo_invocation.execution.items[0]
111
- self.assertIsInstance(
112
- prepare_phase, action_lib.ExecutionTrace
113
- )
171
+ self.assertIsInstance(prepare_phase, action_lib.ExecutionTrace)
172
+ self.assertEqual(prepare_phase.id, 'agent@1:/a1/prepare')
114
173
  self.assertEqual(len(prepare_phase.items), 2)
115
174
  self.assertTrue(prepare_phase.has_started)
116
175
  self.assertTrue(prepare_phase.has_stopped)
117
176
  self.assertEqual(prepare_phase.usage_summary.total.num_requests, 1)
177
+ self.assertIsInstance(prepare_phase.items[0], lf.logging.LogEntry)
178
+ self.assertIsInstance(prepare_phase.items[1], lf_structured.QueryInvocation)
179
+ self.assertEqual(prepare_phase.items[1].id, 'agent@1:/a1/prepare/q1')
118
180
 
119
181
  # Tracked queries.
120
182
  query_invocation = foo_invocation.execution.items[1]
121
183
  self.assertIsInstance(query_invocation, lf_structured.QueryInvocation)
184
+ self.assertEqual(query_invocation.id, 'agent@1:/a1/q2')
122
185
  self.assertIs(query_invocation.lm, lm)
186
+ self.assertEqual(
187
+ foo_invocation.execution.indexof(
188
+ query_invocation, lf_structured.QueryInvocation
189
+ ),
190
+ 1
191
+ )
192
+ self.assertEqual(
193
+ root.execution.indexof(
194
+ query_invocation, lf_structured.QueryInvocation
195
+ ),
196
+ -1
197
+ )
123
198
 
124
199
  # Tracked parallel executions.
125
200
  parallel_executions = foo_invocation.execution.items[2]
201
+ self.assertEqual(parallel_executions.id, 'agent@1:/a1/p1')
126
202
  self.assertIsInstance(parallel_executions, action_lib.ParallelExecutions)
127
203
  self.assertEqual(len(parallel_executions), 3)
204
+ self.assertEqual(parallel_executions[0].id, 'agent@1:/a1/p1/b1')
205
+ self.assertEqual(parallel_executions[1].id, 'agent@1:/a1/p1/b2')
206
+ self.assertEqual(parallel_executions[2].id, 'agent@1:/a1/p1/b3')
128
207
  self.assertEqual(len(parallel_executions[0].queries), 1)
129
208
  self.assertEqual(len(parallel_executions[1].queries), 1)
130
209
  self.assertEqual(len(parallel_executions[2].queries), 1)
131
210
 
132
211
  # Invocation to Bar.
133
212
  bar_invocation = foo_invocation.execution.items[3]
213
+ self.assertIs(bar_invocation.parent_action, foo_invocation)
214
+ self.assertEqual(bar_invocation.id, 'agent@1:/a1/a1')
134
215
  self.assertIsInstance(bar_invocation, action_lib.ActionInvocation)
135
216
  self.assertIsInstance(bar_invocation.action, Bar)
136
217
  self.assertEqual(bar_invocation.result, 2)
@@ -144,6 +225,51 @@ class SessionTest(unittest.TestCase):
144
225
  json_str = session.to_json_str(save_ref_value=True)
145
226
  self.assertIsInstance(pg.from_json_str(json_str), action_lib.Session)
146
227
 
228
+ def test_failed_action(self):
229
+ lm = fake.StaticResponse('lm response')
230
+ foo = Foo(1, simulate_action_error=True)
231
+ with self.assertRaisesRegex(ValueError, 'Bar error'):
232
+ foo(lm=lm)
233
+
234
+ session = foo.session
235
+ self.assertIsNotNone(session)
236
+ self.assertIsInstance(session.root.action, action_lib.RootAction)
237
+ self.assertIs(session.current_action, session.root)
238
+
239
+ # Inspecting the root invocation.
240
+ root = session.root
241
+ self.assertRegex(root.id, 'agent@.*:')
242
+ self.assertTrue(root.has_error)
243
+ foo_invocation = root.execution.items[0]
244
+ self.assertIsInstance(foo_invocation, action_lib.ActionInvocation)
245
+ self.assertTrue(foo_invocation.has_error)
246
+ bar_invocation = foo_invocation.execution.items[3]
247
+ self.assertIsInstance(bar_invocation, action_lib.ActionInvocation)
248
+ self.assertTrue(bar_invocation.has_error)
249
+
250
+ # Save to HTML
251
+ self.assertIn('error', session.to_html().content)
252
+
253
+ def test_failed_query(self):
254
+ lm = fake.StaticResponse('lm response')
255
+ foo = Foo(1, simulate_query_error=True)
256
+ with self.assertRaisesRegex(lf_structured.MappingError, 'SyntaxError'):
257
+ foo(lm=lm)
258
+
259
+ session = foo.session
260
+ self.assertIsNotNone(session)
261
+ self.assertIsInstance(session.root.action, action_lib.RootAction)
262
+ self.assertIs(session.current_action, session.root)
263
+
264
+ # Inspecting the root invocation.
265
+ root = session.root
266
+ self.assertRegex(root.id, 'agent@.*:')
267
+ self.assertTrue(root.has_error)
268
+ foo_invocation = root.execution.items[0]
269
+ self.assertIsInstance(foo_invocation, action_lib.ActionInvocation)
270
+ self.assertTrue(foo_invocation.has_error)
271
+ self.assertEqual(len(foo_invocation.execution.items), 2)
272
+
147
273
  def test_log(self):
148
274
  session = action_lib.Session()
149
275
  session.debug('hi', x=1, y=2)
@@ -153,8 +279,8 @@ class SessionTest(unittest.TestCase):
153
279
  session.fatal('hi', x=1, y=2)
154
280
 
155
281
  def test_as_message(self):
156
- session = action_lib.Session(id='abc')
157
- self.assertEqual(session.id, 'abc')
282
+ session = action_lib.Session()
283
+ self.assertIn('agent@', session.id)
158
284
  self.assertIsInstance(session.as_message(), lf.AIMessage)
159
285
 
160
286
 
@@ -456,6 +456,11 @@ def class_definition(
456
456
  )
457
457
  continue
458
458
 
459
+ # Skip fields that are marked as excluded from the prompt sent to LLM
460
+ # for OOP.
461
+ if field.metadata.get('exclude_from_prompt', False):
462
+ continue
463
+
459
464
  # Write field doc string as comments before the field definition.
460
465
  if field.description:
461
466
  for line in field.description.split('\n'):
@@ -538,6 +538,19 @@ class SchemaPythonReprTest(unittest.TestCase):
538
538
  """) + '\n'
539
539
  )
540
540
 
541
+ class E(pg.Object):
542
+ x: str
543
+ y: typing.Annotated[int, 'y', dict(exclude_from_prompt=True)]
544
+
545
+ self.assertEqual(
546
+ schema_lib.class_definition(E),
547
+ inspect.cleandoc(
548
+ """
549
+ class E(Object):
550
+ x: str
551
+ """) + '\n'
552
+ )
553
+
541
554
  def test_repr(self):
542
555
  class Foo(pg.Object):
543
556
  x: int
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfun
3
- Version: 0.1.2.dev202504270803
3
+ Version: 0.1.2.dev202504290805
4
4
  Summary: Langfun: Language as Functions.
5
5
  Home-page: https://github.com/google/langfun
6
6
  Author: Langfun Authors
@@ -193,7 +193,9 @@ To install a nightly build, include the `--pre` flag, like this:
193
193
  pip install langfun[all] --pre
194
194
  ```
195
195
 
196
- If you want to customize your installation, you can select specific features using package names like `langfun[X1, X2, ..., Xn]`, where `Xi` corresponds to a tag from the list below:
196
+ If you want to customize your installation, you can select specific features
197
+ using package names like `langfun[X1, X2, ..., Xn]`, where `Xi` corresponds to
198
+ a tag from the list below:
197
199
 
198
200
  | Tag | Description |
199
201
  | ------------------- | ---------------------------------------- |
@@ -201,15 +203,36 @@ If you want to customize your installation, you can select specific features usi
201
203
  | vertexai | VertexAI access. |
202
204
  | mime | All MIME supports. |
203
205
  | mime-auto | Automatic MIME type detection. |
204
- | mime-docx | DocX format support. |
205
206
  | mime-pil | Image support for PIL. |
206
- | mime-xlsx | XlsX format support. |
207
207
  | ui | UI enhancements |
208
208
 
209
-
210
- For example, to install a nightly build that includes VertexAI access, full modality support, and UI enhancements, use:
209
+ For example, to install a nightly build that includes VertexAI access, full
210
+ modality support, and UI enhancements, use:
211
211
  ```
212
212
  pip install langfun[vertexai,mime,ui] --pre
213
213
  ```
214
214
 
215
+ ### Solving import issue with `libmagic`
216
+
217
+ Langfun utilizes `libmagic` for automatic MIME type detection to support
218
+ multi-modal functionalities. However, `pip install libmagic` may not work
219
+ out-of-the-box on all operation systems, sometimes leading to an
220
+ `'ImportError: failed to find libmagic.'` error after Langfun installation.
221
+
222
+ If you encounter this error, you will need to follow the recommendations below
223
+ to fix the installation of `libmagic` library.
224
+
225
+ #### OSX
226
+
227
+ ```
228
+ conda install conda-forge::libmagic
229
+ ```
230
+
231
+ #### Windows:
232
+ ```
233
+ pip install python-magic
234
+ pip uninstall python-magic-bin
235
+ pip install python-magic-bin
236
+ ```
237
+
215
238
  *Disclaimer: this is not an officially supported Google product.*
@@ -26,10 +26,10 @@ langfun/core/subscription_test.py,sha256=Y4ZdbZEwm83YNZBxHff0QR4QUa4rdaNXA3_jfIc
26
26
  langfun/core/template.py,sha256=jNhYSrbLIn9kZOa03w5QZbyjgfnzJzE_ZrrMvvWY4t4,24929
27
27
  langfun/core/template_test.py,sha256=AQv_m9qE93WxhEhSlm1xaBgB4hu0UVtA53dljngkUW0,17090
28
28
  langfun/core/agentic/__init__.py,sha256=qR3jlfUO4rhIoYdRDLz-d22YZf3FvU4FW88vsjiGDQQ,1224
29
- langfun/core/agentic/action.py,sha256=83zH_cj0s9mxq7mnaTZXbB0Uf-y8d0ip5TrF-mabeT8,34000
30
- langfun/core/agentic/action_eval.py,sha256=-ZcWt_eYqnTyOe9HFGyrLJkQRd4iG3hfN4IZ-NYAWwA,4534
29
+ langfun/core/agentic/action.py,sha256=9P7xDiZVUV9MvJDfuAfLx-xa7qvS5F0EOGWDQnjAZBw,38931
30
+ langfun/core/agentic/action_eval.py,sha256=8cdH-ubWUL1sscqagrQltPg19vTYNKrI3CKLvluGP24,4615
31
31
  langfun/core/agentic/action_eval_test.py,sha256=tRUkWmOE9p0rpNOq19xAY2oDEnYsEEykjg6sUpAwJk0,2832
32
- langfun/core/agentic/action_test.py,sha256=ilirTICURoAR6LYjlg3oC3P1KSbLzJQDK4WaZONed2Q,5708
32
+ langfun/core/agentic/action_test.py,sha256=9EZKgLaBrqTErSRoxtrSlzmCz_cbnwWu0ZqpwKLst-s,10224
33
33
  langfun/core/coding/__init__.py,sha256=5utju_fwEsImaiftx4oXKl9FAM8p281k8-Esdh_-m1w,835
34
34
  langfun/core/coding/python/__init__.py,sha256=4ByknuoNU-mOIHwHKnTtmo6oD64oMFtlqPlYWmA5Wic,1736
35
35
  langfun/core/coding/python/correction.py,sha256=7zBedlhQKMPA4cfchUMxAOFl6Zl5RqCyllRHGWys40s,7092
@@ -139,10 +139,10 @@ langfun/core/structured/parsing.py,sha256=MGvI7ypXlwfzr5XB8_TFU9Ei0_5reYqkWkv64e
139
139
  langfun/core/structured/parsing_test.py,sha256=kNPrhpdPY3iWhUld0TFYU-Zgn44wC0d6YuQ9XdVbQ8o,22346
140
140
  langfun/core/structured/querying.py,sha256=GGNtHtJcKh8rzLBNx_Df1ATvsPZzyfZuGkzSQVabdpo,24885
141
141
  langfun/core/structured/querying_test.py,sha256=vUuVUClYBFGEaO9KuD60huPE1dmP6RCRLeRnBv67NmQ,34263
142
- langfun/core/structured/schema.py,sha256=OGHmjyjraTnrq11Y5KrGyvKahviB1cfyvV9avd4UUZU,28126
142
+ langfun/core/structured/schema.py,sha256=pGiAjez-ON2nKLUSeAls27gJsMto5aJnCXLVwH3pUKM,28296
143
143
  langfun/core/structured/schema_generation.py,sha256=3AcuKvv3VOtKY5zMVqODrxfOuDxzoZtGeBxHlOWDOWw,5308
144
144
  langfun/core/structured/schema_generation_test.py,sha256=RM9s71kMNg2jTePwInkiW9fK1ACN37eyPeF8OII-0zw,2950
145
- langfun/core/structured/schema_test.py,sha256=L-HY9GWvfO4-kWuol6tY_zpLakNrwpMAxz2jUsY_e8s,25744
145
+ langfun/core/structured/schema_test.py,sha256=N8qNVA2hrHlxmHIoKtpzhBASa2RMVoPowF_noAfgPME,26035
146
146
  langfun/core/structured/scoring.py,sha256=Y7Jqs5VVjUQLF_9Z1uIY_dw5zasv2FF52Cz-cxGMsro,5857
147
147
  langfun/core/structured/scoring_test.py,sha256=QvlwDAzwuamKL5tCotm1L3Sx0cs3idoNK4aIEhaO4Yk,2272
148
148
  langfun/core/structured/tokenization.py,sha256=-b4_693quHeYn2AqndwucuXNmhd5NVXVTU3mmDane98,2189
@@ -156,8 +156,8 @@ langfun/core/templates/demonstration.py,sha256=vCrgYubdZM5Umqcgp8NUVGXgr4P_c-fik
156
156
  langfun/core/templates/demonstration_test.py,sha256=SafcDQ0WgI7pw05EmPI2S4v1t3ABKzup8jReCljHeK4,2162
157
157
  langfun/core/templates/selfplay.py,sha256=yhgrJbiYwq47TgzThmHrDQTF4nDrTI09CWGhuQPNv-s,2273
158
158
  langfun/core/templates/selfplay_test.py,sha256=Ot__1P1M8oJfoTp-M9-PQ6HUXqZKyMwvZ5f7yQ3yfyM,2326
159
- langfun-0.1.2.dev202504270803.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
160
- langfun-0.1.2.dev202504270803.dist-info/METADATA,sha256=-p76zsXSD3qQx9ZqulXFmhbENRRXiZr9hHLMgDuh4Us,7692
161
- langfun-0.1.2.dev202504270803.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
162
- langfun-0.1.2.dev202504270803.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
163
- langfun-0.1.2.dev202504270803.dist-info/RECORD,,
159
+ langfun-0.1.2.dev202504290805.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
160
+ langfun-0.1.2.dev202504290805.dist-info/METADATA,sha256=KWW7NslPxpSS2u0eVRcN022ylI5TeWFdswXMnve9144,8178
161
+ langfun-0.1.2.dev202504290805.dist-info/WHEEL,sha256=ck4Vq1_RXyvS4Jt6SI0Vz6fyVs4GWg7AINwpsaGEgPE,91
162
+ langfun-0.1.2.dev202504290805.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
163
+ langfun-0.1.2.dev202504290805.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.1)
2
+ Generator: setuptools (80.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5