langfun 0.1.2.dev202510230805__py3-none-any.whl → 0.1.2.dev202511160804__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 (146) hide show
  1. langfun/core/__init__.py +1 -0
  2. langfun/core/agentic/action.py +107 -12
  3. langfun/core/agentic/action_eval.py +9 -2
  4. langfun/core/agentic/action_test.py +25 -0
  5. langfun/core/async_support.py +32 -3
  6. langfun/core/coding/python/correction.py +19 -9
  7. langfun/core/coding/python/execution.py +14 -12
  8. langfun/core/coding/python/generation.py +21 -16
  9. langfun/core/coding/python/sandboxing.py +23 -3
  10. langfun/core/component.py +42 -3
  11. langfun/core/concurrent.py +70 -6
  12. langfun/core/concurrent_test.py +1 -0
  13. langfun/core/console.py +1 -1
  14. langfun/core/data/conversion/anthropic.py +12 -3
  15. langfun/core/data/conversion/anthropic_test.py +8 -6
  16. langfun/core/data/conversion/gemini.py +9 -2
  17. langfun/core/data/conversion/gemini_test.py +12 -9
  18. langfun/core/data/conversion/openai.py +145 -31
  19. langfun/core/data/conversion/openai_test.py +161 -17
  20. langfun/core/eval/base.py +47 -43
  21. langfun/core/eval/base_test.py +4 -4
  22. langfun/core/eval/matching.py +5 -2
  23. langfun/core/eval/patching.py +3 -3
  24. langfun/core/eval/scoring.py +4 -3
  25. langfun/core/eval/v2/__init__.py +1 -0
  26. langfun/core/eval/v2/checkpointing.py +39 -5
  27. langfun/core/eval/v2/checkpointing_test.py +1 -1
  28. langfun/core/eval/v2/eval_test_helper.py +96 -0
  29. langfun/core/eval/v2/evaluation.py +87 -15
  30. langfun/core/eval/v2/evaluation_test.py +9 -3
  31. langfun/core/eval/v2/example.py +45 -39
  32. langfun/core/eval/v2/example_test.py +3 -3
  33. langfun/core/eval/v2/experiment.py +51 -8
  34. langfun/core/eval/v2/metric_values.py +31 -3
  35. langfun/core/eval/v2/metric_values_test.py +32 -0
  36. langfun/core/eval/v2/metrics.py +157 -44
  37. langfun/core/eval/v2/metrics_test.py +39 -18
  38. langfun/core/eval/v2/progress.py +30 -1
  39. langfun/core/eval/v2/progress_test.py +27 -0
  40. langfun/core/eval/v2/progress_tracking_test.py +3 -0
  41. langfun/core/eval/v2/reporting.py +90 -71
  42. langfun/core/eval/v2/reporting_test.py +20 -6
  43. langfun/core/eval/v2/runners/__init__.py +26 -0
  44. langfun/core/eval/v2/{runners.py → runners/base.py} +22 -124
  45. langfun/core/eval/v2/runners/debug.py +40 -0
  46. langfun/core/eval/v2/runners/debug_test.py +79 -0
  47. langfun/core/eval/v2/runners/parallel.py +100 -0
  48. langfun/core/eval/v2/runners/parallel_test.py +98 -0
  49. langfun/core/eval/v2/runners/sequential.py +47 -0
  50. langfun/core/eval/v2/runners/sequential_test.py +175 -0
  51. langfun/core/langfunc.py +45 -130
  52. langfun/core/langfunc_test.py +6 -4
  53. langfun/core/language_model.py +103 -16
  54. langfun/core/language_model_test.py +9 -3
  55. langfun/core/llms/__init__.py +7 -1
  56. langfun/core/llms/anthropic.py +157 -2
  57. langfun/core/llms/azure_openai.py +29 -17
  58. langfun/core/llms/cache/base.py +25 -3
  59. langfun/core/llms/cache/in_memory.py +48 -7
  60. langfun/core/llms/cache/in_memory_test.py +14 -4
  61. langfun/core/llms/compositional.py +25 -1
  62. langfun/core/llms/deepseek.py +30 -2
  63. langfun/core/llms/fake.py +32 -1
  64. langfun/core/llms/gemini.py +14 -9
  65. langfun/core/llms/google_genai.py +29 -1
  66. langfun/core/llms/groq.py +28 -3
  67. langfun/core/llms/llama_cpp.py +23 -4
  68. langfun/core/llms/openai.py +36 -3
  69. langfun/core/llms/openai_compatible.py +148 -27
  70. langfun/core/llms/openai_compatible_test.py +207 -20
  71. langfun/core/llms/openai_test.py +0 -2
  72. langfun/core/llms/rest.py +12 -1
  73. langfun/core/llms/vertexai.py +51 -8
  74. langfun/core/logging.py +1 -1
  75. langfun/core/mcp/client.py +77 -22
  76. langfun/core/mcp/client_test.py +8 -35
  77. langfun/core/mcp/session.py +94 -29
  78. langfun/core/mcp/session_test.py +54 -0
  79. langfun/core/mcp/tool.py +151 -22
  80. langfun/core/mcp/tool_test.py +197 -0
  81. langfun/core/memory.py +1 -0
  82. langfun/core/message.py +160 -55
  83. langfun/core/message_test.py +65 -81
  84. langfun/core/modalities/__init__.py +8 -0
  85. langfun/core/modalities/audio.py +21 -1
  86. langfun/core/modalities/image.py +19 -1
  87. langfun/core/modalities/mime.py +62 -3
  88. langfun/core/modalities/pdf.py +19 -1
  89. langfun/core/modalities/video.py +21 -1
  90. langfun/core/modality.py +167 -29
  91. langfun/core/modality_test.py +42 -12
  92. langfun/core/natural_language.py +1 -1
  93. langfun/core/sampling.py +4 -4
  94. langfun/core/sampling_test.py +20 -4
  95. langfun/core/structured/__init__.py +2 -24
  96. langfun/core/structured/completion.py +34 -44
  97. langfun/core/structured/completion_test.py +23 -43
  98. langfun/core/structured/description.py +54 -50
  99. langfun/core/structured/function_generation.py +29 -12
  100. langfun/core/structured/mapping.py +81 -37
  101. langfun/core/structured/parsing.py +95 -79
  102. langfun/core/structured/parsing_test.py +0 -3
  103. langfun/core/structured/querying.py +215 -142
  104. langfun/core/structured/querying_test.py +65 -29
  105. langfun/core/structured/schema/__init__.py +48 -0
  106. langfun/core/structured/schema/base.py +664 -0
  107. langfun/core/structured/schema/base_test.py +531 -0
  108. langfun/core/structured/schema/json.py +174 -0
  109. langfun/core/structured/schema/json_test.py +121 -0
  110. langfun/core/structured/schema/python.py +316 -0
  111. langfun/core/structured/schema/python_test.py +410 -0
  112. langfun/core/structured/schema_generation.py +33 -14
  113. langfun/core/structured/scoring.py +47 -36
  114. langfun/core/structured/tokenization.py +26 -11
  115. langfun/core/subscription.py +2 -2
  116. langfun/core/template.py +174 -49
  117. langfun/core/template_test.py +123 -17
  118. langfun/env/__init__.py +8 -2
  119. langfun/env/base_environment.py +320 -128
  120. langfun/env/base_environment_test.py +473 -0
  121. langfun/env/base_feature.py +92 -15
  122. langfun/env/base_feature_test.py +228 -0
  123. langfun/env/base_sandbox.py +84 -361
  124. langfun/env/base_sandbox_test.py +1235 -0
  125. langfun/env/event_handlers/__init__.py +1 -1
  126. langfun/env/event_handlers/chain.py +233 -0
  127. langfun/env/event_handlers/chain_test.py +253 -0
  128. langfun/env/event_handlers/event_logger.py +95 -98
  129. langfun/env/event_handlers/event_logger_test.py +21 -21
  130. langfun/env/event_handlers/metric_writer.py +225 -140
  131. langfun/env/event_handlers/metric_writer_test.py +23 -6
  132. langfun/env/interface.py +854 -40
  133. langfun/env/interface_test.py +112 -2
  134. langfun/env/load_balancers_test.py +23 -2
  135. langfun/env/test_utils.py +126 -84
  136. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/METADATA +1 -1
  137. langfun-0.1.2.dev202511160804.dist-info/RECORD +211 -0
  138. langfun/core/eval/v2/runners_test.py +0 -343
  139. langfun/core/structured/schema.py +0 -987
  140. langfun/core/structured/schema_test.py +0 -982
  141. langfun/env/base_test.py +0 -1481
  142. langfun/env/event_handlers/base.py +0 -350
  143. langfun-0.1.2.dev202510230805.dist-info/RECORD +0 -195
  144. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/WHEEL +0 -0
  145. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/licenses/LICENSE +0 -0
  146. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/top_level.txt +0 -0
