langfun 0.1.2.dev202508250805__py3-none-any.whl → 0.1.2.dev202511110805__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of langfun might be problematic. Click here for more details.

Files changed (133) hide show
  1. langfun/__init__.py +1 -1
  2. langfun/core/__init__.py +6 -1
  3. langfun/core/agentic/__init__.py +4 -0
  4. langfun/core/agentic/action.py +412 -103
  5. langfun/core/agentic/action_eval.py +9 -2
  6. langfun/core/agentic/action_test.py +68 -6
  7. langfun/core/async_support.py +104 -5
  8. langfun/core/async_support_test.py +23 -0
  9. langfun/core/coding/python/correction.py +19 -9
  10. langfun/core/coding/python/execution.py +14 -12
  11. langfun/core/coding/python/generation.py +21 -16
  12. langfun/core/coding/python/sandboxing.py +23 -3
  13. langfun/core/component.py +42 -3
  14. langfun/core/concurrent.py +70 -6
  15. langfun/core/concurrent_test.py +9 -2
  16. langfun/core/console.py +1 -1
  17. langfun/core/data/conversion/anthropic.py +12 -3
  18. langfun/core/data/conversion/anthropic_test.py +8 -6
  19. langfun/core/data/conversion/gemini.py +9 -2
  20. langfun/core/data/conversion/gemini_test.py +12 -9
  21. langfun/core/data/conversion/openai.py +145 -31
  22. langfun/core/data/conversion/openai_test.py +161 -17
  23. langfun/core/eval/base.py +47 -43
  24. langfun/core/eval/base_test.py +4 -4
  25. langfun/core/eval/matching.py +5 -2
  26. langfun/core/eval/patching.py +3 -3
  27. langfun/core/eval/scoring.py +4 -3
  28. langfun/core/eval/v2/__init__.py +1 -0
  29. langfun/core/eval/v2/checkpointing.py +30 -4
  30. langfun/core/eval/v2/eval_test_helper.py +1 -1
  31. langfun/core/eval/v2/evaluation.py +60 -14
  32. langfun/core/eval/v2/example.py +22 -11
  33. langfun/core/eval/v2/experiment.py +51 -8
  34. langfun/core/eval/v2/metric_values.py +31 -3
  35. langfun/core/eval/v2/metric_values_test.py +32 -0
  36. langfun/core/eval/v2/metrics.py +39 -4
  37. langfun/core/eval/v2/metrics_test.py +14 -0
  38. langfun/core/eval/v2/progress.py +30 -1
  39. langfun/core/eval/v2/progress_test.py +27 -0
  40. langfun/core/eval/v2/progress_tracking_test.py +6 -0
  41. langfun/core/eval/v2/reporting.py +90 -71
  42. langfun/core/eval/v2/reporting_test.py +20 -6
  43. langfun/core/eval/v2/runners.py +27 -7
  44. langfun/core/eval/v2/runners_test.py +3 -0
  45. langfun/core/langfunc.py +45 -130
  46. langfun/core/langfunc_test.py +6 -4
  47. langfun/core/language_model.py +151 -31
  48. langfun/core/language_model_test.py +9 -3
  49. langfun/core/llms/__init__.py +12 -1
  50. langfun/core/llms/anthropic.py +157 -2
  51. langfun/core/llms/azure_openai.py +29 -17
  52. langfun/core/llms/cache/base.py +25 -3
  53. langfun/core/llms/cache/in_memory.py +48 -7
  54. langfun/core/llms/cache/in_memory_test.py +14 -4
  55. langfun/core/llms/compositional.py +25 -1
  56. langfun/core/llms/deepseek.py +30 -2
  57. langfun/core/llms/fake.py +39 -1
  58. langfun/core/llms/fake_test.py +9 -0
  59. langfun/core/llms/gemini.py +43 -7
  60. langfun/core/llms/google_genai.py +34 -1
  61. langfun/core/llms/groq.py +28 -3
  62. langfun/core/llms/llama_cpp.py +23 -4
  63. langfun/core/llms/openai.py +93 -3
  64. langfun/core/llms/openai_compatible.py +148 -27
  65. langfun/core/llms/openai_compatible_test.py +207 -20
  66. langfun/core/llms/openai_test.py +0 -2
  67. langfun/core/llms/rest.py +16 -1
  68. langfun/core/llms/vertexai.py +59 -8
  69. langfun/core/logging.py +1 -1
  70. langfun/core/mcp/__init__.py +10 -0
  71. langfun/core/mcp/client.py +177 -0
  72. langfun/core/mcp/client_test.py +71 -0
  73. langfun/core/mcp/session.py +241 -0
  74. langfun/core/mcp/session_test.py +54 -0
  75. langfun/core/mcp/testing/simple_mcp_client.py +33 -0
  76. langfun/core/mcp/testing/simple_mcp_server.py +33 -0
  77. langfun/core/mcp/tool.py +256 -0
  78. langfun/core/mcp/tool_test.py +197 -0
  79. langfun/core/memory.py +1 -0
  80. langfun/core/message.py +160 -55
  81. langfun/core/message_test.py +65 -81
  82. langfun/core/modalities/__init__.py +8 -0
  83. langfun/core/modalities/audio.py +21 -1
  84. langfun/core/modalities/image.py +19 -1
  85. langfun/core/modalities/mime.py +62 -3
  86. langfun/core/modalities/pdf.py +19 -1
  87. langfun/core/modalities/video.py +21 -1
  88. langfun/core/modality.py +167 -29
  89. langfun/core/modality_test.py +42 -12
  90. langfun/core/natural_language.py +1 -1
  91. langfun/core/sampling.py +4 -4
  92. langfun/core/sampling_test.py +20 -4
  93. langfun/core/structured/completion.py +34 -44
  94. langfun/core/structured/completion_test.py +23 -43
  95. langfun/core/structured/description.py +54 -50
  96. langfun/core/structured/function_generation.py +29 -12
  97. langfun/core/structured/mapping.py +74 -28
  98. langfun/core/structured/parsing.py +90 -74
  99. langfun/core/structured/parsing_test.py +0 -3
  100. langfun/core/structured/querying.py +242 -156
  101. langfun/core/structured/querying_test.py +95 -64
  102. langfun/core/structured/schema.py +70 -10
  103. langfun/core/structured/schema_generation.py +33 -14
  104. langfun/core/structured/scoring.py +45 -34
  105. langfun/core/structured/tokenization.py +24 -9
  106. langfun/core/subscription.py +2 -2
  107. langfun/core/template.py +175 -50
  108. langfun/core/template_test.py +123 -17
  109. langfun/env/__init__.py +43 -0
  110. langfun/env/base_environment.py +827 -0
  111. langfun/env/base_environment_test.py +473 -0
  112. langfun/env/base_feature.py +304 -0
  113. langfun/env/base_feature_test.py +228 -0
  114. langfun/env/base_sandbox.py +842 -0
  115. langfun/env/base_sandbox_test.py +1235 -0
  116. langfun/env/event_handlers/__init__.py +14 -0
  117. langfun/env/event_handlers/chain.py +233 -0
  118. langfun/env/event_handlers/chain_test.py +253 -0
  119. langfun/env/event_handlers/event_logger.py +472 -0
  120. langfun/env/event_handlers/event_logger_test.py +304 -0
  121. langfun/env/event_handlers/metric_writer.py +726 -0
  122. langfun/env/event_handlers/metric_writer_test.py +214 -0
  123. langfun/env/interface.py +1640 -0
  124. langfun/env/interface_test.py +151 -0
  125. langfun/env/load_balancers.py +59 -0
  126. langfun/env/load_balancers_test.py +139 -0
  127. langfun/env/test_utils.py +497 -0
  128. {langfun-0.1.2.dev202508250805.dist-info → langfun-0.1.2.dev202511110805.dist-info}/METADATA +7 -3
  129. langfun-0.1.2.dev202511110805.dist-info/RECORD +200 -0
  130. langfun-0.1.2.dev202508250805.dist-info/RECORD +0 -172
  131. {langfun-0.1.2.dev202508250805.dist-info → langfun-0.1.2.dev202511110805.dist-info}/WHEEL +0 -0
  132. {langfun-0.1.2.dev202508250805.dist-info → langfun-0.1.2.dev202511110805.dist-info}/licenses/LICENSE +0 -0
  133. {langfun-0.1.2.dev202508250805.dist-info → langfun-0.1.2.dev202511110805.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,151 @@
1
+ # Copyright 2025 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import contextlib
15
+ from typing import Iterator
16
+ import unittest
17
+ from langfun.env import interface
18
+
19
+
20
+ class IdTest(unittest.TestCase):
21
+
22
+ def test_environment_id(self):
23
+ env_id = interface.Environment.Id('env@1/a b:c#def')
24
+ self.assertEqual(str(env_id), 'env@1/a b:c#def')
25
+ self.assertEqual(
26
+ env_id.working_dir(root_dir='/tmp'),
27
+ '/tmp/env_1/ab_c_def'
28
+ )
29
+ self.assertIsNone(env_id.working_dir(root_dir=None))
30
+
31
+ def test_sandbox_id(self):
32
+ sandbox_id = interface.Sandbox.Id(
33
+ environment_id=interface.Environment.Id('env'),
34
+ image_id='image:2025_01_01_00_00_00',
35
+ sandbox_id='sandbox'
36
+ )
37
+ self.assertEqual(str(sandbox_id), 'env/image:2025_01_01_00_00_00:sandbox')
38
+ self.assertEqual(
39
+ sandbox_id.working_dir(root_dir='/tmp'),
40
+ '/tmp/env/image_2025_01_01_00_00_00/sandbox'
41
+ )
42
+ self.assertIsNone(sandbox_id.working_dir(root_dir=None))
43
+
44
+ def test_feature_id(self):
45
+ # For non-sandboxed feature.
46
+ feature_id = interface.Feature.Id(
47
+ container_id=interface.Environment.Id('env'),
48
+ feature_name='feature'
49
+ )
50
+ self.assertEqual(str(feature_id), 'env/feature')
51
+ self.assertEqual(
52
+ feature_id.working_dir(root_dir='/tmp'),
53
+ '/tmp/env/feature'
54
+ )
55
+ self.assertIsNone(feature_id.working_dir(root_dir=None))
56
+
57
+ # For sandboxed feature.
58
+ feature_id = interface.Feature.Id(
59
+ container_id=interface.Sandbox.Id(
60
+ environment_id=interface.Environment.Id('env'),
61
+ image_id='image1',
62
+ sandbox_id='0'
63
+ ),
64
+ feature_name='feature'
65
+ )
66
+ self.assertEqual(str(feature_id), 'env/image1:0/feature')
67
+ self.assertEqual(
68
+ feature_id.working_dir(root_dir='/tmp'),
69
+ '/tmp/env/image1/0/feature'
70
+ )
71
+ self.assertIsNone(feature_id.working_dir(root_dir=None))
72
+
73
+
74
+ class TestingSandbox(interface.Sandbox):
75
+
76
+ id: interface.Sandbox.Id = interface.Sandbox.Id(
77
+ environment_id=interface.Environment.Id('env'),
78
+ image_id='test_image',
79
+ sandbox_id='0:0'
80
+ )
81
+ image_id: str = 'test_image'
82
+ features: dict[str, interface.Feature] = {}
83
+ status: interface.Sandbox.Status = interface.Sandbox.Status.READY
84
+ session_id: str | None = None
85
+
86
+ def environment(self) -> interface.Environment:
87
+ pass
88
+
89
+ def _on_bound(self) -> None:
90
+ self.activities = []
91
+
92
+ def report_state_error(self, error: interface.SandboxStateError) -> None:
93
+ pass
94
+
95
+ def start(self) -> None:
96
+ pass
97
+
98
+ def shutdown(self) -> None:
99
+ pass
100
+
101
+ def start_session(self, session_id: str) -> None:
102
+ pass
103
+
104
+ def end_session(self, shutdown_sandbox: bool = False) -> None:
105
+ pass
106
+
107
+ @contextlib.contextmanager
108
+ def track_activity(
109
+ self,
110
+ name: str,
111
+ feature: interface.Feature | None = None,
112
+ **kwargs
113
+ ) -> Iterator[None]:
114
+ error = None
115
+ try:
116
+ yield
117
+ except BaseException as e:
118
+ error = e
119
+ raise
120
+ finally:
121
+ self.activities.append((name, error, kwargs))
122
+
123
+
124
+ class DecoratorTest(unittest.TestCase):
125
+
126
+ def test_treat_as_sandbox_state_error(self):
127
+
128
+ class SandboxA(TestingSandbox):
129
+
130
+ @interface.treat_as_sandbox_state_error(errors=(ValueError,))
131
+ def foo(self, bar: str) -> None:
132
+ raise ValueError(bar)
133
+
134
+ with self.assertRaises(interface.SandboxStateError):
135
+ SandboxA().foo('foo')
136
+
137
+ def test_log_sandbox_activity(self):
138
+
139
+ class SandboxB(TestingSandbox):
140
+
141
+ @interface.log_activity()
142
+ def bar(self, x: str) -> None:
143
+ pass
144
+
145
+ sb = SandboxB()
146
+ sb.bar('foo')
147
+ self.assertEqual(sb.activities, [('bar', None, {'x': 'foo'})])
148
+
149
+
150
+ if __name__ == '__main__':
151
+ unittest.main()
@@ -0,0 +1,59 @@
1
+ # Copyright 2025 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Load balancers for environments."""
15
+
16
+ import abc
17
+ import threading
18
+
19
+ from langfun.env import interface
20
+ import pyglove as pg
21
+
22
+
23
+ class LoadBalancer(pg.Object):
24
+ """Base class for load balancers."""
25
+
26
+ @abc.abstractmethod
27
+ def acquire(self, sandbox_pool: list[interface.Sandbox]) -> interface.Sandbox:
28
+ """Acquires a free sandbox from a pool of sandboxes.
29
+
30
+ The load balancer will pick a sandbox from the pool and mark it as pending.
31
+
32
+ Args:
33
+ sandbox_pool: The pool of sandboxes to pick from.
34
+
35
+ Raises:
36
+ IndexError: If all sandboxes in the pool are either busy or dead.
37
+ """
38
+
39
+
40
+ class RoundRobin(LoadBalancer):
41
+ """Round robin load balancer."""
42
+
43
+ def _on_bound(self):
44
+ super()._on_bound()
45
+ self._counter = 0
46
+ self._acquire_lock = threading.Lock()
47
+
48
+ def acquire(self, sandbox_pool: list[interface.Sandbox]) -> interface.Sandbox:
49
+ """Returns a free sandbox from the pool."""
50
+ with self._acquire_lock:
51
+ for _ in range(len(sandbox_pool)):
52
+ sandbox = sandbox_pool[self._counter % len(sandbox_pool)]
53
+ self._counter = self._counter + 1
54
+ if sandbox.status == interface.Sandbox.Status.READY:
55
+ # Mark the sandbox as acquired to prevent it from being acquired by
56
+ # other threads.
57
+ sandbox.set_acquired()
58
+ return sandbox
59
+ raise IndexError('No free sandbox in the pool.')
@@ -0,0 +1,139 @@
1
+ # Copyright 2025 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import concurrent.futures
15
+ import contextlib
16
+ import time
17
+ from typing import Any, Iterator
18
+ import unittest
19
+
20
+ from langfun.env import interface
21
+ from langfun.env import load_balancers
22
+
23
+
24
+ class TestingSandbox(interface.Sandbox):
25
+ sandbox_id: str
26
+ status: interface.Sandbox.Status = interface.Sandbox.Status.READY
27
+ image_id: str = 'test_image'
28
+
29
+ def _on_bound(self) -> None:
30
+ super()._on_bound()
31
+ self._session_id = None
32
+
33
+ @property
34
+ def id(self) -> interface.Sandbox.Id:
35
+ return interface.Sandbox.Id(
36
+ environment_id=interface.Environment.Id('testing-env'),
37
+ image_id=self.image_id,
38
+ sandbox_id=self.sandbox_id
39
+ )
40
+
41
+ @property
42
+ def environment(self) -> interface.Environment:
43
+ raise NotImplementedError()
44
+
45
+ @property
46
+ def features(self) -> dict[str, interface.Feature]:
47
+ raise NotImplementedError()
48
+
49
+ @property
50
+ def state_errors(self) -> list[interface.SandboxStateError]:
51
+ return []
52
+
53
+ def report_state_error(self, error: interface.SandboxStateError) -> None:
54
+ pass
55
+
56
+ def set_status(self, status: interface.Sandbox.Status) -> None:
57
+ self.rebind(status=status, skip_notification=True)
58
+
59
+ def set_acquired(self) -> None:
60
+ self.set_status(self.Status.ACQUIRED)
61
+
62
+ def start(self) -> None:
63
+ pass
64
+
65
+ def shutdown(self) -> None:
66
+ pass
67
+
68
+ def start_session(self, session_id: str) -> None:
69
+ self._session_id = session_id
70
+
71
+ def end_session(self, session_id: str) -> None:
72
+ self._session_id = None
73
+
74
+ @property
75
+ def session_id(self) -> str | None:
76
+ return self._session_id
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
+
90
+
91
+ class RoundRobinTest(unittest.TestCase):
92
+
93
+ def test_basic(self):
94
+ sandbox_pool = [
95
+ TestingSandbox('0', interface.Sandbox.Status.OFFLINE),
96
+ TestingSandbox('1', interface.Sandbox.Status.SETTING_UP),
97
+ TestingSandbox('2', interface.Sandbox.Status.IN_SESSION),
98
+ TestingSandbox('3', status=interface.Sandbox.Status.READY),
99
+ TestingSandbox('4', status=interface.Sandbox.Status.READY),
100
+ ]
101
+ lb = load_balancers.RoundRobin()
102
+ sandbox = lb.acquire(sandbox_pool)
103
+ self.assertIs(sandbox, sandbox_pool[3])
104
+ self.assertEqual(sandbox.status, interface.Sandbox.Status.ACQUIRED)
105
+
106
+ sandbox = lb.acquire(sandbox_pool)
107
+ self.assertIs(sandbox, sandbox_pool[4])
108
+ self.assertEqual(sandbox.status, interface.Sandbox.Status.ACQUIRED)
109
+
110
+ sandbox_pool[0].set_status(interface.Sandbox.Status.READY)
111
+ sandbox = lb.acquire(sandbox_pool)
112
+ self.assertIs(sandbox, sandbox_pool[0])
113
+ self.assertEqual(sandbox.status, interface.Sandbox.Status.ACQUIRED)
114
+
115
+ with self.assertRaisesRegex(IndexError, 'No free sandbox in the pool.'):
116
+ lb.acquire(sandbox_pool)
117
+
118
+ def test_thread_safety(self):
119
+ sandbox_pool = [TestingSandbox(str(i)) for i in range(64)]
120
+
121
+ lb = load_balancers.RoundRobin()
122
+
123
+ def _thread_func(i):
124
+ sandbox = lb.acquire(sandbox_pool)
125
+ time.sleep(0.1)
126
+ sandbox.set_status(interface.Sandbox.Status.IN_SESSION)
127
+ time.sleep(0.1)
128
+ sandbox.set_status(interface.Sandbox.Status.OFFLINE)
129
+ time.sleep(0.1)
130
+ sandbox.set_status(interface.Sandbox.Status.READY)
131
+ return i
132
+
133
+ with concurrent.futures.ThreadPoolExecutor(max_workers=64) as executor:
134
+ for i, o in enumerate(executor.map(_thread_func, range(1024))):
135
+ self.assertEqual(o, i)
136
+
137
+
138
+ if __name__ == '__main__':
139
+ unittest.main()