langfun 0.1.2.dev202510290805__py3-none-any.whl → 0.1.2.dev202510300805__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.

@@ -538,6 +538,18 @@ class ExecutionTrace(pg.Object, pg.views.html.HtmlTreeView.Extension):
538
538
  remove_class=['not-started'],
539
539
  )
540
540
 
541
+ def remove(self, item: TracedItem) -> None:
542
+ """Removes an item from the sequence."""
543
+ index = self.items.index(item)
544
+ if index == -1:
545
+ raise ValueError(f'Item not found in execution trace: {item!r}')
546
+
547
+ with pg.notify_on_change(False):
548
+ self.items.pop(index)
549
+
550
+ if self._tab_control is not None:
551
+ self._tab_control.remove(index)
552
+
541
553
  def extend(self, items: Iterable[TracedItem]) -> None:
542
554
  """Extends the sequence with a list of items."""
543
555
  for item in items:
@@ -1648,13 +1660,20 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1648
1660
  @contextlib.contextmanager
1649
1661
  def track_queries(
1650
1662
  self,
1651
- phase: str | None = None
1663
+ phase: str | None = None,
1664
+ track_if: Callable[
1665
+ [lf_structured.QueryInvocation],
1666
+ bool
1667
+ ] | None = None,
1652
1668
  ) -> Iterator[list[lf_structured.QueryInvocation]]:
1653
1669
  """Tracks `lf.query` made within the context.
1654
1670
 
1655
1671
  Args:
1656
1672
  phase: The name of a new phase to track the queries in. If not provided,
1657
1673
  the queries will be tracked in the parent phase.
1674
+ track_if: A function that takes a `lf_structured.QueryInvocation` and
1675
+ returns True if the query should be included in the result. If None,
1676
+ all queries (including failed queries) will be included.
1658
1677
 
1659
1678
  Yields:
1660
1679
  A list of `lf.QueryInvocation` objects, each for a single `lf.query`
@@ -1673,6 +1692,11 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1673
1692
  self.event_handler.on_query_start(self, self._current_action, invocation)
1674
1693
 
1675
1694
  def _query_end(invocation: lf_structured.QueryInvocation):
1695
+ if track_if is not None and not track_if(invocation):
1696
+ self._current_execution.remove(invocation)
1697
+ # Even if the query is not included in the execution trace, we still
1698
+ # count the usage summary to the current execution and trigger the
1699
+ # event handler to log the query.
1676
1700
  self._current_execution.merge_usage_summary(invocation.usage_summary)
1677
1701
  self.event_handler.on_query_end(self, self._current_action, invocation)
1678
1702
 
@@ -1705,8 +1729,9 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1705
1729
  *,
1706
1730
  lm: lf.LanguageModel,
1707
1731
  examples: list[lf_structured.MappingExample] | None = None,
1732
+ track_if: Callable[[lf_structured.QueryInvocation], bool] | None = None,
1708
1733
  **kwargs
1709
- ) -> Any:
1734
+ ) -> Any:
1710
1735
  """Calls `lf.query` and associates it with the current invocation.
1711
1736
 
1712
1737
  The following code are equivalent:
@@ -1731,12 +1756,15 @@ class Session(pg.Object, pg.views.html.HtmlTreeView.Extension):
1731
1756
  default: The default value to return if the query fails.
1732
1757
  lm: The language model to use for the query.
1733
1758
  examples: The examples to use for the query.
1759
+ track_if: A function that takes a `lf_structured.QueryInvocation`
1760
+ and returns True if the query should be tracked.
1761
+ If None, all queries (including failed queries) will be tracked.
1734
1762
  **kwargs: Additional keyword arguments to pass to `lf.query`.
1735
1763
 
1736
1764
  Returns:
1737
1765
  The result of the query.