@@ -26,10 +26,9 @@ 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
- from langfun.env.event_handlers import base as event_handler_base
33
32
  import pyglove as pg
34
33
 
35
34
 
@@ -41,6 +40,11 @@ class BaseSandbox(interface.Sandbox):
41
40
  'The identifier for the sandbox.'
42
41
  ]
43
42
 
43
+ image_id: Annotated[
44
+ str,
45
+ 'The image id for the sandbox.'
46
+ ]
47
+
44
48
  environment: Annotated[
45
49
  pg.Ref[interface.Environment],
46
50
  'The parent environment.'
@@ -95,10 +99,9 @@ class BaseSandbox(interface.Sandbox):
95
99
  self._status = status
96
100
  self._status_start_time = time.time()
97
101
 
98
- def report_maybe_state_error(self, e: BaseException | None) -> None:
102
+ def report_state_error(self, e: interface.SandboxStateError) -> None:
99
103
  """Reports sandbox state errors."""
100
- if (isinstance(e, interface.SandboxStateError)
101
- and e not in self._state_errors):
104
+ if e not in self._state_errors:
102
105
  self._state_errors.append(e)
103
106
 
104
107
  def _setup_features(self) -> None:
@@ -136,7 +139,8 @@ class BaseSandbox(interface.Sandbox):
136
139
  try:
137
140
  feature.teardown()
138
141
  except BaseException as e: # pylint: disable=broad-except
139
- self.report_maybe_state_error(e)
142
+ if isinstance(e, interface.SandboxStateError):
143
+ self.report_state_error(e)
140
144
  errors[feature.name] = e
141
145
  if errors:
142
146
  return interface.FeatureTeardownError(sandbox=self, errors=errors)
@@ -170,7 +174,8 @@ class BaseSandbox(interface.Sandbox):
170
174
  try:
171
175
  feature.teardown_session()
172
176
  except BaseException as e: # pylint: disable=broad-except
173
- self.report_maybe_state_error(e)
177
+ if isinstance(e, interface.SandboxStateError):
178
+ self.report_state_error(e)
174
179
  feature_teardown_errors[name] = e
175
180
 
176
181
  return interface.SessionTeardownError(
@@ -187,12 +192,12 @@ class BaseSandbox(interface.Sandbox):
187
192
  def _on_bound(self) -> None:
188
193
  """Called when the sandbox is bound."""
189
194
  super()._on_bound()
190
-
191
195
  self._features = pg.Dict({
192
196
  name: pg.clone(feature)
193
197
  for name, feature in self.environment.features.items()
198
+ if feature.is_applicable(self.image_id)
194
199
  })
195
- self._event_handlers = []
200
+ self._event_handler = self.environment.event_handler
196
201
  self._enable_pre_session_setup = (
197
202
  self.reusable and self.proactive_session_setup
198
203
  )
@@ -241,20 +246,6 @@ class BaseSandbox(interface.Sandbox):
241
246
  """Returns the housekeeping counter."""
242
247
  return self._housekeep_counter
243
248
 
244
- def add_event_handler(
245
- self,
246
- event_handler: event_handler_base.EventHandler | None
247
- ) -> None:
248
- """Sets the event handler for the sandbox."""
249
- self._event_handlers.append(event_handler)
250
-
251
- def remove_event_handler(
252
- self,
253
- event_handler: event_handler_base.EventHandler | None
254
- ) -> None:
255
- """Removes the event handler for the sandbox."""
256
- self._event_handlers.remove(event_handler)
257
-
258
249
  @property
259
250
  def state_errors(self) -> list[interface.SandboxStateError]:
260
251
  """Returns all errors encountered during sandbox lifecycle."""
@@ -272,28 +263,6 @@ class BaseSandbox(interface.Sandbox):
272
263
  """Returns the features in the sandbox."""
273
264
  return self._features
274
265
 
275
- def _enter_service_call(self) -> bool:
276
- """Enters a service call.
277
-
278
- Returns:
279
- True if the service call is at the top of the call stack.
280
- """
281
- v = getattr(self._tls_state, 'service_call_depth', None)
282
- if v is None:
283
- v = 0
284
- setattr(self._tls_state, 'service_call_depth', v + 1)
285
- return v == 0
286
-
287
- def _exit_service_call(self) -> bool:
288
- """Exits a service call.
289
-
290
- Returns:
291
- True if the service call is at the top of the call stack.
292
- """
293
- v = getattr(self._tls_state, 'service_call_depth')
294
- setattr(self._tls_state, 'service_call_depth', v - 1)
295
- return v == 1
296
-
297
266
  #
298
267
  # Sandbox start/shutdown.
299
268
  #
@@ -364,7 +333,8 @@ class BaseSandbox(interface.Sandbox):
364
333
  '[%s]: Sandbox failed to start in %.2f seconds: %s',
365
334
  self.id, duration, e
366
335
  )
367
- self.report_maybe_state_error(e)
336
+ if isinstance(e, interface.SandboxStateError):
337
+ self.report_state_error(e)
368
338
  self.on_start(duration, e)
369
339
  self.shutdown()
370
340
  raise e
@@ -437,7 +407,8 @@ class BaseSandbox(interface.Sandbox):
437
407
  shutdown_error = None
438
408
  except BaseException as e: # pylint: disable=broad-except
439
409
  shutdown_error = e
440
- self.report_maybe_state_error(e)
410
+ if isinstance(e, interface.SandboxStateError):
411
+ self.report_state_error(e)
441
412
  self._set_status(interface.Sandbox.Status.OFFLINE)
442
413
  pg.logging.error(
443
414
  '[%s]: Sandbox shutdown with error: %s',
@@ -535,7 +506,8 @@ class BaseSandbox(interface.Sandbox):
535
506
  self._set_status(self.Status.IN_SESSION)
536
507
  self.on_session_start(session_id, time.time() - self._session_start_time)
537
508
  except BaseException as e: # pylint: disable=broad-except
538
- self.report_maybe_state_error(e)
509
+ if isinstance(e, interface.SandboxStateError):
510
+ self.report_state_error(e)
539
511
  self.on_session_start(
540
512
  session_id, time.time() - self._session_start_time, e
541
513
  )
@@ -621,7 +593,8 @@ class BaseSandbox(interface.Sandbox):
621
593
  self.id,
622
594
  e
623
595
  )
624
- self.report_maybe_state_error(e)
596
+ if isinstance(e, interface.SandboxStateError):
597
+ self.report_state_error(e)
625
598
  self.shutdown()
626
599
 
627
600
  # End session before setting up the next session.
@@ -660,7 +633,6 @@ class BaseSandbox(interface.Sandbox):
660
633
  shutdown_sandbox = True
661
634
 
662
635
  self._session_start_time = None
663
- self._session_event_handler = None
664
636
 
665
637
  if shutdown_sandbox:
666
638
  self.shutdown()
@@ -672,6 +644,28 @@ class BaseSandbox(interface.Sandbox):
672
644
  and end_session_error.has_non_sandbox_state_error):
