langfun 0.1.2.dev202509250804__py3-none-any.whl → 0.1.2.dev202509270803__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.

@@ -99,6 +99,8 @@ VertexAIGeminiFlash1_5 = VertexAIGemini15Flash
99
99
  # OpenAI models.
100
100
  from langfun.core.llms.openai import OpenAI
101
101
 
102
+ from langfun.core.llms.openai import Gpt5
103
+ from langfun.core.llms.openai import Gpt5Mini
102
104
  from langfun.core.llms.openai import Gpt41
103
105
  from langfun.core.llms.openai import GptO3
104
106
  from langfun.core.llms.openai import GptO4Mini
@@ -49,6 +49,53 @@ class OpenAIModelInfo(lf.ModelInfo):
49
49
  #
50
50
 
51
51
  SUPPORTED_MODELS = [
52
+ # GPT-5 models
53
+ OpenAIModelInfo(
54
+ model_id='gpt-5',
55
+ alias_for='gpt-5-2025-08-07',
56
+ in_service=True,
57
+ model_type='instruction-tuned',
58
+ description='GPT 5 model (latest stable).',
59
+ url='https://platform.openai.com/docs/models/gpt-5',
60
+ input_modalities=OpenAIModelInfo.INPUT_IMAGE_TYPES,
61
+ context_length=lf.ModelInfo.ContextLength(
62
+ max_input_tokens=400_000,
63
+ max_output_tokens=128_000,
64
+ ),
65
+ pricing=lf.ModelInfo.Pricing(
66
+ cost_per_1m_cached_input_tokens=0.125,
67
+ cost_per_1m_input_tokens=1.25,
68
+ cost_per_1m_output_tokens=10.0,
69
+ ),
70
+ # Tier 5 rate limits.
71
+ rate_limits=lf.ModelInfo.RateLimits(
72
+ max_requests_per_minute=15_000,
73
+ max_tokens_per_minute=40_000_000,
74
+ ),
75
+ ),
76
+ OpenAIModelInfo(
77
+ model_id='gpt-5-mini',
78
+ alias_for='gpt-5-mini-2025-08-07',
79
+ in_service=True,
80
+ model_type='instruction-tuned',
81
+ description='GPT 5 mini model (latest stable).',
82
+ url='https://platform.openai.com/docs/models/gpt-5-mini',
83
+ input_modalities=OpenAIModelInfo.INPUT_IMAGE_TYPES,
84
+ context_length=lf.ModelInfo.ContextLength(
85
+ max_input_tokens=400_000,
86
+ max_output_tokens=128_000,
87
+ ),
88
+ pricing=lf.ModelInfo.Pricing(
89
+ cost_per_1m_cached_input_tokens=0.025,
90
+ cost_per_1m_input_tokens=0.25,
91
+ cost_per_1m_output_tokens=2.0,
92
+ ),
93
+ # Tier 5 rate limits.
94
+ rate_limits=lf.ModelInfo.RateLimits(
95
+ max_requests_per_minute=180_000_000,
96
+ max_tokens_per_minute=30_000_000,
97
+ ),
98
+ ),
52
99
  # GPT-4.1 models
53
100
  OpenAIModelInfo(
54
101
  model_id='gpt-4.1',
@@ -1069,6 +1116,16 @@ class OpenAI(openai_compatible.OpenAICompatible):
1069
1116
  return super()._request_args(options)
1070
1117
 
1071
1118
 
1119
+ class Gpt5(OpenAI):
1120
+ """GPT-5."""
1121
+ model = 'gpt-5'
1122
+
1123
+
1124
+ class Gpt5Mini(OpenAI):
1125
+ """GPT-5 mini."""
1126
+ model = 'gpt-5-mini'
1127
+
1128
+
1072
1129
  class Gpt41(OpenAI):
1073
1130
  """GPT-4.1."""
1074
1131
  model = 'gpt-4.1'
langfun/env/__init__.py CHANGED
@@ -13,16 +13,12 @@
13
13
  # limitations under the License.
14
14
  """Environment for LLM agents."""
15
15
 
16
- # pylint: disable=g-importing-member, g-bad-import-order
17
- from langfun.env.interface import EnvironmentId
18
- from langfun.env.interface import SandboxId
19
-
16
+ # pylint: disable=g-importing-member, g-bad-import-order, g-import-not-at-top
20
17
  from langfun.env.interface import EnvironmentError # pylint: disable=redefined-builtin
21
18
  from langfun.env.interface import EnvironmentOutageError
22
19
  from langfun.env.interface import EnvironmentOverloadError
23
20
  from langfun.env.interface import SandboxError
24
21
  from langfun.env.interface import SandboxStateError
25
- from langfun.env.interface import EnvironmentEventHandler
26
22
 
27
23
  from langfun.env.interface import Environment
28
24
  from langfun.env.interface import Sandbox
@@ -35,4 +31,7 @@ from langfun.env.base_feature import BaseFeature
35
31
  from langfun.env import load_balancers
36
32
  from langfun.env.load_balancers import LoadBalancer
37
33
 
34
+ from langfun.env import event_handlers
35
+ EventHandler = event_handlers.EventHandler
36
+
38
37
  # Google-internal imports.
@@ -34,6 +34,7 @@ import langfun.core as lf
34
34
  from langfun.env import base_sandbox
35
35
  from langfun.env import interface
36
36
  from langfun.env import load_balancers
37
+ from langfun.env.event_handlers import base as event_handler_base
37
38
  import pyglove as pg
38
39
 
39
40
 
@@ -65,10 +66,20 @@ class BaseEnvironment(interface.Environment):
65
66
  load_balancer: Annotated[
66
67
  load_balancers.LoadBalancer,
67
68
  (
68
- 'The load balancer for the environment.'
69
+ 'The load balancer for the environment to acquire sandboxes.'
69
70
  )
70
71
  ] = load_balancers.RoundRobin()
71
72
 
73
+ sandbox_keepalive_interval: Annotated[
74
+ float | None,
75
+ (
76
+ 'The interval in seconds to send keepalive pings to sandboxes. '
77
+ 'If None, sandbox keepalive is disabled. Please note that sandbox '
78
+ 'keepalive is different from feature housekeeping. Usually sandbox '
79
+ 'keepalive and feature housekeeping are different operations.'
80
+ )
81
+ ] = None
82
+
72
83
  proactive_session_setup: Annotated[
73
84
  bool,
74
85
  (
@@ -79,6 +90,13 @@ class BaseEnvironment(interface.Environment):
79
90
  )
80
91
  ] = True
81
92
 
93
+ event_handlers: Annotated[
94
+ list[event_handler_base.EventHandler],
95
+ (
96
+ 'User handler for the environment events.'
97
+ )
98
+ ] = []
99
+
82
100
  outage_grace_period: Annotated[
83
101
  float,
84
102
  (
@@ -97,13 +115,15 @@ class BaseEnvironment(interface.Environment):
97
115
  )
98
116
  ] = 10.0
99
117
 
100
- stats_report_interval: Annotated[
118
+ housekeep_interval: Annotated[
101
119
  float,
102
120
  (
103
- 'The interval in seconds for reporting the environment stats. '
104
- 'If 0, stats will not be reported.'
121
+ 'The interval in seconds for environment housekeeping. It recycles '
122
+ 'the dead sandboxes in the pool. This interval is the minimal time '
123
+ 'to detect outage while there is no request to obtain new sandboxes.'
124
+ 'This is applicable only when the environment enables pooling.'
105
125
  )
106
- ] = 60.0
126
+ ] = 10.0
107
127
 
108
128
  pool_operation_max_parallelism: Annotated[
109
129
  int,
@@ -145,6 +165,7 @@ class BaseEnvironment(interface.Environment):
145
165
  sandbox_id: str,
146
166
  reusable: bool,
147
167
  proactive_session_setup: bool,
168
+ keepalive_interval: float | None,
148
169
  ) -> base_sandbox.BaseSandbox:
149
170
  """Creates a sandbox with the given identifier.
150
171
 
@@ -153,6 +174,8 @@ class BaseEnvironment(interface.Environment):
153
174
  reusable: Whether the sandbox is reusable across user sessions.
154
175
  proactive_session_setup: Whether the sandbox performs session setup work
155
176
  before a user session is started.
177
+ keepalive_interval: Interval to ping the sandbox for keeping it alive.
178
+ If None, the sandbox will not be pinged.
156
179
 
157
180
  Returns:
158
181
  The created sandbox.
@@ -194,6 +217,7 @@ class BaseEnvironment(interface.Environment):
194
217
  def _start(self) -> None:
195
218
  """Implementation of starting the environment."""
196
219
  if self.min_pool_size > 0:
220
+ # Pre-allocate the sandbox pool before usage.
197
221
  self._sandbox_pool = [None] * self.min_pool_size
198
222
  for i, sandbox, _ in lf.concurrent_map(
199
223
  lambda i: self._bring_up_sandbox_with_retry(
@@ -207,12 +231,15 @@ class BaseEnvironment(interface.Environment):
207
231
  ),
208
232
  ):
209
233
  self._sandbox_pool[i] = sandbox
234
+
210
235
  self._next_sandbox_id = len(self._sandbox_pool)
211
- self._housekeep_thread = threading.Thread(
212
- target=self._housekeep_loop, daemon=True
213
- )
214
- self._housekeep_counter = 0
215
- self._housekeep_thread.start()
236
+
237
+ if self.enable_pooling:
238
+ self._housekeep_thread = threading.Thread(
239
+ target=self._housekeep_loop, daemon=True
240
+ )
241
+ self._housekeep_counter = 0
242
+ self._housekeep_thread.start()
216
243
 
217
244
  def _shutdown(self) -> None:
218
245
  """Implementation of shutting down the environment."""
@@ -256,7 +283,7 @@ class BaseEnvironment(interface.Environment):
256
283
  @property
257
284
  def enable_pooling(self) -> bool:
258
285
  """Returns whether the environment enables pooling."""
259
- return self.min_pool_size > 0
286
+ return self.max_pool_size > 0
260
287
 
261
288
  @property
262
289
  def status(self) -> interface.Environment.Status:
@@ -314,11 +341,6 @@ class BaseEnvironment(interface.Environment):
314
341
  self._start_time = time.time()
315
342
  self._set_status(self.Status.ONLINE)
316
343
  self.on_start(duration=time.time() - starting_time)
317
-
318
- pg.logging.info(
319
- '[%s]: %s started in %.2f seconds.',
320
- self.id, self.__class__.__name__, time.time() - starting_time
321
- )
322
344
  except BaseException as e:
323
345
  self.on_start(duration=time.time() - starting_time, error=e)
324
346
  self.shutdown()
@@ -338,13 +360,8 @@ class BaseEnvironment(interface.Environment):
338
360
  self._set_status(self.Status.SHUTTING_DOWN)
339
361
 
340
362
  try:
341
- with pg.timeit('env.shutdown') as t:
342
- self._shutdown()
363
+ self._shutdown()
343
364
  self.on_shutdown()
344
- pg.logging.info(
345
- '[%s]: %s shutdown in %.2f seconds.',
346
- self.id, self.__class__.__name__, t.elapse
347
- )
348
365
  except BaseException as e: # pylint: disable=broad-except
349
366
  self.on_shutdown(error=e)
350
367
  raise e
@@ -416,6 +433,7 @@ class BaseEnvironment(interface.Environment):
416
433
  sandbox_id=sandbox_id,
417
434
  reusable=self.enable_pooling,
418
435
  proactive_session_setup=self.proactive_session_setup,
436
+ keepalive_interval=self.sandbox_keepalive_interval,
419
437
  )
420
438
  for handler in self.event_handlers:
421
439
  sandbox.add_event_handler(handler)
@@ -482,19 +500,8 @@ class BaseEnvironment(interface.Environment):
482
500
 
483
501
  def _housekeep_loop(self) -> None:
484
502
  """Housekeeping loop for the environment."""
485
- pg.logging.info(
486
- '[%s]: %s maintenance thread started.', self.id, self.__class__.__name__
487
- )
488
- stats_report_time = time.time()
489
503
  while self._status not in (self.Status.SHUTTING_DOWN, self.Status.OFFLINE):
490
504
  housekeep_start_time = time.time()
491
- if time.time() - stats_report_time > self.stats_report_interval:
492
- pg.logging.info(
493
- '[%s] %s stats: %s.',
494
- self.id, self.__class__.__name__, self.stats()
495
- )
496
- stats_report_time = time.time()
497
-
498
505
  dead_pool_indices = [
499
506
  i for i, s in enumerate(self._sandbox_pool)
500
507
  if s.status == interface.Sandbox.Status.OFFLINE
@@ -514,7 +521,7 @@ class BaseEnvironment(interface.Environment):
514
521
  break
515
522
  self._housekeep_counter += 1
516
523
  self.on_housekeep(time.time() - housekeep_start_time)
517
- time.sleep(1)
524
+ time.sleep(self.housekeep_interval)
518
525
 
519
526
  def _replace_dead_sandboxes(self, dead_pool_indices: list[int]) -> bool:
520
527
  """Replaces a dead sandbox with a new one.
@@ -23,6 +23,7 @@ the `Environment` and `Sandbox` interfaces directly.
23
23
  """
24
24
 
25
25
  import functools
26
+ import os
26
27
  import time
27
28
  from typing import Annotated, Callable
28
29
 
@@ -106,6 +107,14 @@ class BaseFeature(interface.Feature):
106
107
  assert self._sandbox is not None, 'Feature has not been set up yet.'
107
108
  return self._sandbox
108
109
 
110
+ @property
111
+ def working_dir(self) -> str | None:
112
+ """Returns the working directory of the feature."""
113
+ sandbox_workdir = self.sandbox.working_dir
114
+ if sandbox_workdir is None:
115
+ return None
116
+ return os.path.join(sandbox_workdir, self.name)
117
+
109
118
  #
110
119
  # Setup and teardown of the feature.
111
120
  #
@@ -204,7 +213,7 @@ class BaseFeature(interface.Feature):
204
213
  self,
205
214
  name: str,
206
215
  duration: float,
207
- error: BaseException | None,
216
+ error: BaseException | None = None,
208
217
  **kwargs
209
218
  ) -> None:
210
219
  """Called when a sandbox activity is performed."""
@@ -29,6 +29,7 @@ import time
29
29
  from typing import Annotated, Any, Callable, Iterator, Sequence, Type
30
30
 
31
31
  from langfun.env import interface
32
+ from langfun.env.event_handlers import base as event_handler_base
32
33
  import pyglove as pg
33
34
 
34
35
 
@@ -36,7 +37,7 @@ class BaseSandbox(interface.Sandbox):
36
37
  """Base class for a sandbox."""
37
38
 
38
39
  id: Annotated[
39
- interface.SandboxId,
40
+ interface.Sandbox.Id,
40
41
  'The identifier for the sandbox.'
41
42
  ]
42
43
 
@@ -242,14 +243,14 @@ class BaseSandbox(interface.Sandbox):
242
243
 
243
244
  def add_event_handler(
244
245
  self,
245
- event_handler: interface.EnvironmentEventHandler | None
246
+ event_handler: event_handler_base.EventHandler | None
246
247
  ) -> None:
247
248
  """Sets the event handler for the sandbox."""
248
249
  self._event_handlers.append(event_handler)
249
250
 
250
251
  def remove_event_handler(
251
252
  self,
252
- event_handler: interface.EnvironmentEventHandler | None
253
+ event_handler: event_handler_base.EventHandler | None
253
254
  ) -> None:
254
255
  """Removes the event handler for the sandbox."""
255
256
  self._event_handlers.remove(event_handler)
@@ -357,10 +358,6 @@ class BaseSandbox(interface.Sandbox):
357
358
 
358
359
  duration = time.time() - starting_time
359
360
  self.on_start(duration)
360
- pg.logging.info(
361
- '[%s]: Sandbox started in %.2f seconds.',
362
- self.id, duration
363
- )
364
361
  except BaseException as e: # pylint: disable=broad-except
365
362
  duration = time.time() - starting_time
366
363
  pg.logging.error(
@@ -422,7 +419,6 @@ class BaseSandbox(interface.Sandbox):
422
419
  return
423
420
 
424
421
  self._set_status(interface.Sandbox.Status.SHUTTING_DOWN)
425
- shutdown_start_time = time.time()
426
422
 
427
423
  if (self._housekeep_thread is not None
428
424
  and threading.current_thread() is not self._housekeep_thread):
@@ -433,15 +429,6 @@ class BaseSandbox(interface.Sandbox):
433
429
  try:
434
430
  self._shutdown()
435
431
  self._set_status(interface.Sandbox.Status.OFFLINE)
436
-
437
- pg.logging.info(
438
- '[%s]: Sandbox shutdown in %.2f seconds. '
439
- '(lifetime: %.2f seconds, teardown errors: %s)',
440
- self.id,
441
- time.time() - shutdown_start_time,
442
- time.time() - self._start_time if self._start_time else 0,
443
- teardown_error
444
- )
445
432
  self.on_shutdown(teardown_error)
446
433
  shutdown_error = None
447
434
  except BaseException as e: # pylint: disable=broad-except
@@ -656,14 +643,6 @@ class BaseSandbox(interface.Sandbox):
656
643
  self._set_status(interface.Sandbox.Status.ACQUIRED)
657
644
  shutdown_sandbox = True
658
645
 
659
- pg.logging.info(
660
- '[%s]: User session %s ended. '
661
- '(lifetime: %.2f seconds, teardown errors: %s).',
662
- self.id,
663
- self._session_id,
664
- time.time() - self._session_start_time,
665
- end_session_error
666
- )
667
646
  self._session_start_time = None
668
647
  self._session_event_handler = None
669
648
 
@@ -687,9 +666,31 @@ class BaseSandbox(interface.Sandbox):
687
666
  last_ping = now
688
667
  last_housekeep_time = {name: now for name in self._features.keys()}
689
668
 
669
+ def _next_housekeep_wait_time() -> float:
670
+ # Decide how long to sleep for the next housekeeping.
671
+ next_housekeep_time = None
672
+ if self.keepalive_interval is not None:
673
+ next_housekeep_time = last_ping + self.keepalive_interval
674
+
675
+ for name, feature in self._features.items():
676
+ if feature.housekeep_interval is None:
677
+ continue
678
+ next_feature_housekeep_time = (
679
+ last_housekeep_time[name] + feature.housekeep_interval
680
+ )
681
+ if (next_housekeep_time is None
682
+ or next_housekeep_time > next_feature_housekeep_time):
683
+ next_housekeep_time = next_feature_housekeep_time
684
+
685
+ # Housekeep loop is installed when at least one feature requires
686
+ # housekeeping or the sandbox has a keepalive interval.
687
+ assert next_housekeep_time is not None
688
+ return max(0, next_housekeep_time - time.time())
689
+
690
690
  while self._status not in (self.Status.SHUTTING_DOWN, self.Status.OFFLINE):
691
691
  housekeep_start = time.time()
692
692
  if self.keepalive_interval is not None:
693
+
693
694
  if time.time() - last_ping > self.keepalive_interval:
694
695
  try:
695
696
  self.ping()
@@ -731,7 +732,7 @@ class BaseSandbox(interface.Sandbox):
731
732
 
732
733
  self._housekeep_counter += 1
733
734
  self.on_housekeep(time.time() - housekeep_start)
734
- time.sleep(1)
735
+ time.sleep(_next_housekeep_wait_time())
735
736
 
736
737
  #
737
738
  # Event handlers subclasses can override.