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.
- langfun/core/__init__.py +1 -0
- langfun/core/agentic/action.py +107 -12
- langfun/core/agentic/action_eval.py +9 -2
- langfun/core/agentic/action_test.py +25 -0
- langfun/core/async_support.py +32 -3
- langfun/core/coding/python/correction.py +19 -9
- langfun/core/coding/python/execution.py +14 -12
- langfun/core/coding/python/generation.py +21 -16
- langfun/core/coding/python/sandboxing.py +23 -3
- langfun/core/component.py +42 -3
- langfun/core/concurrent.py +70 -6
- langfun/core/concurrent_test.py +1 -0
- langfun/core/console.py +1 -1
- langfun/core/data/conversion/anthropic.py +12 -3
- langfun/core/data/conversion/anthropic_test.py +8 -6
- langfun/core/data/conversion/gemini.py +9 -2
- langfun/core/data/conversion/gemini_test.py +12 -9
- langfun/core/data/conversion/openai.py +145 -31
- langfun/core/data/conversion/openai_test.py +161 -17
- langfun/core/eval/base.py +47 -43
- langfun/core/eval/base_test.py +4 -4
- langfun/core/eval/matching.py +5 -2
- langfun/core/eval/patching.py +3 -3
- langfun/core/eval/scoring.py +4 -3
- langfun/core/eval/v2/__init__.py +1 -0
- langfun/core/eval/v2/checkpointing.py +39 -5
- langfun/core/eval/v2/checkpointing_test.py +1 -1
- langfun/core/eval/v2/eval_test_helper.py +96 -0
- langfun/core/eval/v2/evaluation.py +87 -15
- langfun/core/eval/v2/evaluation_test.py +9 -3
- langfun/core/eval/v2/example.py +45 -39
- langfun/core/eval/v2/example_test.py +3 -3
- langfun/core/eval/v2/experiment.py +51 -8
- langfun/core/eval/v2/metric_values.py +31 -3
- langfun/core/eval/v2/metric_values_test.py +32 -0
- langfun/core/eval/v2/metrics.py +157 -44
- langfun/core/eval/v2/metrics_test.py +39 -18
- langfun/core/eval/v2/progress.py +30 -1
- langfun/core/eval/v2/progress_test.py +27 -0
- langfun/core/eval/v2/progress_tracking_test.py +3 -0
- langfun/core/eval/v2/reporting.py +90 -71
- langfun/core/eval/v2/reporting_test.py +20 -6
- langfun/core/eval/v2/runners/__init__.py +26 -0
- langfun/core/eval/v2/{runners.py → runners/base.py} +22 -124
- langfun/core/eval/v2/runners/debug.py +40 -0
- langfun/core/eval/v2/runners/debug_test.py +79 -0
- langfun/core/eval/v2/runners/parallel.py +100 -0
- langfun/core/eval/v2/runners/parallel_test.py +98 -0
- langfun/core/eval/v2/runners/sequential.py +47 -0
- langfun/core/eval/v2/runners/sequential_test.py +175 -0
- langfun/core/langfunc.py +45 -130
- langfun/core/langfunc_test.py +6 -4
- langfun/core/language_model.py +103 -16
- langfun/core/language_model_test.py +9 -3
- langfun/core/llms/__init__.py +7 -1
- langfun/core/llms/anthropic.py +157 -2
- langfun/core/llms/azure_openai.py +29 -17
- langfun/core/llms/cache/base.py +25 -3
- langfun/core/llms/cache/in_memory.py +48 -7
- langfun/core/llms/cache/in_memory_test.py +14 -4
- langfun/core/llms/compositional.py +25 -1
- langfun/core/llms/deepseek.py +30 -2
- langfun/core/llms/fake.py +32 -1
- langfun/core/llms/gemini.py +14 -9
- langfun/core/llms/google_genai.py +29 -1
- langfun/core/llms/groq.py +28 -3
- langfun/core/llms/llama_cpp.py +23 -4
- langfun/core/llms/openai.py +36 -3
- langfun/core/llms/openai_compatible.py +148 -27
- langfun/core/llms/openai_compatible_test.py +207 -20
- langfun/core/llms/openai_test.py +0 -2
- langfun/core/llms/rest.py +12 -1
- langfun/core/llms/vertexai.py +51 -8
- langfun/core/logging.py +1 -1
- langfun/core/mcp/client.py +77 -22
- langfun/core/mcp/client_test.py +8 -35
- langfun/core/mcp/session.py +94 -29
- langfun/core/mcp/session_test.py +54 -0
- langfun/core/mcp/tool.py +151 -22
- langfun/core/mcp/tool_test.py +197 -0
- langfun/core/memory.py +1 -0
- langfun/core/message.py +160 -55
- langfun/core/message_test.py +65 -81
- langfun/core/modalities/__init__.py +8 -0
- langfun/core/modalities/audio.py +21 -1
- langfun/core/modalities/image.py +19 -1
- langfun/core/modalities/mime.py +62 -3
- langfun/core/modalities/pdf.py +19 -1
- langfun/core/modalities/video.py +21 -1
- langfun/core/modality.py +167 -29
- langfun/core/modality_test.py +42 -12
- langfun/core/natural_language.py +1 -1
- langfun/core/sampling.py +4 -4
- langfun/core/sampling_test.py +20 -4
- langfun/core/structured/__init__.py +2 -24
- langfun/core/structured/completion.py +34 -44
- langfun/core/structured/completion_test.py +23 -43
- langfun/core/structured/description.py +54 -50
- langfun/core/structured/function_generation.py +29 -12
- langfun/core/structured/mapping.py +81 -37
- langfun/core/structured/parsing.py +95 -79
- langfun/core/structured/parsing_test.py +0 -3
- langfun/core/structured/querying.py +215 -142
- langfun/core/structured/querying_test.py +65 -29
- langfun/core/structured/schema/__init__.py +48 -0
- langfun/core/structured/schema/base.py +664 -0
- langfun/core/structured/schema/base_test.py +531 -0
- langfun/core/structured/schema/json.py +174 -0
- langfun/core/structured/schema/json_test.py +121 -0
- langfun/core/structured/schema/python.py +316 -0
- langfun/core/structured/schema/python_test.py +410 -0
- langfun/core/structured/schema_generation.py +33 -14
- langfun/core/structured/scoring.py +47 -36
- langfun/core/structured/tokenization.py +26 -11
- langfun/core/subscription.py +2 -2
- langfun/core/template.py +174 -49
- langfun/core/template_test.py +123 -17
- langfun/env/__init__.py +8 -2
- langfun/env/base_environment.py +320 -128
- langfun/env/base_environment_test.py +473 -0
- langfun/env/base_feature.py +92 -15
- langfun/env/base_feature_test.py +228 -0
- langfun/env/base_sandbox.py +84 -361
- langfun/env/base_sandbox_test.py +1235 -0
- langfun/env/event_handlers/__init__.py +1 -1
- langfun/env/event_handlers/chain.py +233 -0
- langfun/env/event_handlers/chain_test.py +253 -0
- langfun/env/event_handlers/event_logger.py +95 -98
- langfun/env/event_handlers/event_logger_test.py +21 -21
- langfun/env/event_handlers/metric_writer.py +225 -140
- langfun/env/event_handlers/metric_writer_test.py +23 -6
- langfun/env/interface.py +854 -40
- langfun/env/interface_test.py +112 -2
- langfun/env/load_balancers_test.py +23 -2
- langfun/env/test_utils.py +126 -84
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/METADATA +1 -1
- langfun-0.1.2.dev202511160804.dist-info/RECORD +211 -0
- langfun/core/eval/v2/runners_test.py +0 -343
- langfun/core/structured/schema.py +0 -987
- langfun/core/structured/schema_test.py +0 -982
- langfun/env/base_test.py +0 -1481
- langfun/env/event_handlers/base.py +0 -350
- langfun-0.1.2.dev202510230805.dist-info/RECORD +0 -195
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/top_level.txt +0 -0
langfun/env/base_sandbox.py
CHANGED
|
@@ -26,10 +26,9 @@ import contextlib
|
|
|
26
26
|
import functools
|
|
27
27
|
import threading
|
|
28
28
|
import time
|
|
29
|
-
from typing import Annotated, Any,
|
|
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
|
|
102
|
+
def report_state_error(self, e: interface.SandboxStateError) -> None:
|
|
99
103
|
"""Reports sandbox state errors."""
|
|
100
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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()
|