673
645
  raise end_session_error # pylint: disable=raising-bad-type
674
646
 
647
+ @contextlib.contextmanager
648
+ def track_activity(
649
+ self,
650
+ name: str,
651
+ **kwargs: Any
652
+ ) -> Iterator[None]:
653
+ """Tracks an activity for the sandbox."""
654
+ start_time = time.time()
655
+ error = None
656
+ try:
657
+ yield None
658
+ except BaseException as e: # pylint: disable=broad-except
659
+ error = e
660
+ raise
661
+ finally:
662
+ self.on_activity(
663
+ name=name,
664
+ duration=time.time() - start_time,
665
+ error=error,
666
+ **kwargs
667
+ )
668
+
675
669
  #
676
670
  # Housekeeping.
677
671
  #
@@ -717,7 +711,7 @@ class BaseSandbox(interface.Sandbox):
717
711
  str(e)
718
712
  )
719
713
  self._housekeep_counter += 1
720
- self.report_maybe_state_error(e)
714
+ self.report_state_error(e)
721
715
  self.on_housekeep(time.time() - housekeep_start, e)
722
716
  self.shutdown()
723
717
  break
@@ -739,7 +733,7 @@ class BaseSandbox(interface.Sandbox):
739
733
  feature.name,
740
734
  e,