1738
1766
  """
1739
- with self.track_queries():
1767
+ with self.track_queries(track_if=track_if):
1740
1768
  return lf_structured.query(
1741
1769
  prompt,
1742
1770
  schema=schema,
@@ -530,6 +530,31 @@ class SessionTest(unittest.TestCase):
530
530
  self.assertIn('agent@', session.id)
531
531
  self.assertIsInstance(session.as_message(), lf.AIMessage)
532
532
 
533
+ def test_query_with_track_if(self):
534
+ lm = fake.StaticResponse('lm response')
535
+ session = action_lib.Session()
536
+
537
+ # Render session to trigger javascript updates to the HTML when
538
+ # operating on the session.
539
+ _ = session.to_html()
540
+ with session:
541
+ # This query will succeed.
542
+ session.query(
543
+ 'prompt1',
544
+ schema=None,
545
+ lm=lm,
546
+ track_if=lambda q: not q.has_error,
547
+ default=None)
548
+ # This query will fail during parsing.
549
+ session.query(
550
+ 'prompt2',
551
+ schema=int,
552
+ lm=lm,
553
+ track_if=lambda q: not q.has_error,
554
+ default=None)
555
+ self.assertEqual(len(session.root.queries), 1)
556
+ self.assertIsNone(session.root.queries[0].error)
557
+
533
558
 
534
559
  if __name__ == '__main__':
535
560
  unittest.main()
@@ -830,9 +830,9 @@ class Gemini(rest.REST):
830
830
  )
831
831
 
832
832
  def _error(self, status_code: int, content: str) -> lf.LMError:
833
- if (
834
- status_code == 400
835
- and b'exceeds the maximum number of tokens' in content
833
+ if status_code == 400 and (
834
+ b'exceeds the maximum number of tokens' in content
835
+ or b'Reduce the input token count and try again.' in content
836
836
  ):
837
837
  return lf.ContextLimitError(f'{status_code}: {content}')
838
838
  return super()._error(status_code, content)
langfun/env/__init__.py CHANGED
@@ -24,6 +24,10 @@ from langfun.env.interface import Environment
24
24
  from langfun.env.interface import Sandbox
25
25
  from langfun.env.interface import Feature
26
26
 
27
+ # Decorators for sandbox/feature methods.
28
+ from langfun.env.interface import treat_as_sandbox_state_error
29
+ from langfun.env.interface import log_sandbox_activity
30
+
27
31
  from langfun.env.base_environment import BaseEnvironment
28
32
  from langfun.env.base_sandbox import BaseSandbox
29
33
  from langfun.env.base_feature import BaseFeature
@@ -26,7 +26,7 @@ import contextlib
26
26
  import functools
27
27
  import threading
28
28
  import time
29
- from typing import Annotated, Any, Callable, Iterator, Sequence, Type
29
+ from typing import Annotated, Any, Iterator
30
30
 
31
31
  from langfun.env import interface
32
32
  from langfun.env.event_handlers import base as event_handler_base
@@ -100,10 +100,9 @@ class BaseSandbox(interface.Sandbox):
100
100
  self._status = status
101
101
  self._status_start_time = time.time()
102
102
 
103
- def report_maybe_state_error(self, e: BaseException | None) -> None:
103
+ def report_state_error(self, e: interface.SandboxStateError) -> None:
104
104
  """Reports sandbox state errors."""
105
- if (isinstance(e, interface.SandboxStateError)
106
- and e not in self._state_errors):
105
+ if e not in self._state_errors:
107
106
  self._state_errors.append(e)
108
107
 
109
108
  def _setup_features(self) -> None:
@@ -141,7 +140,8 @@ class BaseSandbox(interface.Sandbox):
141
140
  try:
142
141
  feature.teardown()
143
142
  except BaseException as e: # pylint: disable=broad-except
144
- self.report_maybe_state_error(e)
143
+ if isinstance(e, interface.SandboxStateError):
144
+ self.report_state_error(e)
145
145
  errors[feature.name] = e
146
146
  if errors:
147
147
  return interface.FeatureTeardownError(sandbox=self, errors=errors)
@@ -175,7 +175,8 @@ class BaseSandbox(interface.Sandbox):
175
175
  try:
176
176
  feature.teardown_session()
177
177
  except BaseException as e: # pylint: disable=broad-except
178
- self.report_maybe_state_error(e)
178
+ if isinstance(e, interface.SandboxStateError):
179
+ self.report_state_error(e)
179
180
  feature_teardown_errors[name] = e
180
181
 
181
182
  return interface.SessionTeardownError(
@@ -277,28 +278,6 @@ class BaseSandbox(interface.Sandbox):
277
278
  """Returns the features in the sandbox."""
278
279
  return self._features
279
280
 
280
- def _enter_service_call(self) -> bool:
281
- """Enters a service call.
282
-
283
- Returns:
284
- True if the service call is at the top of the call stack.
285
- """
286
- v = getattr(self._tls_state, 'service_call_depth', None)
287
- if v is None:
288
- v = 0
289
- setattr(self._tls_state, 'service_call_depth', v + 1)
290
- return v == 0
291
-
292
- def _exit_service_call(self) -> bool:
293
- """Exits a service call.
294
-
295
- Returns:
296
- True if the service call is at the top of the call stack.
297
- """
298
- v = getattr(self._tls_state, 'service_call_depth')
299
- setattr(self._tls_state, 'service_call_depth', v - 1)
300
- return v == 1
301
-
302
281
  #
303
282
  # Sandbox start/shutdown.
304
283
  #
@@ -369,7 +348,8 @@ class BaseSandbox(interface.Sandbox):
369
348
  '[%s]: Sandbox failed to start in %.2f seconds: %s',
370
349
  self.id, duration, e
371
350
  )
372
- self.report_maybe_state_error(e)
351
+ if isinstance(e, interface.SandboxStateError):
352
+ self.report_state_error(e)
373
353
  self.on_start(duration, e)
374
354
  self.shutdown()
375
355
  raise e
@@ -442,7 +422,8 @@ class BaseSandbox(interface.Sandbox):
442
422
  shutdown_error = None
443
423
  except BaseException as e: # pylint: disable=broad-except
444
424
  shutdown_error = e
445
- self.report_maybe_state_error(e)
425
+ if isinstance(e, interface.SandboxStateError):
426
+ self.report_state_error(e)
446
427
  self._set_status(interface.Sandbox.Status.OFFLINE)
447
428
  pg.logging.error(
448
429
  '[%s]: Sandbox shutdown with error: %s',
@@ -540,7 +521,8 @@ class BaseSandbox(interface.Sandbox):
540
521
  self._set_status(self.Status.IN_SESSION)
541
522
  self.on_session_start(session_id, time.time() - self._session_start_time)
542
523
  except BaseException as e: # pylint: disable=broad-except
543
- self.report_maybe_state_error(e)
524
+ if isinstance(e, interface.SandboxStateError):
525
+ self.report_state_error(e)
544
526
  self.on_session_start(
545
527
  session_id, time.time() - self._session_start_time, e
546
528
  )
@@ -626,7 +608,8 @@ class BaseSandbox(interface.Sandbox):
626
608
  self.id,
627
609
  e
628
610
  )
629
- self.report_maybe_state_error(e)
611
+ if isinstance(e, interface.SandboxStateError):
612
+ self.report_state_error(e)
630
613
  self.shutdown()
631
614
 
632
615
  # End session before setting up the next session.
@@ -677,6 +660,30 @@ class BaseSandbox(interface.Sandbox):
677
660
  and end_session_error.has_non_sandbox_state_error):
678
661
  raise end_session_error # pylint: disable=raising-bad-type
679
662
 
663
+ @contextlib.contextmanager
664
+ def track_activity(
665
+ self,
666
+ name: str,
667
+ feature: interface.Feature | None = None,
668
+ **kwargs: Any
669
+ ) -> Iterator[None]:
670
+ """Tracks an activity for the sandbox."""
671
+ start_time = time.time()
672
+ error = None
673
+ try:
674
+ yield None
675
+ except BaseException as e: # pylint: disable=broad-except
676
+ error = e
677
+ raise
678
+ finally:
679
+ self.on_activity(
680
+ name=name,
681
+ feature=feature,
682
+ duration=time.time() - start_time,
683
+ error=error,
684
+ **kwargs
685
+ )
686
+
680
687
  #
681
688
  # Housekeeping.
682
689
  #
@@ -722,7 +729,7 @@ class BaseSandbox(interface.Sandbox):
722
729
  str(e)
723
730
  )
724
731
  self._housekeep_counter += 1
725
- self.report_maybe_state_error(e)
732
+ self.report_state_error(e)
726
733
  self.on_housekeep(time.time() - housekeep_start, e)
727
734
  self.shutdown()
728
735
  break
@@ -744,7 +751,7 @@ class BaseSandbox(interface.Sandbox):
744
751
  feature.name,
745
752
  e,
746
753
  )
747
- self.report_maybe_state_error(e)
754
+ self.report_state_error(e)
748
755
  self._housekeep_counter += 1
749
756
  self.on_housekeep(time.time() - housekeep_start, e)
750
757
  self.shutdown()
@@ -917,208 +924,3 @@ class BaseSandbox(interface.Sandbox):
917
924
  handler.on_session_end(
918
925
  self.environment, self, session_id, duration, lifetime, error
919
926
  )
920
-
921
-
922
- #
923
- # Sandbox service decorator.
924
- #
925
-
926
-
927
- def sandbox_service(
928
- critical_errors: Sequence[
929
- Type[BaseException] | tuple[Type[BaseException], str]
930
- ] | None = None
931
- ) -> Callable[..., Any]:
932
- """Decorator for Sandbox/Feature methods exposed as sandbox services.
933
-
934
- This decorator will catch errors and map to `SandboxStateError` if the
935
- error matches any of the critical errors. Consequently, the sandbox will be
936
- shutdown automatically when the error is raised.
937
-
938
- Example:
939
-
940
- ```
941
- with env:
942
- with env.sandbox() as sb:
943
- try:
944
- sb.test_feature.do_something_with_non_state_error()
945
- except ValueError:
946
- # sandbox will not be shutdown.
947
- pass
948
-
949
- try:
950
- sb.test_feature.do_something_with_state_error()
951
- except ValueError:
952
- assert sb.state == sb.Status.OFFLINE
953
- ```
954
-
955
- If the decorated method returns a context manager, a wrapper context manager
956
- will be returned, which will end the session when exiting the context.
957
-
958
- Example:
959
-
960
- ```
961
- with env:
962
- with env.test_feature.do_something_with_context_manager() as result:
963
- # sandbox will be alive during the whole context manager cycle.
964
- ```
965
-
966
- For sandbox service methods, an optional `session_id` argument can be passed
967
- to create a new session for the service call, even its signature does not
968
- contain a `session_id` argument.
969
-
970
- Args:
971
- critical_errors: A sequence of exception types or tuples of exception type
972
- and error messages (described in regular expression), when matched, treat
973
- the sandbox as in a bad state, which will trigger a shutdown.
974
-
975
- Returns:
976
- The decorator function.
977
- """
978
- critical_errors = critical_errors or []
979
-
980
- def decorator(func):
981
- signature = pg.typing.get_signature(func)
982
- if 'session_id' in signature.arg_names:
983
- raise ValueError(
984
- '`session_id` should not be used as argument for sandbox '
985
- 'service method. Please use `self.session_id` instead.'
986
- )
987
-
988
- def to_kwargs(*args, **kwargs):
989
- num_non_self_args = len(signature.arg_names) - 1
990
- if len(args) > num_non_self_args:
991
- assert signature.varargs is not None, (signature, args)
992
- kwargs[signature.varargs.name] = tuple(args[num_non_self_args:])
993
- args = args[:num_non_self_args]
994
- for i in range(len(args)):
995
- # The first argument is `self`.
996
- kwargs[signature.arg_names[i + 1]] = args[i]
997
- return kwargs
998
-
999
- @functools.wraps(func)
1000
- def method_wrapper(self, *args, **kwargs) -> Any:
1001
- """Helper function to safely execute logics in the sandbox."""
1002
-
1003
- assert isinstance(self, (BaseSandbox, interface.Feature)), self
1004
- sandbox = self.sandbox if isinstance(self, interface.Feature) else self
1005
-
1006
- # We count the service call stack depth so we could shutdown the sandbox
1007
- # at the top upon sandbox state error.
1008
- sandbox._enter_service_call() # pylint: disable=protected-access
1009
-
1010
- # When a capability is directly accessed from the environment,
1011
- # we create a new session for the capability call. This
1012
- # prevents the sandbox from being reused for other feature calls.
1013
- if sandbox.status == interface.Sandbox.Status.ACQUIRED:
1014
- new_session = True
1015
- new_session_id = kwargs.get('session_id')
1016
- if new_session_id is None:
1017
- new_session_id = sandbox.environment.new_session_id()
1018
-
1019
- # If it's a feature method called from the environment, start a new
1020
- # session for the feature call.
1021
- sandbox.start_session(new_session_id)
1022
- else:
1023
- new_session = False
1024
-
1025
- kwargs.pop('session_id', None)
1026
- result = None
1027
- error = None
1028
- start_time = time.time()
1029
-
1030
- try:
1031
- # Execute the service function.
1032
- result = func(self, *args, **kwargs)
1033
-
1034
- # If the result is a context manager, wrap it with a context manager
1035
- # to end the session when exiting.
1036
- if isinstance(result, contextlib.AbstractContextManager):
1037
- return _service_context_manager_wrapper(
1038
- service=result,
1039
- sandbox_or_feature=self,
1040
- sandbox=sandbox,
1041
- name=func.__name__,
1042
- kwargs=to_kwargs(*args, **kwargs),
1043
- start_time=start_time,
1044
- new_session=new_session
1045
- )
1046
-
1047
- # Otherwise, return the result and end the session in the finally block.
1048
- return result
1049
- except BaseException as e:
1050
- error = e
1051
- sandbox.report_maybe_state_error(e)
1052
- if pg.match_error(e, critical_errors):
1053
- state_error = interface.SandboxStateError(
1054
- 'Sandbox encountered an unexpected error executing '
1055
- f'`{func.__name__}` (args={args!r}, kwargs={kwargs!r}): {e}',
1056
- sandbox=self
1057
- )
1058
- sandbox.report_maybe_state_error(state_error)
1059
- raise state_error from e
1060
- raise
1061
- finally:
1062
- is_topmost_call = sandbox._exit_service_call() # pylint: disable=protected-access
1063
- if not isinstance(result, contextlib.AbstractContextManager):
1064
- self.on_activity(
1065
- name=func.__name__,
1066
- duration=time.time() - start_time,
1067
- error=error,
1068
- **to_kwargs(*args, **kwargs),
1069
- )
1070
- if new_session:
1071
- assert is_topmost_call
1072
-
1073
- # End the session if it's from a feature method and the result
1074
- # is not a context manager.
1075
- sandbox.end_session()
1076
-
1077
- # Shutdown the sandbox if it is at the top of the service call stack and
1078
- # has state errors.
1079
- if (is_topmost_call
1080
- and sandbox.state_errors
1081
- # Sandbox service method might be called during shutting down, in
1082
- # that case we don't want to shutdown the sandbox again.
1083
- and not sandbox.is_shutting_down):
1084
- sandbox.shutdown()
1085
-
1086
- return method_wrapper
1087
- return decorator
1088
-
1089
-
1090
- @contextlib.contextmanager
1091
- def _service_context_manager_wrapper(
1092
- service: contextlib.AbstractContextManager[Any],
1093
- sandbox_or_feature: BaseSandbox | interface.Feature,
1094
- sandbox: interface.Sandbox,
1095
- name: str,
1096
- kwargs: dict[str, Any],
1097
- new_session: bool,
1098
- start_time: float,
1099
- ) -> Iterator[Any]:
1100
- """Context manager wrapper for ending a sandbox session when exiting."""
1101
- error = None
1102
- sandbox._enter_service_call() # pylint: disable=protected-access
1103
-
1104
- try:
1105
- with service as result:
1106
- yield result
1107
- except BaseException as e:
1108
- error = e
1109
- sandbox.report_maybe_state_error(error)
1110
- raise
1111
- finally:
1112
- sandbox_or_feature.on_activity(
1113
- name=name,
1114
- error=error,
1115
- duration=time.time() - start_time,
1116
- **kwargs,
1117
- )
1118
- is_topmost_call = sandbox._exit_service_call() # pylint: disable=protected-access
1119
-
1120
- if new_session:
1121
- assert is_topmost_call
1122
- sandbox.end_session()
1123
- elif isinstance(error, interface.SandboxStateError):
1124
- sandbox.shutdown()
langfun/env/base_test.py CHANGED
@@ -15,7 +15,6 @@ import time
15
15
  from typing import Any
16
16
  import unittest
17
17
 
18
- from langfun.env import base_sandbox
19
18
  from langfun.env import interface
20
19
  from langfun.env import test_utils
21
20
  from langfun.env.event_handlers import base as event_handler_base
@@ -460,10 +459,11 @@ class EnvironmentTests(unittest.TestCase):
460
459
  simulate_setup_error=interface.SandboxStateError,
461
460
  skip_notification=True
462
461
  )
463
- with env.sandbox() as sb:
464
- with self.assertRaises(interface.SandboxStateError):
462
+ with self.assertRaises(interface.SandboxStateError):
463
+ with env.sandbox() as sb:
465
464
  sb.shell('bad command', raise_error=interface.SandboxStateError)
466
465
  self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
466
+ self.assertEqual(len(sb.state_errors), 1)
467
467
  sb_offline_time = time.time()
468
468
  while time.time() - sb_offline_time < 10:
469
469
  if not env.is_online:
@@ -1250,12 +1250,11 @@ class SandboxStatusTests(unittest.TestCase):
1250
1250
  },
1251
1251
  )
1252
1252
  with env:
1253
- with env.sandbox(session_id='session1') as sb:
1254
- with self.assertRaises(interface.SandboxStateError):
1253
+ with self.assertRaises(interface.SandboxStateError):
1254
+ with env.sandbox(session_id='session1') as sb:
1255
1255
  sb.shell('echo foo', raise_error=RuntimeError)
1256
- self.assertEqual(len(sb.state_errors), 1)
1257
- self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1258
- sb.shell('echo bar')
1256
+ self.assertEqual(len(sb.state_errors), 1)
1257
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1259
1258
  self.assertEqual(
1260
1259
  self.event_handler.logs,
1261
1260
  [
@@ -1282,7 +1281,6 @@ class SandboxStatusTests(unittest.TestCase):
1282
1281
  '[testing-env/test_image:0:0/feature1] feature teardown',
1283
1282
  '[testing-env/test_image:0:0] shutting_down -> offline',
1284
1283
  '[testing-env/test_image:0:0] sandbox shutdown',
1285
- '[testing-env/test_image:0:0] shell: echo bar',
1286
1284
  # pylint: enable=line-too-long
1287
1285
  ]
1288
1286
  )
@@ -1306,11 +1304,6 @@ class SandboxActivityTests(unittest.TestCase):
1306
1304
  r'test_feature-session-[0-9a-f]{7}'
1307
1305
  )
1308
1306
 
1309
- with self.assertRaisesRegex(ValueError, '`session_id` should not be used'):
1310
- @base_sandbox.sandbox_service()
1311
- def foo(session_id: str):
1312
- del session_id
1313
-
1314
1307
  def test_ping_error(self):
1315
1308
  env = TestingEnvironment(
1316
1309
  features={'test_feature': TestingFeature(housekeep_interval=0)},
@@ -1487,10 +1480,11 @@ class SandboxServiceTests(unittest.TestCase):
1487
1480
 
1488
1481
  def test_service_call_from_feature_with_error(self):
1489
1482
  with self.env:
1490
- with self.env.sandbox(session_id='session1') as sb:
1491
- with self.assertRaises(interface.SandboxStateError):
1483
+ with self.assertRaises(interface.SandboxStateError):
1484
+ with self.env.sandbox(session_id='session1') as sb:
1492
1485
  sb.test_feature.bad_shell_call()
1493
- self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1486
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1487
+ self.assertEqual(len(sb.state_errors), 1)
1494
1488
 
1495
1489
  self.assertEqual(
1496
1490
  self.event_handler.logs,
@@ -1584,7 +1578,6 @@ class SandboxServiceTests(unittest.TestCase):
1584
1578
  '[testing-env/test_image:0/session1] shell: "test_feature" setup session',
1585
1579
  "[testing-env/test_image:0] session 'session1' started",
1586
1580
  '[testing-env/test_image:0/session1] shell: hello',
1587
- '[testing-env/test_image:0/session1/test_feature] test_feature.my_service: None',
1588
1581
  '[testing-env/test_image:0/session1] shell: foo',
1589
1582
  '[testing-env/test_image:0/session1] shell: "test_feature" teardown session',
1590
1583
  "[testing-env/test_image:0] session 'session1' ended",
@@ -1598,11 +1591,12 @@ class SandboxServiceTests(unittest.TestCase):
1598
1591
 
1599
1592
  def test_service_context_manager_from_feature_with_error(self):
1600
1593
  with self.env:
1601
- with self.env.sandbox(session_id='session1') as sb:
1602
- with self.assertRaises(interface.SandboxStateError):
1594
+ with self.assertRaises(interface.SandboxStateError):
1595
+ with self.env.sandbox(session_id='session1') as sb:
1603
1596
  with sb.test_feature.my_service() as service:
1604
1597
  service.do('hello', raise_error=interface.SandboxStateError)
1605
- self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1598
+ self.assertEqual(sb.status, interface.Sandbox.Status.OFFLINE)
1599
+ self.assertEqual(len(sb.state_errors), 1)
1606
1600
  self.assertEqual(
1607
1601
  self.event_handler.logs,
1608
1602
  [
@@ -1614,7 +1608,6 @@ class SandboxServiceTests(unittest.TestCase):
1614
1608
  '[testing-env/test_image:0/session1] shell: "test_feature" setup session',
1615
1609
  "[testing-env/test_image:0] session 'session1' started",
1616
1610
  '[testing-env/test_image:0/session1] shell: hello with SandboxStateError',
1617
- '[testing-env/test_image:0/session1/test_feature] test_feature.my_service: None with SandboxStateError',
1618
1611
  '[testing-env/test_image:0/session1] shell: "test_feature" teardown session',
1619
1612
  "[testing-env/test_image:0] session 'session1' ended with SandboxStateError",
1620
1613
  '[testing-env/test_image:0] shell: "test_feature" teardown',
@@ -1645,7 +1638,6 @@ class SandboxServiceTests(unittest.TestCase):
1645
1638
  '[testing-env/test_image:0/session1] shell: "test_feature" setup session',
1646
1639
  "[testing-env/test_image:0] session 'session1' started",
1647
1640
  '[testing-env/test_image:0/session1] shell: foo',
1648
- '[testing-env/test_image:0/session1/test_feature] test_feature.my_service: None',
1649
1641
  '[testing-env/test_image:0/session1] shell: "test_feature" teardown session',
1650
1642
  "[testing-env/test_image:0] session 'session1' ended",
1651
1643
  '[testing-env/test_image:0] shell: "test_feature" teardown',
@@ -1657,7 +1649,6 @@ class SandboxServiceTests(unittest.TestCase):
1657
1649
  '[testing-env/test_image:1/test_feature-session-2291d8c] shell: "test_feature" setup session',
1658
1650
  "[testing-env/test_image:1] session 'test_feature-session-2291d8c' started",
1659
1651
  '[testing-env/test_image:1/test_feature-session-2291d8c] shell: bar',
1660
- '[testing-env/test_image:1/test_feature-session-2291d8c/test_feature] test_feature.my_service: None',
1661
1652
  '[testing-env/test_image:1/test_feature-session-2291d8c] shell: "test_feature" teardown session',
1662
1653
  "[testing-env/test_image:1] session 'test_feature-session-2291d8c' ended",
1663
1654
  '[testing-env/test_image:1] shell: "test_feature" teardown',
@@ -1685,7 +1676,6 @@ class SandboxServiceTests(unittest.TestCase):
1685
1676
  '[testing-env/test_image:0/test_feature-session-2291d8c] shell: "test_feature" setup session',
1686
1677
  "[testing-env/test_image:0] session 'test_feature-session-2291d8c' started",
1687
1678
  '[testing-env/test_image:0/test_feature-session-2291d8c] shell: hello with SandboxStateError',
1688
- '[testing-env/test_image:0/test_feature-session-2291d8c/test_feature] test_feature.my_service: None with SandboxStateError',
1689
1679
  '[testing-env/test_image:0/test_feature-session-2291d8c] shell: "test_feature" teardown session',
1690
1680
  "[testing-env/test_image:0] session 'test_feature-session-2291d8c' ended with SandboxStateError",
1691
1681
  '[testing-env/test_image:0] shell: "test_feature" teardown',
langfun/env/interface.py CHANGED
@@ -17,8 +17,9 @@ import abc
17
17
  import contextlib
18
18
  import dataclasses
19
19
  import enum
20
+ import functools
20
21
  import os
21
- from typing import Annotated, Any, ContextManager, ClassVar, Iterator, Optional
22
+ from typing import Annotated, Any, Callable, ContextManager, ClassVar, Iterator, Optional, Sequence, Type
22
23
 
23
24
  import pyglove as pg
24
25
 
@@ -522,10 +523,17 @@ class Sandbox(pg.Object):
522
523
  """Returns True if the sandbox is online."""
523
524
  return self.status.is_online
524
525
 
525
- @property
526
526
  @abc.abstractmethod
527
- def state_errors(self) -> list[SandboxStateError]:
528
- """Returns state errors encountered during sandbox's lifecycle."""
527
+ def report_state_error(self, error: SandboxStateError) -> None:
528
+ """Reports state error the sandbox.
529
+
530
+ If state errors are reported, the sandbox will be forcefully shutdown when
531
+ `Sandbox.end_session()` is called, even if the sandbox is set to be
532
+ reusable.
533
+
534
+ Args:
535
+ error: SandboxStateError to report.
536
+ """
529
537
 
