langfun 0.1.2.dev202509120804__py3-none-any.whl → 0.1.2.dev202512150805__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.
Files changed (162) hide show
  1. langfun/__init__.py +1 -1
  2. langfun/core/__init__.py +7 -1
  3. langfun/core/agentic/__init__.py +8 -1
  4. langfun/core/agentic/action.py +740 -112
  5. langfun/core/agentic/action_eval.py +9 -2
  6. langfun/core/agentic/action_test.py +189 -24
  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 +11 -2
  20. langfun/core/data/conversion/gemini_test.py +48 -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 +48 -44
  24. langfun/core/eval/base_test.py +5 -5
  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 +3 -0
  29. langfun/core/eval/v2/checkpointing.py +148 -46
  30. langfun/core/eval/v2/checkpointing_test.py +9 -2
  31. langfun/core/eval/v2/config_saver.py +37 -0
  32. langfun/core/eval/v2/config_saver_test.py +36 -0
  33. langfun/core/eval/v2/eval_test_helper.py +104 -3
  34. langfun/core/eval/v2/evaluation.py +102 -19
  35. langfun/core/eval/v2/evaluation_test.py +9 -3
  36. langfun/core/eval/v2/example.py +50 -40
  37. langfun/core/eval/v2/example_test.py +16 -8
  38. langfun/core/eval/v2/experiment.py +95 -20
  39. langfun/core/eval/v2/experiment_test.py +19 -0
  40. langfun/core/eval/v2/metric_values.py +31 -3
  41. langfun/core/eval/v2/metric_values_test.py +32 -0
  42. langfun/core/eval/v2/metrics.py +157 -44
  43. langfun/core/eval/v2/metrics_test.py +39 -18
  44. langfun/core/eval/v2/progress.py +31 -1
  45. langfun/core/eval/v2/progress_test.py +27 -0
  46. langfun/core/eval/v2/progress_tracking.py +13 -5
  47. langfun/core/eval/v2/progress_tracking_test.py +9 -1
  48. langfun/core/eval/v2/reporting.py +88 -71
  49. langfun/core/eval/v2/reporting_test.py +24 -6
  50. langfun/core/eval/v2/runners/__init__.py +30 -0
  51. langfun/core/eval/v2/{runners.py → runners/base.py} +73 -180
  52. langfun/core/eval/v2/runners/beam.py +354 -0
  53. langfun/core/eval/v2/runners/beam_test.py +153 -0
  54. langfun/core/eval/v2/runners/ckpt_monitor.py +350 -0
  55. langfun/core/eval/v2/runners/ckpt_monitor_test.py +213 -0
  56. langfun/core/eval/v2/runners/debug.py +40 -0
  57. langfun/core/eval/v2/runners/debug_test.py +76 -0
  58. langfun/core/eval/v2/runners/parallel.py +243 -0
  59. langfun/core/eval/v2/runners/parallel_test.py +182 -0
  60. langfun/core/eval/v2/runners/sequential.py +47 -0
  61. langfun/core/eval/v2/runners/sequential_test.py +169 -0
  62. langfun/core/langfunc.py +45 -130
  63. langfun/core/langfunc_test.py +7 -5
  64. langfun/core/language_model.py +189 -36
  65. langfun/core/language_model_test.py +54 -3
  66. langfun/core/llms/__init__.py +14 -1
  67. langfun/core/llms/anthropic.py +157 -2
  68. langfun/core/llms/azure_openai.py +29 -17
  69. langfun/core/llms/cache/base.py +25 -3
  70. langfun/core/llms/cache/in_memory.py +48 -7
  71. langfun/core/llms/cache/in_memory_test.py +14 -4
  72. langfun/core/llms/compositional.py +25 -1
  73. langfun/core/llms/deepseek.py +30 -2
  74. langfun/core/llms/fake.py +32 -1
  75. langfun/core/llms/gemini.py +90 -12
  76. langfun/core/llms/gemini_test.py +110 -0
  77. langfun/core/llms/google_genai.py +52 -1
  78. langfun/core/llms/groq.py +28 -3
  79. langfun/core/llms/llama_cpp.py +23 -4
  80. langfun/core/llms/openai.py +120 -3
  81. langfun/core/llms/openai_compatible.py +148 -27
  82. langfun/core/llms/openai_compatible_test.py +207 -20
  83. langfun/core/llms/openai_test.py +0 -2
  84. langfun/core/llms/rest.py +16 -1
  85. langfun/core/llms/vertexai.py +78 -8
  86. langfun/core/logging.py +1 -1
  87. langfun/core/mcp/__init__.py +10 -0
  88. langfun/core/mcp/client.py +177 -0
  89. langfun/core/mcp/client_test.py +71 -0
  90. langfun/core/mcp/session.py +241 -0
  91. langfun/core/mcp/session_test.py +54 -0
  92. langfun/core/mcp/testing/simple_mcp_client.py +33 -0
  93. langfun/core/mcp/testing/simple_mcp_server.py +33 -0
  94. langfun/core/mcp/tool.py +254 -0
  95. langfun/core/mcp/tool_test.py +197 -0
  96. langfun/core/memory.py +1 -0
  97. langfun/core/message.py +160 -55
  98. langfun/core/message_test.py +65 -81
  99. langfun/core/modalities/__init__.py +8 -0
  100. langfun/core/modalities/audio.py +21 -1
  101. langfun/core/modalities/image.py +73 -3
  102. langfun/core/modalities/image_test.py +116 -0
  103. langfun/core/modalities/mime.py +78 -4
  104. langfun/core/modalities/mime_test.py +59 -0
  105. langfun/core/modalities/pdf.py +19 -1
  106. langfun/core/modalities/video.py +21 -1
  107. langfun/core/modality.py +167 -29
  108. langfun/core/modality_test.py +42 -12
  109. langfun/core/natural_language.py +1 -1
  110. langfun/core/sampling.py +4 -4
  111. langfun/core/sampling_test.py +20 -4
  112. langfun/core/structured/__init__.py +2 -24
  113. langfun/core/structured/completion.py +34 -44
  114. langfun/core/structured/completion_test.py +23 -43
  115. langfun/core/structured/description.py +54 -50
  116. langfun/core/structured/function_generation.py +29 -12
  117. langfun/core/structured/mapping.py +81 -37
  118. langfun/core/structured/parsing.py +95 -79
  119. langfun/core/structured/parsing_test.py +0 -3
  120. langfun/core/structured/querying.py +230 -154
  121. langfun/core/structured/querying_test.py +69 -33
  122. langfun/core/structured/schema/__init__.py +49 -0
  123. langfun/core/structured/schema/base.py +664 -0
  124. langfun/core/structured/schema/base_test.py +531 -0
  125. langfun/core/structured/schema/json.py +174 -0
  126. langfun/core/structured/schema/json_test.py +121 -0
  127. langfun/core/structured/schema/python.py +316 -0
  128. langfun/core/structured/schema/python_test.py +410 -0
  129. langfun/core/structured/schema_generation.py +33 -14
  130. langfun/core/structured/scoring.py +47 -36
  131. langfun/core/structured/tokenization.py +26 -11
  132. langfun/core/subscription.py +2 -2
  133. langfun/core/template.py +175 -50
  134. langfun/core/template_test.py +123 -17
  135. langfun/env/__init__.py +43 -0
  136. langfun/env/base_environment.py +827 -0
  137. langfun/env/base_environment_test.py +473 -0
  138. langfun/env/base_feature.py +304 -0
  139. langfun/env/base_feature_test.py +228 -0
  140. langfun/env/base_sandbox.py +842 -0
  141. langfun/env/base_sandbox_test.py +1235 -0
  142. langfun/env/event_handlers/__init__.py +14 -0
  143. langfun/env/event_handlers/chain.py +233 -0
  144. langfun/env/event_handlers/chain_test.py +253 -0
  145. langfun/env/event_handlers/event_logger.py +472 -0
  146. langfun/env/event_handlers/event_logger_test.py +304 -0
  147. langfun/env/event_handlers/metric_writer.py +726 -0
  148. langfun/env/event_handlers/metric_writer_test.py +214 -0
  149. langfun/env/interface.py +1640 -0
  150. langfun/env/interface_test.py +153 -0
  151. langfun/env/load_balancers.py +59 -0
  152. langfun/env/load_balancers_test.py +141 -0
  153. langfun/env/test_utils.py +507 -0
  154. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/METADATA +7 -3
  155. langfun-0.1.2.dev202512150805.dist-info/RECORD +217 -0
  156. langfun/core/eval/v2/runners_test.py +0 -343
  157. langfun/core/structured/schema.py +0 -987
  158. langfun/core/structured/schema_test.py +0 -982
  159. langfun-0.1.2.dev202509120804.dist-info/RECORD +0 -172
  160. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/WHEEL +0 -0
  161. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/licenses/LICENSE +0 -0
  162. {langfun-0.1.2.dev202509120804.dist-info → langfun-0.1.2.dev202512150805.dist-info}/top_level.txt +0 -0