741
735
  )
742
- self.report_maybe_state_error(e)
736
+ self.report_state_error(e)
743
737
  self._housekeep_counter += 1
744
738
  self.on_housekeep(time.time() - housekeep_start, e)
745
739
  self.shutdown()
@@ -759,8 +753,7 @@ class BaseSandbox(interface.Sandbox):
759
753
  error: BaseException | None = None
760
754
  ) -> None:
761
755
  """Called when the sandbox is started."""
762
- for handler in self._event_handlers:
763
- handler.on_sandbox_start(self.environment, self, duration, error)
756
+ self._event_handler.on_sandbox_start(self, duration, error)
764
757
 
765
758
  def on_status_change(
766
759
  self,
@@ -769,14 +762,9 @@ class BaseSandbox(interface.Sandbox):
769
762
  ) -> None:
770
763
  """Called when the sandbox status changes."""
771
764
  status_duration = time.time() - self._status_start_time
772
- for handler in self._event_handlers:
773
- handler.on_sandbox_status_change(
774
- self.environment,
775
- self,
776
- old_status,
777
- new_status,
778
- status_duration
779
- )
765
+ self._event_handler.on_sandbox_status_change(
766
+ self, old_status, new_status, status_duration
767
+ )
780
768
 
781
769
  def on_shutdown(
782
770
  self,
@@ -784,14 +772,13 @@ class BaseSandbox(interface.Sandbox):
784
772
  error: BaseException | None = None
785
773
  ) -> None:
786
774
  """Called when the sandbox is shutdown."""
787
- if self._start_time is None:
788
- lifetime = 0.0
789
- else:
790
- lifetime = time.time() - self._start_time
791
- for handler in self._event_handlers:
792
- handler.on_sandbox_shutdown(
793
- self.environment, self, duration, lifetime, error
794
- )
775
+ self._event_handler.on_sandbox_shutdown(
776
+ sandbox=self,
777
+ duration=duration,
778
+ lifetime=(0.0 if self._start_time is None
779
+ else (time.time() - self._start_time)),
780
+ error=error
781
+ )
795
782
 
796
783
  def on_housekeep(
797
784
  self,
@@ -800,72 +787,13 @@ class BaseSandbox(interface.Sandbox):
800
787
  **kwargs
801
788
  ) -> None:
802
789
  """Called when the sandbox finishes a round of housekeeping."""
803
- counter = self._housekeep_counter
804
- for handler in self._event_handlers:
805
- handler.on_sandbox_housekeep(
806
- self.environment, self, counter, duration, error, **kwargs
807
- )
808
-
809
- def on_feature_setup(
810
- self,
811
- feature: interface.Feature,
812
- duration: float,
813
- error: BaseException | None = None
814
- ) -> None:
815
- """Called when a feature is setup."""
816
- for handler in self._event_handlers:
817
- handler.on_feature_setup(
818
- self.environment, self, feature, duration, error
819
- )
820
-
821
- def on_feature_teardown(
822
- self,
823
- feature: interface.Feature,
824
- duration: float,
825
- error: BaseException | None = None
826
- ) -> None:
827
- """Called when a feature is teardown."""
828
- for handler in self._event_handlers:
829
- handler.on_feature_teardown(
830
- self.environment, self, feature, duration, error
831
- )
832
-
833
- def on_feature_setup_session(
834
- self,
835
- feature: interface.Feature,
836
- duration: float,
837
- error: BaseException | None = None
838
- ) -> None:
839
- """Called when a feature is setup for a user session."""
840
- for handler in self._event_handlers:
841
- handler.on_feature_setup_session(
842
- self.environment, self, feature, self.session_id, duration, error
843
- )
844
-
845
- def on_feature_teardown_session(
846
- self,
847
- feature: interface.Feature,
848
- duration: float,
849
- error: BaseException | None = None
850
- ) -> None:
851
- """Called when a feature is teardown for a user session."""
852
- for handler in self._event_handlers:
853
- handler.on_feature_teardown_session(
854
- self.environment, self, feature, self.session_id, duration, error
855
- )
856
-
857
- def on_feature_housekeep(
858
- self,
859
- feature: interface.Feature,
860
- counter: int,
861
- duration: float,
862
- error: BaseException | None = None
863
- ) -> None:
864
- """Called when a feature is housekeeping."""
865
- for handler in self._event_handlers:
866
- handler.on_feature_housekeep(
867
- self.environment, self, feature, counter, duration, error
868
- )
790
+ self._event_handler.on_sandbox_housekeep(
791
+ sandbox=self,
792
+ counter=self._housekeep_counter,
793
+ duration=duration,
794
+ error=error,
795
+ **kwargs
796
+ )
869
797
 
