langfun 0.1.2.dev202510280805__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.
- langfun/core/agentic/action.py +31 -3
- langfun/core/agentic/action_test.py +25 -0
- langfun/core/llms/gemini.py +3 -3
- langfun/env/__init__.py +4 -0
- langfun/env/base_environment.py +247 -105
- langfun/env/base_feature.py +15 -0
- langfun/env/base_sandbox.py +47 -240
- langfun/env/base_test.py +813 -603
- langfun/env/event_handlers/event_logger_test.py +2 -2
- langfun/env/event_handlers/metric_writer.py +49 -0
- langfun/env/event_handlers/metric_writer_test.py +19 -2
- langfun/env/interface.py +197 -13
- langfun/env/interface_test.py +81 -2
- langfun/env/load_balancers_test.py +19 -0
- langfun/env/test_utils.py +10 -7
- {langfun-0.1.2.dev202510280805.dist-info → langfun-0.1.2.dev202510300805.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202510280805.dist-info → langfun-0.1.2.dev202510300805.dist-info}/RECORD +20 -20
- {langfun-0.1.2.dev202510280805.dist-info → langfun-0.1.2.dev202510300805.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510280805.dist-info → langfun-0.1.2.dev202510300805.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510280805.dist-info → langfun-0.1.2.dev202510300805.dist-info}/top_level.txt +0 -0
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
|
|
|
@@ -170,6 +171,9 @@ class SessionTeardownError(SandboxError):
|
|
|
170
171
|
class Environment(pg.Object):
|
|
171
172
|
"""Base class for an environment."""
|
|
172
173
|
|
|
174
|
+
# Disable symbolic comparison and hashing for environment objects.
|
|
175
|
+
use_symbolic_comparison = False
|
|
176
|
+
|
|
173
177
|
@dataclasses.dataclass(frozen=True)
|
|
174
178
|
class Id:
|
|
175
179
|
"""Identifier for an environment."""
|
|
@@ -220,6 +224,20 @@ class Environment(pg.Object):
|
|
|
220
224
|
def id(self) -> Id:
|
|
221
225
|
"""Returns the identifier for the environment."""
|
|
222
226
|
|
|
227
|
+
@property
|
|
228
|
+
@abc.abstractmethod
|
|
229
|
+
def image_ids(self) -> list[str]:
|
|
230
|
+
"""Returns the non-dynamic image IDs served by the environment."""
|
|
231
|
+
|
|
232
|
+
def image_id_for(self, feature: 'Feature') -> str:
|
|
233
|
+
"""Returns the default image ID for the environment."""
|
|
234
|
+
for image_id in self.image_ids:
|
|
235
|
+
if feature.is_applicable(image_id):
|
|
236
|
+
return image_id
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f'No image ID found for feature {feature.name} in {self.image_ids}.'
|
|
239
|
+
)
|
|
240
|
+
|
|
223
241
|
@property
|
|
224
242
|
@abc.abstractmethod
|
|
225
243
|
def status(self) -> Status:
|
|
@@ -245,9 +263,16 @@ class Environment(pg.Object):
|
|
|
245
263
|
"""
|
|
246
264
|
|
|
247
265
|
@abc.abstractmethod
|
|
248
|
-
def acquire(
|
|
266
|
+
def acquire(
|
|
267
|
+
self,
|
|
268
|
+
image_id: str | None = None,
|
|
269
|
+
) -> 'Sandbox':
|
|
249
270
|
"""Acquires a free sandbox from the environment.
|
|
250
271
|
|
|
272
|
+
Args:
|
|
273
|
+
image_id: The image ID to use for the sandbox. If None, it will be
|
|
274
|
+
automatically determined by the environment.
|
|
275
|
+
|
|
251
276
|
Returns:
|
|
252
277
|
A free sandbox from the environment.
|
|
253
278
|
|
|
@@ -257,7 +282,7 @@ class Environment(pg.Object):
|
|
|
257
282
|
"""
|
|
258
283
|
|
|
259
284
|
@abc.abstractmethod
|
|
260
|
-
def new_session_id(self) -> str:
|
|
285
|
+
def new_session_id(self, feature_hint: str | None = None) -> str:
|
|
261
286
|
"""Generates a new session ID."""
|
|
262
287
|
|
|
263
288
|
#
|
|
@@ -298,33 +323,63 @@ class Environment(pg.Object):
|
|
|
298
323
|
|
|
299
324
|
def sandbox(
|
|
300
325
|
self,
|
|
326
|
+
image_id: str | None = None,
|
|
301
327
|
session_id: str | None = None,
|
|
302
328
|
) -> ContextManager['Sandbox']:
|
|
303
329
|
"""Gets a sandbox from the environment and starts a new user session."""
|
|
304
|
-
return self.acquire().new_session(session_id)
|
|
330
|
+
return self.acquire(image_id=image_id).new_session(session_id)
|
|
305
331
|
|
|
306
332
|
def __getattr__(self, name: str) -> Any:
|
|
307
|
-
"""Gets a feature from a free sandbox from the environment.
|
|
333
|
+
"""Gets a feature session from a free sandbox from the environment.
|
|
308
334
|
|
|
309
335
|
Example:
|
|
310
336
|
```
|
|
311
337
|
with XboxEnvironment(
|
|
312
338
|
features={'selenium': SeleniumFeature()}
|
|
313
339
|
) as env:
|
|
314
|
-
|
|
340
|
+
with env.selenium() as selenium:
|
|
341
|
+
driver = selenium.get_driver()
|
|
315
342
|
```
|
|
316
343
|
|
|
317
344
|
Args:
|
|
318
345
|
name: The name of the feature.
|
|
319
346
|
|
|
320
347
|
Returns:
|
|
321
|
-
A
|
|
348
|
+
A callable `(image_id, *, session_id) -> ContextManager[Feature]` that
|
|
349
|
+
creates a context manager for the requested feature under a new client
|
|
350
|
+
session.
|
|
322
351
|
"""
|
|
323
352
|
if name in self.features:
|
|
324
|
-
return self.
|
|
353
|
+
return _feature_session_creator(self, self.features[name])
|
|
325
354
|
raise AttributeError(name)
|
|
326
355
|
|
|
327
356
|
|
|
357
|
+
@contextlib.contextmanager
|
|
358
|
+
def _session_for_feature(
|
|
359
|
+
environment: Environment,
|
|
360
|
+
feature: 'Feature',
|
|
361
|
+
image_id: str | None = None,
|
|
362
|
+
session_id: str | None = None,
|
|
363
|
+
) -> Iterator['Feature']:
|
|
364
|
+
"""Returns a context manager for a session for a feature."""
|
|
365
|
+
if image_id is None:
|
|
366
|
+
image_id = environment.image_id_for(feature)
|
|
367
|
+
elif not feature.is_applicable(image_id):
|
|
368
|
+
raise ValueError(
|
|
369
|
+
f'Feature {feature.name!r} is not applicable to image {image_id!r}.'
|
|
370
|
+
)
|
|
371
|
+
sandbox = environment.acquire(image_id=image_id)
|
|
372
|
+
with sandbox.new_session(session_id=session_id, feature_hint=feature.name):
|
|
373
|
+
yield sandbox.features[feature.name]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _feature_session_creator(environment: Environment, feature: 'Feature'):
|
|
377
|
+
"""Returns a callable that returns a context manager for a feature session."""
|
|
378
|
+
def fn(image_id: str | None = None, session_id: str | None = None):
|
|
379
|
+
return _session_for_feature(environment, feature, image_id, session_id)
|
|
380
|
+
return fn
|
|
381
|
+
|
|
382
|
+
|
|
328
383
|
# Enable automatic conversion from str to Environment.Id.
|
|
329
384
|
pg.typing.register_converter(str, Environment.Id, Environment.Id)
|
|
330
385
|
|
|
@@ -332,14 +387,18 @@ pg.typing.register_converter(str, Environment.Id, Environment.Id)
|
|
|
332
387
|
class Sandbox(pg.Object):
|
|
333
388
|
"""Interface for sandboxes."""
|
|
334
389
|
|
|
390
|
+
# Disable symbolic comparison and hashing for sandbox objects.
|
|
391
|
+
use_symbolic_comparison = False
|
|
392
|
+
|
|
335
393
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
336
394
|
class Id:
|
|
337
395
|
"""Identifier for a sandbox."""
|
|
338
396
|
environment_id: Environment.Id
|
|
397
|
+
image_id: str
|
|
339
398
|
sandbox_id: str
|
|
340
399
|
|
|
341
400
|
def __str__(self) -> str:
|
|
342
|
-
return f'{self.environment_id}/{self.sandbox_id}'
|
|
401
|
+
return f'{self.environment_id}/{self.image_id}:{self.sandbox_id}'
|
|
343
402
|
|
|
344
403
|
def working_dir(self, root_dir: str | None) -> str | None:
|
|
345
404
|
"""Returns the download directory for the sandbox."""
|
|
@@ -347,6 +406,7 @@ class Sandbox(pg.Object):
|
|
|
347
406
|
return None
|
|
348
407
|
return os.path.join(
|
|
349
408
|
self.environment_id.working_dir(root_dir),
|
|
409
|
+
_make_path_compatible(self.image_id),
|
|
350
410
|
_make_path_compatible(self.sandbox_id)
|
|
351
411
|
)
|
|
352
412
|
|
|
@@ -438,6 +498,11 @@ class Sandbox(pg.Object):
|
|
|
438
498
|
def id(self) -> Id:
|
|
439
499
|
"""Returns the identifier for the sandbox."""
|
|
440
500
|
|
|
501
|
+
@property
|
|
502
|
+
@abc.abstractmethod
|
|
503
|
+
def image_id(self) -> str:
|
|
504
|
+
"""Returns the image ID used for bootstrapping the sandbox."""
|
|
505
|
+
|
|
441
506
|
@property
|
|
442
507
|
@abc.abstractmethod
|
|
443
508
|
def environment(self) -> Environment:
|
|
@@ -458,10 +523,17 @@ class Sandbox(pg.Object):
|
|
|
458
523
|
"""Returns True if the sandbox is online."""
|
|
459
524
|
return self.status.is_online
|
|
460
525
|
|
|
461
|
-
@property
|
|
462
526
|
@abc.abstractmethod
|
|
463
|
-
def
|
|
464
|
-
"""
|
|
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
|
+
"""
|
|
465
537
|
|
|
466
538
|
@abc.abstractmethod
|
|
467
539
|
def start(self) -> None:
|
|
@@ -592,6 +664,9 @@ class Sandbox(pg.Object):
|
|
|
592
664
|
`end_session` should always be called for each `start_session` call, even
|
|
593
665
|
when the session fails to start, to ensure proper cleanup.
|
|
594
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
|
+
|
|
595
670
|
`end_session` may fail with two sources of errors:
|
|
596
671
|
|
|
597
672
|
1. SandboxStateError: If the sandbox is in a bad state or session teardown
|
|
@@ -615,6 +690,24 @@ class Sandbox(pg.Object):
|
|
|
615
690
|
def session_id(self) -> str | None:
|
|
616
691
|
"""Returns the current user session identifier."""
|
|
617
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
|
+
|
|
618
711
|
#
|
|
619
712
|
# API related to a user session.
|
|
620
713
|
# A sandbox could be reused across different user sessions.
|
|
@@ -626,6 +719,8 @@ class Sandbox(pg.Object):
|
|
|
626
719
|
def new_session(
|
|
627
720
|
self,
|
|
628
721
|
session_id: str | None = None,
|
|
722
|
+
*,
|
|
723
|
+
feature_hint: str | None = None,
|
|
629
724
|
) -> Iterator['Sandbox']:
|
|
630
725
|
"""Context manager for obtaining a sandbox for a user session.
|
|
631
726
|
|
|
@@ -636,6 +731,9 @@ class Sandbox(pg.Object):
|
|
|
636
731
|
Args:
|
|
637
732
|
session_id: The identifier for the user session. If not provided, a random
|
|
638
733
|
ID will be generated.
|
|
734
|
+
feature_hint: A hint of which feature is the main user intent when
|
|
735
|
+
starting the session. This is used for generating the session id if
|
|
736
|
+
`session_id` is not provided.
|
|
639
737
|
|
|
640
738
|
Yields:
|
|
641
739
|
The sandbox for the user session.
|
|
@@ -646,10 +744,13 @@ class Sandbox(pg.Object):
|
|
|
646
744
|
errors.
|
|
647
745
|
"""
|
|
648
746
|
if session_id is None:
|
|
649
|
-
session_id = self.environment.new_session_id()
|
|
747
|
+
session_id = self.environment.new_session_id(feature_hint)
|
|
650
748
|
self.start_session(session_id)
|
|
651
749
|
try:
|
|
652
750
|
yield self
|
|
751
|
+
except SandboxStateError as e:
|
|
752
|
+
self.report_state_error(e)
|
|
753
|
+
raise
|
|
653
754
|
finally:
|
|
654
755
|
self.end_session()
|
|
655
756
|
|
|
@@ -683,6 +784,9 @@ class Sandbox(pg.Object):
|
|
|
683
784
|
class Feature(pg.Object):
|
|
684
785
|
"""Interface for sandbox features."""
|
|
685
786
|
|
|
787
|
+
# Disable symbolic comparison and hashing for sandbox objects.
|
|
788
|
+
allow_symbolic_comparison = False
|
|
789
|
+
|
|
686
790
|
@property
|
|
687
791
|
@abc.abstractmethod
|
|
688
792
|
def name(self) -> str:
|
|
@@ -700,6 +804,10 @@ class Feature(pg.Object):
|
|
|
700
804
|
AssertError: If the feature is not set up with a sandbox yet.
|
|
701
805
|
"""
|
|
702
806
|
|
|
807
|
+
@abc.abstractmethod
|
|
808
|
+
def is_applicable(self, image_id: str) -> bool:
|
|
809
|
+
"""Returns True if the feature is applicable to the given image."""
|
|
810
|
+
|
|
703
811
|
@abc.abstractmethod
|
|
704
812
|
def setup(self, sandbox: Sandbox) -> None:
|
|
705
813
|
"""Sets up the feature, which is called once when the sandbox is up.
|
|
@@ -811,6 +919,12 @@ class Feature(pg.Object):
|
|
|
811
919
|
assert self.sandbox is not None
|
|
812
920
|
return self.sandbox.session_id
|
|
813
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
|
+
|
|
814
928
|
|
|
815
929
|
def _make_path_compatible(id_str: str) -> str:
|
|
816
930
|
"""Makes a path compatible with CNS."""
|
|
@@ -824,3 +938,73 @@ def _make_path_compatible(id_str: str) -> str:
|
|
|
824
938
|
'>': '',
|
|
825
939
|
})
|
|
826
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
|
langfun/env/interface_test.py
CHANGED
|
@@ -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
|
|
|
@@ -29,15 +31,92 @@ class IdTest(unittest.TestCase):
|
|
|
29
31
|
def test_sandbox_id(self):
|
|
30
32
|
sandbox_id = interface.Sandbox.Id(
|
|
31
33
|
environment_id=interface.Environment.Id('env'),
|
|
34
|
+
image_id='image:2025_01_01_00_00_00',
|
|
32
35
|
sandbox_id='sandbox'
|
|
33
36
|
)
|
|
34
|
-
self.assertEqual(str(sandbox_id), 'env/sandbox')
|
|
37
|
+
self.assertEqual(str(sandbox_id), 'env/image:2025_01_01_00_00_00:sandbox')
|
|
35
38
|
self.assertEqual(
|
|
36
39
|
sandbox_id.working_dir(root_dir='/tmp'),
|
|
37
|
-
'/tmp/env/sandbox'
|
|
40
|
+
'/tmp/env/image_2025_01_01_00_00_00/sandbox'
|
|
38
41
|
)
|
|
39
42
|
self.assertIsNone(sandbox_id.working_dir(root_dir=None))
|
|
40
43
|
|
|
41
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
|
+
|
|
42
121
|
if __name__ == '__main__':
|
|
43
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
|
|
@@ -22,6 +24,7 @@ from langfun.env import load_balancers
|
|
|
22
24
|
class TestingSandbox(interface.Sandbox):
|
|
23
25
|
sandbox_id: str
|
|
24
26
|
status: interface.Sandbox.Status = interface.Sandbox.Status.READY
|
|
27
|
+
image_id: str = 'test_image'
|
|
25
28
|
|
|
26
29
|
def _on_bound(self) -> None:
|
|
27
30
|
super()._on_bound()
|
|
@@ -31,6 +34,7 @@ class TestingSandbox(interface.Sandbox):
|
|
|
31
34
|
def id(self) -> interface.Sandbox.Id:
|
|
32
35
|
return interface.Sandbox.Id(
|
|
33
36
|
environment_id=interface.Environment.Id('testing-env'),
|
|
37
|
+
image_id=self.image_id,
|
|
34
38
|
sandbox_id=self.sandbox_id
|
|
35
39
|
)
|
|
36
40
|
|
|
@@ -46,6 +50,9 @@ class TestingSandbox(interface.Sandbox):
|
|
|
46
50
|
def state_errors(self) -> list[interface.SandboxStateError]:
|
|
47
51
|
return []
|
|
48
52
|
|
|
53
|
+
def report_state_error(self, error: interface.SandboxStateError) -> None:
|
|
54
|
+
pass
|
|
55
|
+
|
|
49
56
|
def set_status(self, status: interface.Sandbox.Status) -> None:
|
|
50
57
|
self.rebind(status=status, skip_notification=True)
|
|
51
58
|
|
|
@@ -68,6 +75,18 @@ class TestingSandbox(interface.Sandbox):
|
|
|
68
75
|
def session_id(self) -> str | None:
|
|
69
76
|
return self._session_id
|
|
70
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
|
+
|
|
71
90
|
|
|
72
91
|
class RoundRobinTest(unittest.TestCase):
|
|
73
92
|
|
langfun/env/test_utils.py
CHANGED
|
@@ -27,7 +27,7 @@ import pyglove as pg
|
|
|
27
27
|
|
|
28
28
|
class TestingEnvironment(base_environment.BaseEnvironment):
|
|
29
29
|
"""Testing environment for unit tests."""
|
|
30
|
-
|
|
30
|
+
image_ids: list[str] = ['test_image']
|
|
31
31
|
housekeep_interval: float = 0.0
|
|
32
32
|
simulate_start_error: Type[BaseException] | None = None
|
|
33
33
|
simulate_shutdown_error: Type[BaseException] | None = None
|
|
@@ -45,6 +45,7 @@ class TestingEnvironment(base_environment.BaseEnvironment):
|
|
|
45
45
|
|
|
46
46
|
def _create_sandbox(
|
|
47
47
|
self,
|
|
48
|
+
image_id: str,
|
|
48
49
|
sandbox_id: str,
|
|
49
50
|
reusable: bool,
|
|
50
51
|
proactive_session_setup: bool,
|
|
@@ -54,8 +55,10 @@ class TestingEnvironment(base_environment.BaseEnvironment):
|
|
|
54
55
|
environment=self,
|
|
55
56
|
id=interface.Sandbox.Id(
|
|
56
57
|
environment_id=self.id,
|
|
58
|
+
image_id=image_id,
|
|
57
59
|
sandbox_id=sandbox_id
|
|
58
60
|
),
|
|
61
|
+
image_id=image_id,
|
|
59
62
|
reusable=reusable,
|
|
60
63
|
proactive_session_setup=proactive_session_setup,
|
|
61
64
|
keepalive_interval=keepalive_interval,
|
|
@@ -109,7 +112,8 @@ class TestingSandbox(base_sandbox.BaseSandbox):
|
|
|
109
112
|
self._raise_error('Sandbox shutdown error', self.simulate_shutdown_error)
|
|
110
113
|
super()._shutdown()
|
|
111
114
|
|
|
112
|
-
@
|
|
115
|
+
@interface.treat_as_sandbox_state_error(errors=(RuntimeError,))
|
|
116
|
+
@interface.log_sandbox_activity()
|
|
113
117
|
def shell(
|
|
114
118
|
self,
|
|
115
119
|
code: str,
|
|
@@ -181,19 +185,19 @@ class TestingFeature(base_feature.BaseFeature):
|
|
|
181
185
|
if self.call_end_session_on_teardown_session:
|
|
182
186
|
self.sandbox.end_session()
|
|
183
187
|
|
|
184
|
-
@
|
|
188
|
+
@interface.log_sandbox_activity()
|
|
185
189
|
def num_shell_calls(self) -> int:
|
|
186
190
|
return len(self.sandbox._shell_history) # pylint: disable=protected-access
|
|
187
191
|
|
|
188
|
-
@
|
|
192
|
+
@interface.log_sandbox_activity()
|
|
189
193
|
def bad_shell_call(self) -> None:
|
|
190
194
|
self.sandbox.shell('bad command', raise_error=RuntimeError)
|
|
191
195
|
|
|
192
|
-
@
|
|
196
|
+
@interface.log_sandbox_activity()
|
|
193
197
|
def show_session_id(self):
|
|
194
198
|
return self.session_id
|
|
195
199
|
|
|
196
|
-
@
|
|
200
|
+
@interface.log_sandbox_activity()
|
|
197
201
|
def call_with_varargs(self, code: str, *args, **kwargs):
|
|
198
202
|
del code, args, kwargs
|
|
199
203
|
return 0
|
|
@@ -202,7 +206,6 @@ class TestingFeature(base_feature.BaseFeature):
|
|
|
202
206
|
super()._on_bound()
|
|
203
207
|
self._service = None
|
|
204
208
|
|
|
205
|
-
@base_sandbox.sandbox_service()
|
|
206
209
|
@contextlib.contextmanager
|
|
207
210
|
def my_service(self) -> Iterator[Service]:
|
|
208
211
|
try:
|
|
@@ -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=
|
|
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=
|
|
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
|
|
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=
|
|
178
|
-
langfun/env/base_environment.py,sha256=
|
|
179
|
-
langfun/env/base_feature.py,sha256=
|
|
180
|
-
langfun/env/base_sandbox.py,sha256=
|
|
181
|
-
langfun/env/base_test.py,sha256=
|
|
182
|
-
langfun/env/interface.py,sha256=
|
|
183
|
-
langfun/env/interface_test.py,sha256=
|
|
177
|
+
langfun/env/__init__.py,sha256=VTjLmS_SkxtkBCmr6hBb4iACeLLPmcbWJkK7MEXsATA,1652
|
|
178
|
+
langfun/env/base_environment.py,sha256=8Cpwb4D37zHj5AK4qgmGIPyxIMEDxTIPHXqxTSNkE_M,25030
|
|
179
|
+
langfun/env/base_feature.py,sha256=JDEhL9LkbBHB0c603guchry7cy_zaIReg5vqExyQQgg,6902
|
|
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=
|
|
186
|
-
langfun/env/test_utils.py,sha256=
|
|
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
|
-
langfun/env/event_handlers/event_logger_test.py,sha256=
|
|
191
|
-
langfun/env/event_handlers/metric_writer.py,sha256=
|
|
192
|
-
langfun/env/event_handlers/metric_writer_test.py,sha256=
|
|
193
|
-
langfun-0.1.2.
|
|
194
|
-
langfun-0.1.2.
|
|
195
|
-
langfun-0.1.2.
|
|
196
|
-
langfun-0.1.2.
|
|
197
|
-
langfun-0.1.2.
|
|
190
|
+
langfun/env/event_handlers/event_logger_test.py,sha256=ryupxaEP9D8wdtSsSwZRSZwqFaHCaSD-bFSea_T7QJo,9133
|
|
191
|
+
langfun/env/event_handlers/metric_writer.py,sha256=F_Gk1lpJX5SZ6-Hyrf_-utf4gvSKvMmcov8VkKogZCU,19618
|
|
192
|
+
langfun/env/event_handlers/metric_writer_test.py,sha256=sntUifTPHGixUshIgVBX4Q9vJL-xbeS0Cpd5X5hSQyQ,5955
|
|
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,,
|
|
File without changes
|
{langfun-0.1.2.dev202510280805.dist-info → langfun-0.1.2.dev202510300805.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{langfun-0.1.2.dev202510280805.dist-info → langfun-0.1.2.dev202510300805.dist-info}/top_level.txt
RENAMED
|
File without changes
|