@@ -45,17 +45,17 @@ def evaluate(
45
45
  global_vars: An optional dict as the globals that could be referenced by the
46
46
  code.
47
47
  permission: Permission for the Python code to run.
48
- returns_stdout: If True, the stdout (a str) will be returned.
48
+ returns_stdout: If True, the stdout (a string) will be returned.
49
49
  outputs_intermediate: Applicable when returns_stdout is False. If True,
50
- intermediate output will be outputted as a dict, with the last line's
51
- value accessible by key '__result__' and the std output accessible by
50
+ intermediate output will be output as a dict, with the last line's
51
+ value accessible by key '__result__' and the stdout accessible by
52
52
  key '__stdout__'. Otherwise the value of the last line will be returned.
53
53
 
54
54
  Returns:
55
55
  The value of the last line of the code block. Or a dict of variable
56
56
  names of all locals to their evaluated values as the output of the code to
57
57
  run. The value for the last line can be accessed by key '__result__'. Or the
58
- stdout as a str.
58
+ stdout as a string.
59
59
  """
60
60
  return pg.coding.evaluate(
61
61
  parsing.clean(code),
@@ -85,28 +85,30 @@ def run(
85
85
 
86
86
  Args:
87
87
  code: Python code to run.
88
- global_vars: An optional dict of
88
+ global_vars: An optional dict as the globals that could be referenced by the
89
+ code.
89
90
  permission: Permission for the Python code to run.
90
- returns_stdout: If True, the stdout (a str) will be returned.
91
+ returns_stdout: If True, the stdout (a string) will be returned.
91
92
  outputs_intermediate: Applicable when returns_stdout is False. If True,
92
- intermediate output will be outputted as a dict, with the last line's
93
- value accessible by key '__result__' and the std output accessible by
93
+ intermediate output will be output as a dict, with the last line's
94
+ value accessible by key '__result__' and the stdout accessible by
94
95
  key '__stdout__'. Otherwise the value of the last line will be returned.
95
96
  sandbox: If True, run code in sandbox; If False, run code in current
96
97
  process. If None, run in sandbox first, if the output could not be
97
- serialized and pass to current process, run the code again in current
98
+ serialized and passed to current process, run the code again in current
98
99
  process.
99
- timeout: Execution timeout in seconds. If None, wait the code the complete.
100
+ timeout: Execution timeout in seconds. If None, wait for the code to
101
+ complete.
100
102
 
101
103
  Returns:
102
104
  The value of the last line of the code block. Or a dict of variable
103
105
  names of all locals to their evaluated values as the output of the code to
104
106
  run. The value for the last line can be accessed by key '__result__'. Or the
105
- stdout as a str.
107
+ stdout as a string.
106
108
 
107
109
  Raises:
108
110
  TimeoutError: If the execution time exceeds the timeout.
109
- Exception: Exception that are raised from the code.
111
+ Exception: Exceptions that are raised from the code.
110
112
  """
111
113
  return pg.coding.run(
112
114
  parsing.clean(code),
@@ -22,9 +22,13 @@ import pyglove as pg
22
22
 
23
23
 
24
24
  class PythonCode(pg.Object):
25
- """Symbolic class for Python code.
25
+ """Represents a piece of Python code that can be executed.
26
26
 
27
- The value of the last expression of the source will be the returned value.
27
+ When `PythonCode` is instantiated within a `PythonCode.auto_run()` context,
28
+ it automatically executes the code and returns the result of the last
29
+ expression. Otherwise, it acts as a container for the source code, which
30
+ can be executed by calling the instance. The class also supports automatic
31
+ error correction via `lf.coding.run_with_correction` when called.
28
32
  """
29
33
 
30
34
  source: Annotated[
@@ -56,7 +60,7 @@ class PythonCode(pg.Object):
56
60
  Otherwise, auto call will be disabled.
57
61
  sandbox: If True, run code in sandbox; If False, run code in current
58
62
  process. If None, run in sandbox first, if the output could not be
59
- serialized and pass to current process, run the code again in current
63
+ serialized and passed to current process, run the code again in current
60
64
  process. Applicable when `enabled` is set to True.
61
65
  timeout: Timeout in seconds. Applicable when both `enabled` and `sandbox`
62
66
  are set to True.
@@ -98,17 +102,17 @@ class PythonCode(pg.Object):
98
102
  Args:
99
103
  sandbox: If True, run code in sandbox; If False, run code in current
100
104
  process. If None, run in sandbox first, if the output could not be
101
- serialized and pass to current process, run the code again in current
105
+ serialized and passed to current process, run the code again in current
102
106
  process.
103
107
  timeout: Timeout in seconds. If None, there is no timeout. Applicable when
104
108
  sandbox is set to True.
105
109
  global_vars: Global variables that could be accessed from the source code.
106
- returns_stdout: If True, the stdout (a str) will be returned.
110
+ returns_stdout: If True, the stdout (a string) will be returned.
107
111
  outputs_intermediate: Applicable when returns_stdout is False. If True,
108
- intermediate output will be outputted as a dict, with the last line's
109
- value accessible by key '__result__' and the std output accessible by
112
+ intermediate output will be output as a dict, with the last line's
113
+ value accessible by key '__result__' and the stdout accessible by
110
114
  key '__stdout__'. Otherwise the value of the last line will be returned.
111
- autofix: Number of attempts to auto fix the generated code. If 0, autofix
115
+ autofix: Number of attempts to autofix the generated code. If 0, autofix
112
116
  is disabled.
113
117
  autofix_lm: Language model to be used. If not specified, it will try to
114
118
  use the `lm` under `lf.context`.
@@ -117,8 +121,8 @@ class PythonCode(pg.Object):
117
121
  The value of the last expression in the source code. Or a dict of local
118
122
  variable names defined in the source code to their values if
119
123
  `outputs_intermediate` is set to True. The value for the last line can be
120
- accessed by key '__result__'. Or the stdout as a str if `returns_stdout`
121
- is set to True.
124
+ accessed by key '__result__'. Or the stdout as a string if
125
+ `returns_stdout` is set to True.
122
126
 
123
127
  Raises:
124
128
  TimeoutError: If `sandbox` is True and timeout has reached.
@@ -152,12 +156,12 @@ class PythonCode(pg.Object):
152
156
  Args:
153
157
  sandbox: If True, run code in sandbox; If False, run code in current
154
158
  process. If None, run in sandbox first, if the output could not be
155
- serialized and pass to current process, run the code again in current
159
+ serialized and passed to current process, run the code again in current
156
160
  process.
157
161
  timeout: Timeout in seconds. If None, there is no timeout. Applicable when
158
162
  sandbox is set to True.
159
163
  global_vars: Global variables that could be accessed from the source code.
160
- autofix: Number of attempts to auto fix the generated code. If 0, autofix
164
+ autofix: Number of attempts to autofix the generated code. If 0, autofix
161
165
  is disabled. Auto-fix is not supported for 'json' protocol.
162
166
  autofix_lm: Language model to be used. If not specified, it will try to
163
167
  use the `lm` under `lf.context`.
@@ -182,10 +186,11 @@ class PythonCode(pg.Object):
182
186
 
183
187
 
184
188
  class PythonFunction(pg.Object):
185
- """Generated Python function via source code.
189
+ """Represents a Python function defined by source code.
186
190
 
187
- The source code will be directly passed into eval() for execution and the
188
- output of the function will be returned.
191
+ This class takes Python source code that defines a function and makes it
192
+ callable. The source code is evaluated to create a function object, which
193
+ can then be invoked like a regular Python function.
189
194
  """
190
195
 
191
196
  name: str
@@ -214,7 +219,7 @@ class PythonFunction(pg.Object):
214
219
  *args: Positional arguments that will be passed to the implementation.
215
220
  sandbox: If True, run code in sandbox; If False, run code in current
216
221
  process. If None, run in sandbox first, if the output could not be
217
- serialized and pass to current process, run the code again in current
222
+ serialized and passed to current process, run the code again in current
218
223
  process.
219
224
  timeout: Timeout in seconds. If None, there is no timeout. Applicable when
220
225
  sandbox is set to True.
@@ -23,7 +23,14 @@ import pyglove as pg
23
23
 
24
24
 
25
25
  class SandboxOutput(pg.Object):
26
- """Sandbox output."""
26
+ """A structure containing the output from a sandbox execution.
27
+
28
+ Attributes:
29
+ stdout: The standard output captured during execution.
30
+ stderr: The standard error captured during execution.
31
+ output_files: A dictionary of file names to their byte content for files
32
+ generated during execution.
33
+ """
27
34
 
28
35
  stdout: Annotated[
29
36
  str,
@@ -42,7 +49,14 @@ class SandboxOutput(pg.Object):
42
49
 
43
50
 
44
51
  class BaseSandbox(pg.Object):
45
- """Interface and partial implementation for Python sandbox."""
52
+ """Base class for Python code sandboxing.
53
+
54
+ A sandbox provides an isolated environment for executing Python code,
55
+ typically with restrictions on file system access, network calls, or other
56
+ potentially harmful operations. This base class defines the interface for
57
+ sandboxes, including methods for running code (`run`), uploading files
58
+ (`upload`), and managing the sandbox lifecycle (`setup`, `cleanup`).
59
+ """
46
60
 
47
61
  def _on_bound(self):
48
62
  super()._on_bound()
@@ -111,7 +125,13 @@ class BaseSandbox(pg.Object):
111
125
 
112
126
 
113
127
  class MultiProcessingSandbox(BaseSandbox):
114
- """Sandbox using multiprocessing."""
128
+ """A sandbox implementation using Python's `multiprocessing`.
129
+
130
+ This sandbox executes code in a separate process, providing isolation from
131
+ the main process. It uses a temporary directory for file operations,
132
+ which is cleaned up when the sandbox is closed. It relies on
133
+ `pg.coding.run` with `sandbox=True` for execution.
134
+ """
115
135
 
116
136
  def _on_bound(self):
117
137
  super()._on_bound()
langfun/core/component.py CHANGED
@@ -11,7 +11,7 @@
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
- """langfun Component."""
14
+ """Base component for Langfun."""
15
15
 
16
16
  from typing import ContextManager
17
17
  import pyglove as pg
@@ -22,7 +22,37 @@ RAISE_IF_HAS_ERROR = (pg.MISSING_VALUE,)
22
22
 
23
23
 
24
24
  class Component(pg.ContextualObject):
25
- """Base class for langfun components."""
25
+ """Base class for Langfun components.
26
+
27
+ Langfun components are context-aware symbolic objects powered by PyGlove.
28
+ (See [PyGlove basics](https://pyglove.readthedocs.io/en/latest/basics.html)
29
+ for more details).
30
+
31
+ **Context-awareness**
32
+
33
+ Langfun components can have contextual attributes using `lf.contextual`,
34
+ whose values can be provided or overridden via `lf.context` or
35
+ `lf.use_settings`.
36
+
37
+ Example:
38
+ ```python
39
+ import langfun as lf
40
+
41
+ class Bar(lf.Component):
42
+ y = lf.contextual(1)
43
+
44
+ class Foo(lf.Component):
45
+ x = lf.contextual(0)
46
+ bar = Bar()
47
+
48
+ f = Foo()
49
+ assert f.x == 0 and f.bar.y == 1
50
+
51
+ # `lf.context` overrides `lf.contextual` attributes.
52
+ with lf.context(x=10, y=20):
53
+ assert f.x == 10 and f.bar.y == 20
54
+ ```
55
+ """
26
56
 
27
57
  # Allow symbolic assignment, which invalidates the object and recomputes
28
58
  # states upon update.
@@ -78,6 +108,15 @@ def use_settings(
78
108
  ) -> ContextManager[dict[str, pg.utils.ContextualOverride]]:
79
109
  """Shortcut method for overriding component attributes.
80
110
 
111
+ Example:
112
+
113
+ ```
114
+ with lf.use_settings(
115
+ lm=lf.llms.Gpt35(),
116
+ temperature=0.0):
117
+ lf.query('who are you?')
118
+ ```
119
+
81
120
  Args:
82
121
  cascade: If True, this override will apply to both current scope and nested
83
122
  scope, meaning that this `lf.context` will take precedence over all
@@ -85,6 +124,6 @@ def use_settings(
85
124
  **settings: Key/values as override for component attributes.
86
125
 
87
126
  Returns:
88
- A dict of attribute names to their contextual overrides.
127
+ A context manager for overriding settings.
89
128
  """
90
129
  return context(cascade=cascade, override_attrs=True, **settings)
@@ -11,7 +11,7 @@
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
- """Utility library for handling concurrency in langfun."""
14
+ """Utilities for concurrency in Langfun."""
15
15
 
16
16
  import abc
17
17
  import collections
@@ -97,7 +97,7 @@ class RetryError(RuntimeError):
97
97
 
98
98
 
99
99
  def with_retry(
100
- func: Callable[[Any], Any],
100
+ func: Callable[..., Any],
101
101
  retry_on_errors: Union[
102
102
  Union[Type[BaseException], Tuple[Type[BaseException], str]],
103
103
  Sequence[Union[Type[BaseException], Tuple[Type[BaseException], str]]],
@@ -108,10 +108,25 @@ def with_retry(
108
108
  max_retry_interval: int = 300,
109
109
  seed: int | None = None,
110
110
  ) -> Callable[..., Any]:
111
- """Derives a user function with retry on error.
111
+ """Decorator-like function to add retry mechanism to a function.
112
+
113
+ Example:
114
+
115
+ ```
116
+ def flaky_function():
117
+ if random.random() < 0.5:
118
+ raise ValueError('error')
119
+ return 1
120
+
121
+ reliable_function = lf.with_retry(
122
+ flaky_function,
123
+ retry_on_errors=ValueError,
124
+ max_attempts=3)
125
+ reliable_function()
126
+ ```
112
127
 
113
128
  Args:
114
- func: A user function.
129
+ func: The function to add retry mechanism.
115
130
  retry_on_errors: A sequence of exception types or tuples of exception type
116
131
  and error messages (described in regular expression) as the desired
117
132
  exception types to retry.
@@ -128,8 +143,7 @@ def with_retry(
128
143
  determined based on current time.
129
144
 
130
145
  Returns:
131
- A function with the same signature of the input function, with the retry
132
- capability.
146
+ A function with the same signature of `func`, but with retry capability.
133
147
  """
134
148
 
135
149
  def _func(*args, **kwargs):
@@ -179,6 +193,24 @@ def concurrent_execute(
179
193
  ) -> list[Any]:
180
194
  """Executes a function concurrently under current component context.
181
195
 
196
+ `lf.concurrent_execute` applies a function to each item in an iterable of
197
+ inputs in parallel and returns a list of results in the same order as the
198
+ inputs. It is a convenient wrapper around `lf.concurrent_map` for synchronous
199
+ bulk processing.
200
+
201
+ **Example:**
202
+
203
+ ```python
204
+ import langfun as lf
205
+
206
+ def square(x):
207
+ return x ** 2
208
+
209
+ results = lf.concurrent_execute(square, [1, 2, 3, 4], max_workers=2)
210
+ print(results)
211
+ # Output: [1, 4, 9, 16]
212
+ ```
213
+
182
214
  Args:
183
215
  func: A user function.
184
216
  parallel_inputs: The inputs for `func` which will be processed in parallel.
@@ -649,6 +681,38 @@ def concurrent_map(
649
681
  ) -> Iterator[Any]:
650
682
  """Maps inputs to outptus via func concurrently under current context.
651
683
 
684
+ `lf.concurrent_map` applies a function to each item in an iterable of
685
+ inputs in parallel and yields `(input, output, error)` tuples as they are
686
+ completed. It supports features like ordered/unordered results, progress
687
+ bars, timeouts, and automatic retries for transient errors.
688
+
689
+ **Example:**
690
+
691
+ ```python
692
+ import langfun as lf
693
+ import time
694
+ import random
695
+
696
+ def flaky_square(x):
697
+ time.sleep(random.random())
698
+ if random.random() < 0.3:
699
+ raise ValueError("Flaky error")
700
+ return x ** 2
701
+
702
+ # Unordered execution with progress bar and retries
703
+ for input, output, error in lf.concurrent_map(
704
+ flaky_square,
705
+ range(10),
706
+ max_workers=3,
707
+ show_progress=True,
708
+ retry_on_errors=ValueError,
709
+ max_attempts=3):
710
+ if error:
711
+ print(f"Input {input} failed with error: {error}")
712
+ else:
713
+ print(f"Input {input} succeeded with output: {output}")
714
+ ```
715
+
652
716
  Args:
653
717
  func: A user function.
654
718
  parallel_inputs: The inputs for `func` which will be processed in parallel.
@@ -262,6 +262,7 @@ class ProgressControlTest(unittest.TestCase):
262
262
  with contextlib.redirect_stderr(string_io):
263
263
  ctrl.update(1)
264
264
  ctrl.refresh()
265
+ sys.stderr.flush()
265
266
  self.assertEqual(string_io.getvalue(), '')
266
267
  concurrent.progress_bar = 'tqdm'
267
268
 
@@ -274,6 +275,7 @@ class ProgressControlTest(unittest.TestCase):
274
275
  ctrl.set_status('bar')
275
276
  ctrl.update(10)
276
277
  ctrl.refresh()
278
+ sys.stderr.flush()
277
279
  self.assertEqual(
278
280
  string_io.getvalue(),
279
281
  '\x1b[1m\x1b[31mfoo\x1b[0m: \x1b[34m10% (10/100)\x1b[0m : bar\n'
@@ -288,6 +290,7 @@ class ProgressControlTest(unittest.TestCase):
288
290
  self.assertIsInstance(ctrl, concurrent._TqdmProgressControl)
289
291
  ctrl.update(10)
290
292
  ctrl.refresh()
293
+ sys.stderr.flush()
291
294
  self.assertIn('10/100', string_io.getvalue())
292
295
 
293
296
  tqdm = concurrent.tqdm
@@ -316,6 +319,7 @@ class ProgressBarTest(unittest.TestCase):
316
319
  for _ in concurrent.concurrent_execute(fun, range(5)):
317
320
  concurrent.ProgressBar.refresh()
318
321
  concurrent.ProgressBar.uninstall(bar_id)
322
+ sys.stderr.flush()
319
323
  output_str = string_io.getvalue()
320
324
  self.assertIn('100%', output_str)
321
325
  self.assertIn('5/5', output_str)
@@ -332,7 +336,7 @@ class ProgressBarTest(unittest.TestCase):
332
336
  concurrent.ProgressBar.update(bar_id, 0, status=1)
333
337
  concurrent.ProgressBar.uninstall(bar_id)
334
338
  sys.stderr.flush()
335
- time.sleep(1)
339
+ time.sleep(1)
336
340
  self.assertIn('1/4', string_io.getvalue())
337
341
  # TODO(daiyip): Re-enable once flakiness is fixed.
338
342
  # self.assertIn('2/4', string_io.getvalue())
@@ -564,7 +568,8 @@ class ConcurrentMapTest(unittest.TestCase):
564
568
  fun, [1, 2, 3], timeout=1.5, max_workers=1, show_progress=True
565
569
  )
566
570
  ], key=lambda x: x[0])
567
- string_io.flush()
571
+ sys.stderr.flush()
572
+
568
573
  self.assertEqual( # pylint: disable=g-generic-assert
569
574
  output,
570
575
  [
@@ -592,6 +597,7 @@ class ConcurrentMapTest(unittest.TestCase):
592
597
  show_progress=bar_id, status_fn=lambda p: dict(x=1, y=1)
593
598
  )
594
599
  ], key=lambda x: x[0])
600
+ sys.stderr.flush()
595
601
 
596
602
  self.assertEqual( # pylint: disable=g-generic-assert
597
603
  output,
@@ -602,6 +608,7 @@ class ConcurrentMapTest(unittest.TestCase):
602
608
  ],
603
609
  )
604
610
  concurrent.ProgressBar.uninstall(bar_id)
611
+ concurrent.ProgressBar.refresh()
605
612
  self.assertIn('100%', string_io.getvalue())
606
613
 
607
614
 
langfun/core/console.py CHANGED
@@ -11,7 +11,7 @@
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
- """Console utilities."""
14
+ """Utilities for console output and notebook display."""
15
15
 
16
16
  import sys
17
17
  from typing import Any
@@ -21,7 +21,14 @@ from langfun.core import modalities as lf_modalities
21
21
 
22
22
 
23
23
  class AnthropicMessageConverter(lf.MessageConverter):
24
- """Converter to Anthropic public API."""
24
+ """Converter between Langfun messages and Anthropic API message format.
25
+
26
+ This converter translates `lf.Message` objects into the JSON format required
27
+ by the Anthropic API and vice versa. It handles text and modalities like
28
+ images and PDFs by encoding them in base64 format as expected by Anthropic.
29
+ An optional `chunk_preprocessor` can be provided to modify or filter
30
+ chunks before conversion.
31
+ """
25
32
 
26
33
  FORMAT_ID = 'anthropic'
27
34
 
@@ -30,12 +37,12 @@ class AnthropicMessageConverter(lf.MessageConverter):
30
37
  (
31
38
  'Chunk preprocessor for Langfun chunk to Anthropic chunk conversion. '
32
39
  'It will be applied before each Langfun chunk is converted. '
33
- 'If returns None, the chunk will be skipped.'
40
+ 'If it returns None, the chunk will be skipped.'
34
41
  )
35
42
  ] = None
36
43
 
37
44
  def to_value(self, message: lf.Message) -> dict[str, Any]:
38
- """Converts a Langfun message to Gemini API."""
45
+ """Converts a Langfun message to Anthropic API."""
39
46
  content = []
40
47
  for chunk in message.chunk():
41
48
  if self.chunk_preprocessor:
@@ -97,6 +104,8 @@ class AnthropicMessageConverter(lf.MessageConverter):
97
104
  self._safe_read(source, 'media_type')
98
105
  ).from_bytes(base64.b64decode(self._safe_read(source, 'data')))
99
106
  )
107
+ elif t in ('server_tool_use', 'web_search_tool_result'):
108
+ continue
100
109
  else:
101
110
  raise ValueError(f'Unsupported content part: {part!r}.')
102
111
  message = message_cls.from_chunks(chunks)
@@ -253,14 +253,16 @@ class AnthropicConversionTest(unittest.TestCase):
253
253
  )
254
254
  self.assertEqual(
255
255
  m.text,
256
- 'What are the common words from <<[[obj0]]>> and <<[[obj1]]>> ?'
256
+ 'What are the common words from <<[[image:dc6e1e43]]>> and'
257
+ ' <<[[pdf:5daf5f31]]>> ?'
257
258
  )
258
- self.assertIsInstance(m.obj0, lf_modalities.Image)
259
- self.assertEqual(m.obj0.mime_type, 'image/png')
260
- self.assertEqual(m.obj0.to_bytes(), image_content)
259
+ modalities = m.modalities()
260
+ self.assertIsInstance(modalities[0], lf_modalities.Image)
261
+ self.assertEqual(modalities[0].mime_type, 'image/png')
262
+ self.assertEqual(modalities[0].content, image_content)
261
263
 
262
- self.assertIsInstance(m.obj1, lf_modalities.PDF)
263
- self.assertEqual(m.obj1.to_bytes(), pdf_content)
264
+ self.assertIsInstance(modalities[1], lf_modalities.PDF)
265
+ self.assertEqual(modalities[1].content, pdf_content)
264
266
 
265
267
 
266
268
  if __name__ == '__main__':
@@ -21,7 +21,14 @@ from langfun.core import modalities as lf_modalities
21
21
 
22
22
 
23
23
  class GeminiMessageConverter(lf.MessageConverter):
24
- """Converter to Gemini public API."""
24
+ """Converter between Langfun messages and Gemini API message format.
25
+
26
+ This converter translates `lf.Message` objects into the JSON format required
27
+ by the public Gemini API (e.g., via Vertex AI or Google AI Studio) and
28
+ vice versa. It handles text and modalities like images, extracting thought
29
+ chunks if present. An optional `chunk_preprocessor` can be provided to
30
+ modify or filter chunks before conversion.
31
+ """
25
32
 
26
33
  FORMAT_ID = 'gemini'
27
34
 
@@ -30,7 +37,7 @@ class GeminiMessageConverter(lf.MessageConverter):
30
37
  (
31
38
  'Chunk preprocessor for Langfun chunk to Gemini chunk conversion. '
32
39
  'It will be applied before each Langfun chunk is converted. '
33
- 'If returns None, the chunk will be skipped.'
40
+ 'If it returns None, the chunk will be skipped.'
34
41
  ),
35
42
  ] = None
36
43
 
@@ -131,6 +138,8 @@ class GeminiMessageConverter(lf.MessageConverter):
131
138
  self._safe_read(data, 'mimeType')
132
139
  ).from_uri(self._safe_read(data, 'fileUri'))
133
140
  )
141
+ elif 'functionCall' in part or 'functionResponse' in part:
142
+ pass
134
143
  else:
135
144
  raise ValueError(f'Unsupported content part: {part!r}.')
136
145
  message = message_cls.from_chunks(chunks)
@@ -225,19 +225,58 @@ class GeminiConversionTest(unittest.TestCase):
225
225
  self.assertEqual(
226
226
  m.text,
227
227
  (
228
- 'What are the common words from <<[[obj0]]>> , <<[[obj1]]>> '
229
- 'and <<[[obj2]]>> ?'
228
+ 'What are the common words from <<[[image:dc6e1e43]]>> , '
229
+ '<<[[pdf:4dc12e93]]>> and <<[[video:7e169565]]>> ?'
230
230
  )
231
231
  )
232
- self.assertIsInstance(m.obj0, lf_modalities.Image)
233
- self.assertEqual(m.obj0.mime_type, 'image/png')
234
- self.assertEqual(m.obj0.to_bytes(), image_content)
232
+ self.assertIsInstance(m.modalities()[0], lf_modalities.Image)
233
+ self.assertEqual(m.modalities()[0].mime_type, 'image/png')
234
+ self.assertEqual(m.modalities()[0].to_bytes(), image_content)
235
235
 
236
- self.assertIsInstance(m.obj1, lf_modalities.PDF)
237
- self.assertEqual(m.obj1.uri, 'https://my.pdf')
236
+ self.assertIsInstance(m.modalities()[1], lf_modalities.PDF)
237
+ self.assertEqual(m.modalities()[1].uri, 'https://my.pdf')
238
238
 
239
- self.assertIsInstance(m.obj2, lf_modalities.Video)
240
- self.assertEqual(m.obj2.uri, 'https://www.youtube.com/watch?v=abcd')
239
+ self.assertIsInstance(m.modalities()[2], lf_modalities.Video)
240
+ self.assertEqual(
241
+ m.modalities()[2].uri,
242
+ 'https://www.youtube.com/watch?v=abcd'
243
+ )
244
+
245
+ def test_from_value_with_function_call(self):
246
+ message = lf.Message.from_value(
247
+ {
248
+ 'role': 'model',
249
+ 'parts': [
250
+ {'text': 'Let me search for that.'},
251
+ {
252
+ 'functionCall': {
253
+ 'name': 'search',
254
+ 'args': {'query': 'test'},
255
+ }
256
+ },
257
+ ],
258
+ },
259
+ format='gemini',
260
+ )
261
+ self.assertEqual(message.text, 'Let me search for that.')
262
+
263
+ def test_from_value_with_function_response(self):
264
+ message = lf.Message.from_value(
265
+ {
266
+ 'role': 'user',
267
+ 'parts': [
268
+ {
269
+ 'functionResponse': {
270
+ 'name': 'search',
271
+ 'response': {'results': ['a', 'b']},
272
+ }
273
+ },
274
+ {'text': 'Here are the results.'},
275
+ ],
276
+ },
277
+ format='gemini',
278
+ )
279
+ self.assertEqual(message.text, 'Here are the results.')
241
280
 
242
281
 
243
282
  if __name__ == '__main__':