870
798
  def on_session_start(
871
799
  self,
@@ -874,31 +802,29 @@ class BaseSandbox(interface.Sandbox):
874
802
  error: BaseException | None = None
875
803
  ) -> None:
876
804
  """Called when the user session starts."""
877
- for handler in self._event_handlers:
878
- handler.on_session_start(
879
- self.environment, self, session_id, duration, error
880
- )
805
+ self._event_handler.on_sandbox_session_start(
806
+ sandbox=self,
807
+ session_id=session_id,
808
+ duration=duration,
809
+ error=error
810
+ )
881
811
 
882
812
  def on_activity(
883
813
  self,
884
814
  name: str,
885
815
  duration: float,
886
- feature: interface.Feature | None = None,
887
816
  error: BaseException | None = None,
888
817
  **kwargs
889
818
  ) -> None:
890
819
  """Called when a sandbox activity is performed."""
891
- for handler in self._event_handlers:
892
- handler.on_sandbox_activity(
893
- name=name,
894
- environment=self.environment,
895
- sandbox=self,
896
- feature=feature,
897
- session_id=self.session_id,
898
- duration=duration,
899
- error=error,
900
- **kwargs
901
- )
820
+ self._event_handler.on_sandbox_activity(
821
+ name=name,
822
+ sandbox=self,
823
+ session_id=self.session_id,
824
+ duration=duration,
825
+ error=error,
826
+ **kwargs
827
+ )
902
828
 
903
829
  def on_session_end(
904
830
  self,
@@ -907,213 +833,10 @@ class BaseSandbox(interface.Sandbox):
907
833
  error: BaseException | None = None
908
834
  ) -> None:
909
835
  """Called when the user session ends."""