530
538
  @abc.abstractmethod
531
539
  def start(self) -> None:
@@ -656,6 +664,9 @@ class Sandbox(pg.Object):
656
664
  `end_session` should always be called for each `start_session` call, even
657
665
  when the session fails to start, to ensure proper cleanup.
658
666
 
667
+ When `end_session` is called with state errors reported, the sandbox will be
668
+ forcefully shutdown even if the sandbox is set to be reusable.
669
+
659
670
  `end_session` may fail with two sources of errors:
660
671
 
661
672
  1. SandboxStateError: If the sandbox is in a bad state or session teardown
@@ -679,6 +690,24 @@ class Sandbox(pg.Object):
679
690
  def session_id(self) -> str | None:
680
691
  """Returns the current user session identifier."""
681
692
 
693
+ @abc.abstractmethod
694
+ def track_activity(
695
+ self,
696
+ name: str,
697
+ feature: Optional['Feature'] = None,
698
+ **kwargs: Any
699
+ ) -> ContextManager[None]:
700
+ """Context manager that tracks a sandbox activity.
701
+
702
+ Args:
703
+ name: The name of the activity.
704
+ feature: The feature that the activity is associated with.
705
+ **kwargs: Additional keyword arguments to pass to the activity handler.
706
+
707
+ Returns:
708
+ A context manager that tracks the activity, including duration and error.
709
+ """
710
+
682
711
  #
