langfun 0.1.2.dev202510290805__py3-none-any.whl → 0.1.2.dev202510310805__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_sandbox.py +41 -239
- langfun/env/base_test.py +15 -25
- langfun/env/interface.py +112 -4
- langfun/env/interface_test.py +78 -0
- langfun/env/load_balancers_test.py +17 -0
- langfun/env/test_utils.py +6 -6
- {langfun-0.1.2.dev202510290805.dist-info → langfun-0.1.2.dev202510310805.dist-info}/METADATA +1 -1
- {langfun-0.1.2.dev202510290805.dist-info → langfun-0.1.2.dev202510310805.dist-info}/RECORD +15 -15
- {langfun-0.1.2.dev202510290805.dist-info → langfun-0.1.2.dev202510310805.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510290805.dist-info → langfun-0.1.2.dev202510310805.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510290805.dist-info → langfun-0.1.2.dev202510310805.dist-info}/top_level.txt +0 -0
langfun/core/agentic/action.py
CHANGED
|
@@ -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
|
-
|
|
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()
|
langfun/core/llms/gemini.py
CHANGED
|
@@ -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
|
-
|
|
835
|
-
|
|
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
|
langfun/env/base_sandbox.py
CHANGED
|
@@ -26,7 +26,7 @@ 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
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
|
|
103
|
+
def report_state_error(self, e: interface.SandboxStateError) -> None:
|
|
104
104
|
"""Reports sandbox state errors."""
|
|
105
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
464
|
-
with
|
|
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
|
|
1254
|
-
with
|
|
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
|
-
|
|
1257
|
-
|
|
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.
|
|
1491
|
-
with self.
|
|
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
|
-
|
|
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.
|
|
1602
|
-
with self.
|
|
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
|
-
|
|
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
|
|
528
|
-
"""
|
|
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
|
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
|
|
|
@@ -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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
196
|
+
@interface.log_sandbox_activity()
|
|
196
197
|
def show_session_id(self):
|
|
197
198
|
return self.session_id
|
|
198
199
|
|
|
199
|
-
@
|
|
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:
|
|
@@ -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=
|
|
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=
|
|
181
|
-
langfun/env/base_test.py,sha256=
|
|
182
|
-
langfun/env/interface.py,sha256=
|
|
183
|
-
langfun/env/interface_test.py,sha256=
|
|
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
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.
|
|
194
|
-
langfun-0.1.2.
|
|
195
|
-
langfun-0.1.2.
|
|
196
|
-
langfun-0.1.2.
|
|
197
|
-
langfun-0.1.2.
|
|
193
|
+
langfun-0.1.2.dev202510310805.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
194
|
+
langfun-0.1.2.dev202510310805.dist-info/METADATA,sha256=IKnFNK0k4M96hXuSz3sVo6QOFf9z36tKYsRaeJNvKqg,7522
|
|
195
|
+
langfun-0.1.2.dev202510310805.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
196
|
+
langfun-0.1.2.dev202510310805.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
|
|
197
|
+
langfun-0.1.2.dev202510310805.dist-info/RECORD,,
|
|
File without changes
|
{langfun-0.1.2.dev202510290805.dist-info → langfun-0.1.2.dev202510310805.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{langfun-0.1.2.dev202510290805.dist-info → langfun-0.1.2.dev202510310805.dist-info}/top_level.txt
RENAMED
|
File without changes
|