910
- lifetime = time.time() - self._session_start_time
911
- for handler in self._event_handlers:
912
- handler.on_session_end(
913
- self.environment, self, session_id, duration, lifetime, error
914
- )
915
-
916
-
917
- #
918
- # Sandbox service decorator.
919
- #
920
-
921
-
922
- def sandbox_service(
923
- critical_errors: Sequence[
924
- Type[BaseException] | tuple[Type[BaseException], str]
925
- ] | None = None
926
- ) -> Callable[..., Any]:
927
- """Decorator for Sandbox/Feature methods exposed as sandbox services.
928
-
929
- This decorator will catch errors and map to `SandboxStateError` if the
930
- error matches any of the critical errors. Consequently, the sandbox will be
931
- shutdown automatically when the error is raised.
932
-
933
- Example:
934
-
935
- ```
936
- with env:
937
- with env.sandbox() as sb:
938
- try:
939
- sb.test_feature.do_something_with_non_state_error()
940
- except ValueError:
941
- # sandbox will not be shutdown.
942
- pass
943
-
944
- try:
945
- sb.test_feature.do_something_with_state_error()
946
- except ValueError:
947
- assert sb.state == sb.Status.OFFLINE
948
- ```
949
-
950
- If the decorated method returns a context manager, a wrapper context manager
951
- will be returned, which will end the session when exiting the context.
952
-
953
- Example:
954
-
955
- ```
956
- with env:
957
- with env.test_feature.do_something_with_context_manager() as result:
958
- # sandbox will be alive during the whole context manager cycle.
959
- ```
960
-
961
- For sandbox service methods, an optional `session_id` argument can be passed
962
- to create a new session for the service call, even its signature does not
963
- contain a `session_id` argument.
964
-
965
- Args:
966
- critical_errors: A sequence of exception types or tuples of exception type
967
- and error messages (described in regular expression), when matched, treat
968
- the sandbox as in a bad state, which will trigger a shutdown.
969
-
970
- Returns:
971
- The decorator function.
972
- """
973
- critical_errors = critical_errors or []
974
-
975
- def decorator(func):
976
- signature = pg.typing.get_signature(func)
977
- if 'session_id' in signature.arg_names:
978
- raise ValueError(
979
- '`session_id` should not be used as argument for sandbox '
980
- 'service method. Please use `self.session_id` instead.'
981
- )
982
-
983
- def to_kwargs(*args, **kwargs):
984
- num_non_self_args = len(signature.arg_names) - 1
985
- if len(args) > num_non_self_args:
986
- assert signature.varargs is not None, (signature, args)
987
- kwargs[signature.varargs.name] = tuple(args[num_non_self_args:])
988
- args = args[:num_non_self_args]
989
- for i in range(len(args)):
990
- # The first argument is `self`.
991
- kwargs[signature.arg_names[i + 1]] = args[i]
992
- return kwargs
993
-
994
- @functools.wraps(func)
995
- def method_wrapper(self, *args, **kwargs) -> Any:
996
- """Helper function to safely execute logics in the sandbox."""
997
-
998
- assert isinstance(self, (BaseSandbox, interface.Feature)), self
999
- sandbox = self.sandbox if isinstance(self, interface.Feature) else self
1000
-
1001
- # We count the service call stack depth so we could shutdown the sandbox
1002
- # at the top upon sandbox state error.
1003
- sandbox._enter_service_call() # pylint: disable=protected-access
1004
-
1005
- # When a capability is directly accessed from the environment,
1006
- # we create a new session for the capability call. This
1007
- # prevents the sandbox from being reused for other feature calls.
1008
- if sandbox.status == interface.Sandbox.Status.ACQUIRED:
1009
- new_session = True
1010
- new_session_id = kwargs.get('session_id')
1011
- if new_session_id is None:
1012
- new_session_id = sandbox.environment.new_session_id()
1013
-
1014
- # If it's a feature method called from the environment, start a new
1015
- # session for the feature call.
1016
- sandbox.start_session(new_session_id)
1017
- else:
1018
- new_session = False
1019
-
1020
- kwargs.pop('session_id', None)
1021
- result = None
1022
- error = None
1023
- start_time = time.time()
1024
-
1025
- try:
1026
- # Execute the service function.
1027
- result = func(self, *args, **kwargs)
1028
-
1029
- # If the result is a context manager, wrap it with a context manager
1030
- # to end the session when exiting.
1031
- if isinstance(result, contextlib.AbstractContextManager):
1032
- return _service_context_manager_wrapper(
1033
- service=result,
1034
- sandbox_or_feature=self,
1035
- sandbox=sandbox,
1036
- name=func.__name__,
1037
- kwargs=to_kwargs(*args, **kwargs),
1038
- start_time=start_time,
1039
- new_session=new_session
1040
- )
1041
-
1042
- # Otherwise, return the result and end the session in the finally block.
1043
- return result
1044
- except BaseException as e:
1045
- error = e
1046
- sandbox.report_maybe_state_error(e)
1047
- if pg.match_error(e, critical_errors):
1048
- state_error = interface.SandboxStateError(
1049
- 'Sandbox encountered an unexpected error executing '
1050
- f'`{func.__name__}` (args={args!r}, kwargs={kwargs!r}): {e}',
1051
- sandbox=self
1052
- )
1053
- sandbox.report_maybe_state_error(state_error)
1054
- raise state_error from e
1055
- raise
1056
- finally:
1057
- is_topmost_call = sandbox._exit_service_call() # pylint: disable=protected-access
1058
- if not isinstance(result, contextlib.AbstractContextManager):
1059
- self.on_activity(
1060
- name=func.__name__,
1061
- duration=time.time() - start_time,
1062
- error=error,
1063
- **to_kwargs(*args, **kwargs),
1064
- )
1065
- if new_session:
1066
- assert is_topmost_call
1067
-
1068
- # End the session if it's from a feature method and the result
1069
- # is not a context manager.
1070
- sandbox.end_session()
1071
-
1072
- # Shutdown the sandbox if it is at the top of the service call stack and
1073
- # has state errors.
1074
- if (is_topmost_call
1075
- and sandbox.state_errors
1076
- # Sandbox service method might be called during shutting down, in
1077
- # that case we don't want to shutdown the sandbox again.
1078
- and not sandbox.is_shutting_down):
1079
- sandbox.shutdown()
1080
-
1081
- return method_wrapper
1082
- return decorator
1083
-
1084
-
1085
- @contextlib.contextmanager
1086
- def _service_context_manager_wrapper(
1087
- service: contextlib.AbstractContextManager[Any],
1088
- sandbox_or_feature: BaseSandbox | interface.Feature,
1089
- sandbox: interface.Sandbox,
1090
- name: str,
1091
- kwargs: dict[str, Any],
1092
- new_session: bool,
1093
- start_time: float,
1094
- ) -> Iterator[Any]:
1095
- """Context manager wrapper for ending a sandbox session when exiting."""
1096
- error = None
1097
- sandbox._enter_service_call() # pylint: disable=protected-access
1098
-
1099
- try:
1100
- with service as result:
1101
- yield result
1102
- except BaseException as e:
1103
- error = e
1104
- sandbox.report_maybe_state_error(error)
1105
- raise
1106
- finally:
1107
- sandbox_or_feature.on_activity(
1108
- name=name,
1109
- error=error,
1110
- duration=time.time() - start_time,
1111
- **kwargs,
836
+ self._event_handler.on_sandbox_session_end(
837
+ sandbox=self,
838
+ session_id=session_id,
839
+ duration=duration,
840
+ lifetime=time.time() - self._session_start_time,
841
+ error=error
1112
842
  )
1113
- is_topmost_call = sandbox._exit_service_call() # pylint: disable=protected-access
1114
-
1115
- if new_session:
1116
- assert is_topmost_call
1117
- sandbox.end_session()
1118
- elif isinstance(error, interface.SandboxStateError):
1119
- sandbox.shutdown()