683
712
  # API related to a user session.
684
713
  # A sandbox could be reused across different user sessions.
@@ -719,6 +748,9 @@ class Sandbox(pg.Object):
719
748
  self.start_session(session_id)
720
749
  try:
721
750
  yield self
751
+ except SandboxStateError as e:
752
+ self.report_state_error(e)
753
+ raise
722
754
  finally:
723
755
  self.end_session()
724
756
 
@@ -887,6 +919,12 @@ class Feature(pg.Object):
887
919
  assert self.sandbox is not None
888
920
  return self.sandbox.session_id
889
921
 
922
+ def track_activity(self, name: str, **kwargs: Any) -> ContextManager[None]:
923
+ """Context manager that tracks a feature activity."""
924
+ return self.sandbox.track_activity(
925
+ f'{self.name}.{name}', feature=self, **kwargs
926
+ )
927
+
890
928
 
891
929
  def _make_path_compatible(id_str: str) -> str:
892
930
  """Makes a path compatible with CNS."""
@@ -900,3 +938,73 @@ def _make_path_compatible(id_str: str) -> str:
900
938
  '>': '',
901
939
  })
902
940
  )
941
+
942
+
943
+ def treat_as_sandbox_state_error(
944
+ errors: Sequence[
945
+ Type[BaseException] | tuple[Type[BaseException], str]
946
+ ] | None = None
947
+ ) -> Callable[..., Any]:
948
+ """Decorator for Sandbox/Feature methods to convert errors to SandboxStateError.
949
+
950
+ Args:
951
+ errors: A sequence of exception types or tuples of (error_type, msg_regex).
952
+ when matched, treat the error as SandboxStateError, which will lead to
953
+ a sandbox shutdown when caught by `Sandbox.new_session()` context manager.
954
+
955
+ Returns:
956
+ The decorator function.
957
+ """
958
+
959
+ def decorator(func):
960
+ @functools.wraps(func)
961
+ def method_wrapper(self, *args, **kwargs) -> Any:
962
+ """Helper function to safely execute logics in the sandbox."""
963
+
964
+ assert isinstance(self, (Sandbox, Feature)), self
965
+ sandbox = self.sandbox if isinstance(self, Feature) else self
966
+
967
+ try:
968
+ # Execute the service function.
969
+ return func(self, *args, **kwargs)
970
+ except BaseException as e:
971
+ if pg.match_error(e, errors):
972
+ state_error = SandboxStateError(
973
+ 'Sandbox encountered an unexpected error executing '
974
+ f'`{func.__name__}` (args={args!r}, kwargs={kwargs!r}): {e}',
975
+ sandbox=sandbox
976
+ )
977
+ raise state_error from e
978
+ raise
979
+ return method_wrapper
980
+ return decorator
981
+
982
+
983
+ def log_sandbox_activity(name: str | None = None):
984
+ """Decorator for Sandbox/Feature methods to log sandbox activity."""
985
+
986
+ def decorator(func):
987
+ signature = pg.typing.get_signature(func)
988
+ def to_kwargs(*args, **kwargs):
989
+ num_non_self_args = len(signature.arg_names) - 1
990
+ if len(args) > num_non_self_args:
991
+ assert signature.varargs is not None, (signature, args)
992
+ kwargs[signature.varargs.name] = tuple(args[num_non_self_args:])
993
+ args = args[:num_non_self_args]
994
+ for i in range(len(args)):
995
+ # The first argument is `self`.
996
+ kwargs[signature.arg_names[i + 1]] = args[i]
997
+ return kwargs
998
+
999
+ @functools.wraps(func)
1000
+ def method_wrapper(self, *args, **kwargs) -> Any:
1001
+ """Helper function to safely execute logics in the sandbox."""
1002
+
1003
+ assert isinstance(self, (Sandbox, Feature)), self
1004
+ with self.track_activity(
1005
+ name or func.__name__,
1006
+ **to_kwargs(*args, **kwargs)
1007
+ ):
1008
+ return func(self, *args, **kwargs)
1009
+ return method_wrapper
1010
+ return decorator
@@ -11,6 +11,8 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ import contextlib
15
+ from typing import Iterator
14
16
  import unittest
15
17
  from langfun.env import interface
16
18
 
@@ -40,5 +42,81 @@ class IdTest(unittest.TestCase):
40
42
  self.assertIsNone(sandbox_id.working_dir(root_dir=None))
41
43
 
42
44
 
45
+ class TestingSandbox(interface.Sandbox):
46
+
47
+ id: interface.Sandbox.Id = interface.Sandbox.Id(
48
+ environment_id=interface.Environment.Id('env'),
49
+ image_id='test_image',
50
+ sandbox_id='0:0'
51
+ )
52
+ image_id: str = 'test_image'
53
+ features: dict[str, interface.Feature] = {}
54
+ status: interface.Sandbox.Status = interface.Sandbox.Status.READY
55
+ session_id: str | None = None
56
+
57
+ def environment(self) -> interface.Environment:
58
+ pass
59
+
60
+ def _on_bound(self) -> None:
61
+ self.activities = []
62
+
63
+ def report_state_error(self, error: interface.SandboxStateError) -> None:
64
+ pass
65
+
66
+ def start(self) -> None:
67
+ pass
68
+
69
+ def shutdown(self) -> None:
70
+ pass
71
+
72
+ def start_session(self, session_id: str) -> None:
73
+ pass
74
+
75
+ def end_session(self, shutdown_sandbox: bool = False) -> None:
76
+ pass
77
+
78
+ @contextlib.contextmanager
79
+ def track_activity(
80
+ self,
81
+ name: str,
82
+ feature: interface.Feature | None = None,
83
+ **kwargs
84
+ ) -> Iterator[None]:
85
+ error = None
86
+ try:
87
+ yield
88
+ except BaseException as e:
89
+ error = e
90
+ raise
91
+ finally:
92
+ self.activities.append((name, error, kwargs))
93
+
94
+
95
+ class DecoratorTest(unittest.TestCase):
96
+
97
+ def test_treat_as_sandbox_state_error(self):
98
+
99
+ class SandboxA(TestingSandbox):
100
+
101
+ @interface.treat_as_sandbox_state_error(errors=(ValueError,))
102
+ def foo(self, bar: str) -> None:
103
+ raise ValueError(bar)
104
+
105
+ with self.assertRaises(interface.SandboxStateError):
106
+ SandboxA().foo('foo')
107
+
108
+ def test_log_sandbox_activity(self):
109
+
110
+ class SandboxB(TestingSandbox):
111
+
112
+ @interface.log_sandbox_activity()
113
+ def bar(self, x: str) -> None:
114
+ pass
115
+
116
+ sb = SandboxB()
117
+ sb.bar('foo')
118
+ self.assertEqual(sb.activities, [('bar', None, {'x': 'foo'})])
119
+
120
+
43
121
  if __name__ == '__main__':
44
122
  unittest.main()
@@ -12,7 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  import concurrent.futures
15
+ import contextlib
15
16
  import time
17
+ from typing import Any, Iterator
16
18
  import unittest
17
19
 
18
20
  from langfun.env import interface
@@ -48,6 +50,9 @@ class TestingSandbox(interface.Sandbox):
48
50
  def state_errors(self) -> list[interface.SandboxStateError]:
49
51
  return []
50
52
 
53
+ def report_state_error(self, error: interface.SandboxStateError) -> None:
54
+ pass
55
+
51
56
  def set_status(self, status: interface.Sandbox.Status) -> None:
52
57
  self.rebind(status=status, skip_notification=True)
53
58
 
@@ -70,6 +75,18 @@ class TestingSandbox(interface.Sandbox):
70
75
  def session_id(self) -> str | None:
71
76
  return self._session_id
72
77
 
78
+ @contextlib.contextmanager
79
+ def track_activity(
80
+ self,
81
+ name: str,
82
+ feature: interface.Feature | None = None,
83
+ **kwargs: Any
84
+ ) -> Iterator[None]:
85
+ try:
86
+ yield
87
+ finally:
88
+ pass
89
+
73
90
 
74
91
  class RoundRobinTest(unittest.TestCase):
75
92
 
langfun/env/test_utils.py CHANGED
@@ -112,7 +112,8 @@ class TestingSandbox(base_sandbox.BaseSandbox):
112
112
  self._raise_error('Sandbox shutdown error', self.simulate_shutdown_error)
113
113
  super()._shutdown()
114
114
 
115
- @base_sandbox.sandbox_service(critical_errors=(RuntimeError,))
115
+ @interface.treat_as_sandbox_state_error(errors=(RuntimeError,))
116
+ @interface.log_sandbox_activity()
116
117
  def shell(
117
118
  self,
118
119
  code: str,
@@ -184,19 +185,19 @@ class TestingFeature(base_feature.BaseFeature):
184
185
  if self.call_end_session_on_teardown_session:
185
186
  self.sandbox.end_session()
186
187
 
187
- @base_sandbox.sandbox_service()
188
+ @interface.log_sandbox_activity()
188
189
  def num_shell_calls(self) -> int:
189
190
  return len(self.sandbox._shell_history) # pylint: disable=protected-access
190
191
 
191
- @base_sandbox.sandbox_service()
192
+ @interface.log_sandbox_activity()
192
193
  def bad_shell_call(self) -> None:
193
194
  self.sandbox.shell('bad command', raise_error=RuntimeError)
194
195
 
195
- @base_sandbox.sandbox_service()
196
+ @interface.log_sandbox_activity()
196
197
  def show_session_id(self):
197
198
  return self.session_id
198
199
 
199
- @base_sandbox.sandbox_service()
200
+ @interface.log_sandbox_activity()
200
201
  def call_with_varargs(self, code: str, *args, **kwargs):
201
202
  del code, args, kwargs
202
203
  return 0
@@ -205,7 +206,6 @@ class TestingFeature(base_feature.BaseFeature):
205
206
  super()._on_bound()
206
207
  self._service = None
207
208
 
208
- @base_sandbox.sandbox_service()
209
209
  @contextlib.contextmanager
210
210
  def my_service(self) -> Iterator[Service]:
211
211
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfun
3
- Version: 0.1.2.dev202510290805
3
+ Version: 0.1.2.dev202510300805
4
4
  Summary: Langfun: Language as Functions.
5
5
  Home-page: https://github.com/google/langfun
6
6
  Author: Langfun Authors
@@ -35,10 +35,10 @@ langfun/core/subscription_test.py,sha256=Y4ZdbZEwm83YNZBxHff0QR4QUa4rdaNXA3_jfIc
35
35
  langfun/core/template.py,sha256=CQOEA1Lq0gU_uk43K1FJjpBSwL8o5fglFr2tGCbOxpI,26008
36
36
  langfun/core/template_test.py,sha256=vQZvFVS4VHk-6GUdOEQ-UA_4tlVBbpRPqB1Bw7DFJJg,19052
37
37
  langfun/core/agentic/__init__.py,sha256=vsnuvjaz9-nysBjdihGf43JC8AyLPhPJwIOevyONyAQ,1517
38
- langfun/core/agentic/action.py,sha256=ojwaPIV_a_khKPR6x1Fk5i2dsUTSe3VjKaxnZ92b0nE,58243
38
+ langfun/core/agentic/action.py,sha256=Zz-17gegJxzCZ9hcOhv-atJX1EfIN-vHonS6z99Bllc,59523
39
39
  langfun/core/agentic/action_eval.py,sha256=YTilyUEkJl_8FVMgdfO17PurWWaEJ6oA15CuefJJRLk,4887
40
40
  langfun/core/agentic/action_eval_test.py,sha256=7AkOwNbUX-ZgR1R0a7bvUZ5abNTUV7blf_8Mnrwb-II,2811
41
- langfun/core/agentic/action_test.py,sha256=a2D4FOuob7MviuPZR2Wy6xNKnjlTLxhK8HUy8WIyt08,19076
41
+ langfun/core/agentic/action_test.py,sha256=4X11bNffNZG8WfjB1Z-Sm33Z2hs0YPzz-6ct77V8M7Q,19824
42
42
  langfun/core/coding/__init__.py,sha256=5utju_fwEsImaiftx4oXKl9FAM8p281k8-Esdh_-m1w,835
43
43
  langfun/core/coding/python/__init__.py,sha256=yTXm92oLpQb4A-fZ2qy-bzfhPYND7B-oidtbv1PNaX0,1678
44
44
  langfun/core/coding/python/correction.py,sha256=4PD76Xfv36Xrm8Ji3-GgGDNImtcDqWfMw3z6ircJMlM,7285
@@ -101,7 +101,7 @@ langfun/core/llms/deepseek.py,sha256=Pyv2Pmviu91HfNGR994Mh7AKNvWHAAlue7Xfb9MZaPo
101
101
  langfun/core/llms/deepseek_test.py,sha256=DvROWPlDuow5E1lfoSkhyGt_ELA19JoQoDsTnRgDtTg,1847
102
102
  langfun/core/llms/fake.py,sha256=bDk_4u7V2LmYUotyOaicwzi0-lnWOIIBbR3-Bil1P3o,3481
103
103
  langfun/core/llms/fake_test.py,sha256=lC-C2TpEsnf2kmZpa3OiH2H944I4hMWTAaHEXzRj1DU,7855
104
- langfun/core/llms/gemini.py,sha256=-DL5PebzaTjz7rTFw_1RC5O1aE4EYSv3oNsM65YKCoo,30143
104
+ langfun/core/llms/gemini.py,sha256=Bsd9UmI-Z6j_ZJposExuh7ChCt1ZF2QE1nWPWHaE39k,30204
105
105
  langfun/core/llms/gemini_test.py,sha256=y1s0W65SrdepbZxzgIeoTB2MI7sXnfBDf1NsGn57LbM,7617
106
106
  langfun/core/llms/google_genai.py,sha256=ogyoOUK4s1OcSFKun0YK5xBRDVyxmvz9WsYNKAwuB0g,5918
107
107
  langfun/core/llms/google_genai_test.py,sha256=NKNtpebArQ9ZR7Qsnhd2prFIpMjleojy6o6VMXkJ1zY,1502
@@ -174,24 +174,24 @@ langfun/core/templates/demonstration.py,sha256=vCrgYubdZM5Umqcgp8NUVGXgr4P_c-fik
174
174
  langfun/core/templates/demonstration_test.py,sha256=SafcDQ0WgI7pw05EmPI2S4v1t3ABKzup8jReCljHeK4,2162
175
175
  langfun/core/templates/selfplay.py,sha256=yhgrJbiYwq47TgzThmHrDQTF4nDrTI09CWGhuQPNv-s,2273
176
176
  langfun/core/templates/selfplay_test.py,sha256=Ot__1P1M8oJfoTp-M9-PQ6HUXqZKyMwvZ5f7yQ3yfyM,2326
177
- langfun/env/__init__.py,sha256=m6Y8y16ms9lytvO_r-Br8HmTp2rNDhb3R6JJaH5dEEk,1491
177
+ langfun/env/__init__.py,sha256=VTjLmS_SkxtkBCmr6hBb4iACeLLPmcbWJkK7MEXsATA,1652
178
178
  langfun/env/base_environment.py,sha256=8Cpwb4D37zHj5AK4qgmGIPyxIMEDxTIPHXqxTSNkE_M,25030
179
179
  langfun/env/base_feature.py,sha256=JDEhL9LkbBHB0c603guchry7cy_zaIReg5vqExyQQgg,6902
180
- langfun/env/base_sandbox.py,sha256=aBzPWJGszWuwv8BObvBoIVtl8aiOp41YjTEfVbYYDKQ,37425
181
- langfun/env/base_test.py,sha256=PqFLqT-IwOe3zH4JWeCtUdDjSqG9KQ4RBQebo67AIqc,74983
182
- langfun/env/interface.py,sha256=_XguM6xjPQuiTUUrs-4_3ejm8rnolO5cfxpuYE6yoX8,28520
183
- langfun/env/interface_test.py,sha256=1ZeLPsB9QUyervNrg2LdkJeYx-rQYQ1Zq_IzIKJtS5M,1496
180
+ langfun/env/base_sandbox.py,sha256=0dduNozBtPqawvSX-pv6sEweDNTi89lrT_5qKk5FWak,30656
181
+ langfun/env/base_test.py,sha256=6yHtExP3Soa-l0ug73ZSV9ZfS0B4vu-uaS_K_xvsDwk,74261
182
+ langfun/env/interface.py,sha256=2X-gGD41cnejde5DIQ7QxgW3r85oKP4Gp0iWP-ABmLM,32126
183
+ langfun/env/interface_test.py,sha256=d8vdXL1PkrNQGwzfEI2r6esd6SBnTMgc-3GNArsnuj4,3295
184
184
  langfun/env/load_balancers.py,sha256=qRhCthqzjZIQBwta8qC1C0s0J-VQAVomJQqI7Nqv-r4,1948
185
- langfun/env/load_balancers_test.py,sha256=E1ksJ5Xsoj5aJTM4i9EKDiWKDFljs75FpoeJpbtCKL8,3771
186
- langfun/env/test_utils.py,sha256=5_lCkTvXC1J9unPdgmqn5PxaucU7GCcro0iFtjehJo0,14160
185
+ langfun/env/load_balancers_test.py,sha256=Bg0h-AL7LpWb_aixFXs_FpgQp4dWLvodcsQj-mys6zs,4125
186
+ langfun/env/test_utils.py,sha256=9fqhxxKERiyAte9bbtwqXqZ1ihNGyuOdCiPPY8G0I8w,14171
187
187
  langfun/env/event_handlers/__init__.py,sha256=H34n-TbKSgtxqBhE-yAti8fY6weF2_v3yw59M9_zmGM,443
188
188
  langfun/env/event_handlers/base.py,sha256=eGdJ6N5em9kX-c9wzm1TdnRP5_5IAltX5JTHILdjzLM,10124
189
189
  langfun/env/event_handlers/event_logger.py,sha256=3dbPjBe53dBgntYHlyLlj_77hVecPSXkmKeiGXMxlO0,12699
190
190
  langfun/env/event_handlers/event_logger_test.py,sha256=ryupxaEP9D8wdtSsSwZRSZwqFaHCaSD-bFSea_T7QJo,9133
191
191
  langfun/env/event_handlers/metric_writer.py,sha256=F_Gk1lpJX5SZ6-Hyrf_-utf4gvSKvMmcov8VkKogZCU,19618
192
192
  langfun/env/event_handlers/metric_writer_test.py,sha256=sntUifTPHGixUshIgVBX4Q9vJL-xbeS0Cpd5X5hSQyQ,5955
193
- langfun-0.1.2.dev202510290805.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
194
- langfun-0.1.2.dev202510290805.dist-info/METADATA,sha256=0QBqKpkm1e7B23GJpMUX5XQWfJEWwn_6nl-Qw_m2tos,7522
195
- langfun-0.1.2.dev202510290805.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
196
- langfun-0.1.2.dev202510290805.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
197
- langfun-0.1.2.dev202510290805.dist-info/RECORD,,
193
+ langfun-0.1.2.dev202510300805.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
194
+ langfun-0.1.2.dev202510300805.dist-info/METADATA,sha256=mKbGN2kywExiipSgV10M59iPfOncuioqSZ-9BDa2zRo,7522
195
+ langfun-0.1.2.dev202510300805.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
196
+ langfun-0.1.2.dev202510300805.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
197
+ langfun-0.1.2.dev202510300805.dist-info/RECORD,,