langfun 0.1.2.dev202510230805__py3-none-any.whl → 0.1.2.dev202511270805__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 (155) hide show
  1. langfun/core/__init__.py +2 -0
  2. langfun/core/agentic/__init__.py +4 -1
  3. langfun/core/agentic/action.py +447 -29
  4. langfun/core/agentic/action_eval.py +9 -2
  5. langfun/core/agentic/action_test.py +149 -21
  6. langfun/core/async_support.py +32 -3
  7. langfun/core/coding/python/correction.py +19 -9
  8. langfun/core/coding/python/execution.py +14 -12
  9. langfun/core/coding/python/generation.py +21 -16
  10. langfun/core/coding/python/sandboxing.py +23 -3
  11. langfun/core/component.py +42 -3
  12. langfun/core/concurrent.py +70 -6
  13. langfun/core/concurrent_test.py +1 -0
  14. langfun/core/console.py +1 -1
  15. langfun/core/data/conversion/anthropic.py +12 -3
  16. langfun/core/data/conversion/anthropic_test.py +8 -6
  17. langfun/core/data/conversion/gemini.py +9 -2
  18. langfun/core/data/conversion/gemini_test.py +12 -9
  19. langfun/core/data/conversion/openai.py +145 -31
  20. langfun/core/data/conversion/openai_test.py +161 -17
  21. langfun/core/eval/base.py +47 -43
  22. langfun/core/eval/base_test.py +5 -5
  23. langfun/core/eval/matching.py +5 -2
  24. langfun/core/eval/patching.py +3 -3
  25. langfun/core/eval/scoring.py +4 -3
  26. langfun/core/eval/v2/__init__.py +1 -0
  27. langfun/core/eval/v2/checkpointing.py +64 -6
  28. langfun/core/eval/v2/checkpointing_test.py +9 -2
  29. langfun/core/eval/v2/eval_test_helper.py +103 -2
  30. langfun/core/eval/v2/evaluation.py +91 -16
  31. langfun/core/eval/v2/evaluation_test.py +9 -3
  32. langfun/core/eval/v2/example.py +50 -40
  33. langfun/core/eval/v2/example_test.py +16 -8
  34. langfun/core/eval/v2/experiment.py +74 -8
  35. langfun/core/eval/v2/experiment_test.py +19 -0
  36. langfun/core/eval/v2/metric_values.py +31 -3
  37. langfun/core/eval/v2/metric_values_test.py +32 -0
  38. langfun/core/eval/v2/metrics.py +157 -44
  39. langfun/core/eval/v2/metrics_test.py +39 -18
  40. langfun/core/eval/v2/progress.py +30 -1
  41. langfun/core/eval/v2/progress_test.py +27 -0
  42. langfun/core/eval/v2/progress_tracking.py +12 -3
  43. langfun/core/eval/v2/progress_tracking_test.py +6 -1
  44. langfun/core/eval/v2/reporting.py +90 -71
  45. langfun/core/eval/v2/reporting_test.py +24 -6
  46. langfun/core/eval/v2/runners/__init__.py +30 -0
  47. langfun/core/eval/v2/{runners.py → runners/base.py} +59 -142
  48. langfun/core/eval/v2/runners/beam.py +341 -0
  49. langfun/core/eval/v2/runners/beam_test.py +131 -0
  50. langfun/core/eval/v2/runners/ckpt_monitor.py +294 -0
  51. langfun/core/eval/v2/runners/ckpt_monitor_test.py +162 -0
  52. langfun/core/eval/v2/runners/debug.py +40 -0
  53. langfun/core/eval/v2/runners/debug_test.py +76 -0
  54. langfun/core/eval/v2/runners/parallel.py +100 -0
  55. langfun/core/eval/v2/runners/parallel_test.py +95 -0
  56. langfun/core/eval/v2/runners/sequential.py +47 -0
  57. langfun/core/eval/v2/runners/sequential_test.py +172 -0
  58. langfun/core/langfunc.py +45 -130
  59. langfun/core/langfunc_test.py +7 -5
  60. langfun/core/language_model.py +141 -21
  61. langfun/core/language_model_test.py +54 -3
  62. langfun/core/llms/__init__.py +9 -1
  63. langfun/core/llms/anthropic.py +157 -2
  64. langfun/core/llms/azure_openai.py +29 -17
  65. langfun/core/llms/cache/base.py +25 -3
  66. langfun/core/llms/cache/in_memory.py +48 -7
  67. langfun/core/llms/cache/in_memory_test.py +14 -4
  68. langfun/core/llms/compositional.py +25 -1
  69. langfun/core/llms/deepseek.py +30 -2
  70. langfun/core/llms/fake.py +32 -1
  71. langfun/core/llms/gemini.py +55 -17
  72. langfun/core/llms/gemini_test.py +84 -0
  73. langfun/core/llms/google_genai.py +34 -1
  74. langfun/core/llms/groq.py +28 -3
  75. langfun/core/llms/llama_cpp.py +23 -4
  76. langfun/core/llms/openai.py +36 -3
  77. langfun/core/llms/openai_compatible.py +148 -27
  78. langfun/core/llms/openai_compatible_test.py +207 -20
  79. langfun/core/llms/openai_test.py +0 -2
  80. langfun/core/llms/rest.py +12 -1
  81. langfun/core/llms/vertexai.py +58 -8
  82. langfun/core/logging.py +1 -1
  83. langfun/core/mcp/client.py +77 -22
  84. langfun/core/mcp/client_test.py +8 -35
  85. langfun/core/mcp/session.py +94 -29
  86. langfun/core/mcp/session_test.py +54 -0
  87. langfun/core/mcp/tool.py +151 -22
  88. langfun/core/mcp/tool_test.py +197 -0
  89. langfun/core/memory.py +1 -0
  90. langfun/core/message.py +160 -55
  91. langfun/core/message_test.py +65 -81
  92. langfun/core/modalities/__init__.py +8 -0
  93. langfun/core/modalities/audio.py +21 -1
  94. langfun/core/modalities/image.py +19 -1
  95. langfun/core/modalities/mime.py +64 -3
  96. langfun/core/modalities/mime_test.py +11 -0
  97. langfun/core/modalities/pdf.py +19 -1
  98. langfun/core/modalities/video.py +21 -1
  99. langfun/core/modality.py +167 -29
  100. langfun/core/modality_test.py +42 -12
  101. langfun/core/natural_language.py +1 -1
  102. langfun/core/sampling.py +4 -4
  103. langfun/core/sampling_test.py +20 -4
  104. langfun/core/structured/__init__.py +2 -24
  105. langfun/core/structured/completion.py +34 -44
  106. langfun/core/structured/completion_test.py +23 -43
  107. langfun/core/structured/description.py +54 -50
  108. langfun/core/structured/function_generation.py +29 -12
  109. langfun/core/structured/mapping.py +81 -37
  110. langfun/core/structured/parsing.py +95 -79
  111. langfun/core/structured/parsing_test.py +0 -3
  112. langfun/core/structured/querying.py +215 -142
  113. langfun/core/structured/querying_test.py +65 -29
  114. langfun/core/structured/schema/__init__.py +49 -0
  115. langfun/core/structured/schema/base.py +664 -0
  116. langfun/core/structured/schema/base_test.py +531 -0
  117. langfun/core/structured/schema/json.py +174 -0
  118. langfun/core/structured/schema/json_test.py +121 -0
  119. langfun/core/structured/schema/python.py +316 -0
  120. langfun/core/structured/schema/python_test.py +410 -0
  121. langfun/core/structured/schema_generation.py +33 -14
  122. langfun/core/structured/scoring.py +47 -36
  123. langfun/core/structured/tokenization.py +26 -11
  124. langfun/core/subscription.py +2 -2
  125. langfun/core/template.py +174 -49
  126. langfun/core/template_test.py +123 -17
  127. langfun/env/__init__.py +8 -2
  128. langfun/env/base_environment.py +320 -128
  129. langfun/env/base_environment_test.py +473 -0
  130. langfun/env/base_feature.py +92 -15
  131. langfun/env/base_feature_test.py +228 -0
  132. langfun/env/base_sandbox.py +84 -361
  133. langfun/env/base_sandbox_test.py +1235 -0
  134. langfun/env/event_handlers/__init__.py +1 -1
  135. langfun/env/event_handlers/chain.py +233 -0
  136. langfun/env/event_handlers/chain_test.py +253 -0
  137. langfun/env/event_handlers/event_logger.py +95 -98
  138. langfun/env/event_handlers/event_logger_test.py +21 -21
  139. langfun/env/event_handlers/metric_writer.py +225 -140
  140. langfun/env/event_handlers/metric_writer_test.py +23 -6
  141. langfun/env/interface.py +854 -40
  142. langfun/env/interface_test.py +112 -2
  143. langfun/env/load_balancers_test.py +23 -2
  144. langfun/env/test_utils.py +126 -84
  145. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/METADATA +1 -1
  146. langfun-0.1.2.dev202511270805.dist-info/RECORD +215 -0
  147. langfun/core/eval/v2/runners_test.py +0 -343
  148. langfun/core/structured/schema.py +0 -987
  149. langfun/core/structured/schema_test.py +0 -982
  150. langfun/env/base_test.py +0 -1481
  151. langfun/env/event_handlers/base.py +0 -350
  152. langfun-0.1.2.dev202510230805.dist-info/RECORD +0 -195
  153. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/WHEEL +0 -0
  154. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/licenses/LICENSE +0 -0
  155. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511270805.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,95 @@
1
+ # Copyright 2024 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
+ """Tests for parallel runner."""
15
+ import os
16
+ import tempfile
17
+ from typing import Any
18
+ import unittest
19
+
20
+ from langfun.core.eval.v2 import eval_test_helper
21
+ from langfun.core.eval.v2.runners import parallel # pylint: disable=unused-import
22
+
23
+ import pyglove as pg
24
+
25
+
26
+ class ParallelRunnerTest(unittest.TestCase):
27
+
28
+ def assert_same_list(self, actual: list[Any], expected: list[Any]):
29
+ self.assertEqual(len(actual), len(expected))
30
+ for i, (x, y) in enumerate(zip(actual, expected)):
31
+ if x is not y:
32
+ print(i, pg.diff(x, y))
33
+ self.assertIs(x, y)
34
+
35
+ def test_parallel_runner(self):
36
+ plugin = eval_test_helper.TestPlugin()
37
+ exp = eval_test_helper.test_experiment()
38
+ root_dir = os.path.join(tempfile.mkdtemp(), 'test_parallel_runner')
39
+ run = exp.run(root_dir, runner='parallel', plugins=[plugin])
40
+
41
+ self.assertIsNotNone(plugin.start_time)
42
+ self.assertIsNotNone(plugin.complete_time)
43
+ self.assertGreater(plugin.complete_time, plugin.start_time)
44
+
45
+ self.assertEqual(
46
+ len(plugin.started_experiments), len(exp.nodes)
47
+ )
48
+ self.assertEqual(
49
+ len(plugin.completed_experiments), len(exp.nodes)
50
+ )
51
+ self.assertEqual(
52
+ len(plugin.started_example_ids), 6 * 10
53
+ )
54
+ self.assertEqual(
55
+ len(plugin.completed_example_ids), 6 * 10
56
+ )
57
+ self.assert_same_list(plugin.skipped_experiments, [])
58
+ self.assertTrue(
59
+ pg.io.path_exists(os.path.join(run.output_root, 'run.json'))
60
+ )
61
+
62
+ for node in exp.nodes:
63
+ self.assertTrue(node.progress.is_started)
64
+ self.assertTrue(node.progress.is_completed)
65
+ if node.is_leaf:
66
+ self.assertEqual(node.progress.num_skipped, 0)
67
+ self.assertEqual(node.progress.num_completed, 10)
68
+ self.assertEqual(node.progress.num_failed, 1)
69
+ else:
70
+ self.assertEqual(node.progress.num_skipped, 0)
71
+ self.assertEqual(node.progress.num_failed, 0)
72
+ self.assertEqual(node.progress.num_processed, node.progress.num_total)
73
+
74
+ def test_raise_if_has_error(self):
75
+ root_dir = os.path.join(tempfile.mkdtemp(), 'test_raise_if_has_error')
76
+ exp = eval_test_helper.TestEvaluation()
77
+ with self.assertRaisesRegex(ValueError, 'x should not be 5'):
78
+ exp.run(root_dir, runner='parallel', plugins=[], raise_if_has_error=True)
79
+
80
+ def test_concurrent_startup_delay(self):
81
+ plugin = eval_test_helper.TestPlugin()
82
+ exp = eval_test_helper.test_experiment()
83
+ root_dir = os.path.join(
84
+ tempfile.mkdtemp(), 'test_concurrent_startup_delay'
85
+ )
86
+ _ = exp.run(
87
+ root_dir,
88
+ runner='parallel',
89
+ plugins=[plugin],
90
+ concurrent_startup_delay=(0, 5),
91
+ )
92
+
93
+
94
+ if __name__ == '__main__':
95
+ unittest.main()
@@ -0,0 +1,47 @@
1
+ # Copyright 2024 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
+ """Sequential runner."""
15
+
16
+ from typing import Any, Callable, Iterator
17
+ from langfun.core.eval.v2.runners import base
18
+
19
+
20
+ class SequentialRunner(base.RunnerBase):
21
+ """A runner that executes evaluations and examples sequentially.
22
+
23
+ The sequential runner executes all evaluations and their examples in the
24
+ calling thread. Background tasks are also run sequentially, which makes it
25
+ easier to debug as exceptions from background tasks will be raised
26
+ immediately.
27
+ """
28
+
29
+ NAME = 'sequential'
30
+
31
+ def background_run(
32
+ self, func: Callable[..., Any], *args: Any, **kwargs: Any
33
+ ) -> None:
34
+ """Runs the function with the IO pool."""
35
+ func(*args, **kwargs)
36
+
37
+ def _run(self, evaluations: list[base.Evaluation]) -> None:
38
+ """Runs the experiment in sequence."""
39
+ for e in evaluations:
40
+ self.run_evaluation(e)
41
+
42
+ def _evaluate_items(
43
+ self, evaluation: base.Evaluation, items: Iterator[base.Example]
44
+ ) -> None:
45
+ """Runs the evaluation items in sequence."""
46
+ for item in items:
47
+ self.evaluate_item(evaluation, item)
@@ -0,0 +1,172 @@
1
+ # Copyright 2024 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
+ """Tests for sequential runner."""
15
+ import os
16
+ import tempfile
17
+ from typing import Any
18
+ import unittest
19
+
20
+ from langfun.core.eval.v2 import eval_test_helper
21
+ from langfun.core.eval.v2.runners import sequential # pylint: disable=unused-import
22
+
23
+ import pyglove as pg
24
+
25
+
26
+ class SequentialRunnerTest(unittest.TestCase):
27
+
28
+ def assert_same_list(self, actual: list[Any], expected: list[Any]):
29
+ self.assertEqual(len(actual), len(expected))
30
+ for i, (x, y) in enumerate(zip(actual, expected)):
31
+ if x is not y:
32
+ print(i, pg.diff(x, y))
33
+ self.assertIs(x, y)
34
+
35
+ def test_basic(self):
36
+ plugin = eval_test_helper.TestPlugin()
37
+ exp = eval_test_helper.test_experiment()
38
+ root_dir = os.path.join(tempfile.mkdtemp(), 'test_sequential_runner')
39
+ run = exp.run(root_dir, runner='sequential', plugins=[plugin])
40
+
41
+ self.assertIsNotNone(plugin.start_time)
42
+ self.assertIsNotNone(plugin.complete_time)
43
+ self.assertGreater(plugin.complete_time, plugin.start_time)
44
+
45
+ self.assert_same_list(
46
+ plugin.started_experiments,
47
+ exp.nonleaf_nodes + exp.leaf_nodes
48
+ )
49
+ self.assert_same_list(
50
+ plugin.completed_experiments,
51
+ exp.leaf_nodes + list(reversed(exp.nonleaf_nodes))
52
+ )
53
+ self.assert_same_list(
54
+ plugin.started_example_ids, list(range(1, 11)) * 6
55
+ )
56
+ self.assert_same_list(
57
+ plugin.completed_example_ids, list(range(1, 11)) * 6
58
+ )
59
+ self.assert_same_list(plugin.skipped_experiments, [])
60
+ self.assertTrue(
61
+ pg.io.path_exists(os.path.join(run.output_root, 'run.json'))
62
+ )
63
+
64
+ for node in exp.nodes:
65
+ self.assertTrue(node.progress.is_started)
66
+ self.assertTrue(node.progress.is_completed)
67
+ if node.is_leaf:
68
+ self.assertEqual(node.progress.num_skipped, 0)
69
+ self.assertEqual(node.progress.num_completed, 10)
70
+ self.assertEqual(node.progress.num_failed, 1)
71
+ else:
72
+ self.assertEqual(node.progress.num_skipped, 0)
73
+ self.assertEqual(node.progress.num_failed, 0)
74
+ self.assertEqual(node.progress.num_processed, node.progress.num_total)
75
+
76
+ def test_raise_if_has_error(self):
77
+ root_dir = os.path.join(tempfile.mkdtemp(), 'test_raise_if_has_error')
78
+ exp = eval_test_helper.TestEvaluation()
79
+ with self.assertRaisesRegex(ValueError, 'x should not be 5'):
80
+ exp.run(
81
+ root_dir, runner='sequential', plugins=[], raise_if_has_error=True
82
+ )
83
+
84
+ def test_example_ids(self):
85
+ root_dir = os.path.join(tempfile.mkdtemp(), 'test_example_ids')
86
+ exp = eval_test_helper.test_experiment()
87
+ plugin = eval_test_helper.TestPlugin()
88
+ _ = exp.run(
89
+ root_dir, runner='sequential', plugins=[plugin], example_ids=[5, 7, 9]
90
+ )
91
+ self.assertEqual(plugin.started_example_ids, [5, 7, 9] * 6)
92
+ self.assertEqual(plugin.completed_example_ids, [5, 7, 9] * 6)
93
+
94
+ def test_shuffle_inputs(self):
95
+ root_dir = os.path.join(tempfile.mkdtemp(), 'test_shuffle_inputs')
96
+ exp = eval_test_helper.test_experiment()
97
+ plugin = eval_test_helper.TestPlugin()
98
+ run = exp.run(
99
+ root_dir, runner='sequential', plugins=[plugin], shuffle_inputs=True
100
+ )
101
+ self.assertTrue(run.shuffle_inputs)
102
+
103
+ def test_filter(self):
104
+ plugin = eval_test_helper.TestPlugin()
105
+ exp = eval_test_helper.test_experiment()
106
+ root_dir = os.path.join(tempfile.mkdtemp(), 'test_filter')
107
+
108
+ _ = exp.run(
109
+ root_dir, runner='sequential', plugins=[plugin],
110
+ filter=lambda e: e.lm.offset != 0
111
+ )
112
+ self.assert_same_list(
113
+ plugin.started_experiments,
114
+ exp.nonleaf_nodes + exp.leaf_nodes[2:]
115
+ )
116
+ self.assert_same_list(
117
+ plugin.skipped_experiments, exp.leaf_nodes[:2]
118
+ )
119
+ self.assert_same_list(
120
+ plugin.completed_experiments,
121
+ exp.leaf_nodes[2:] + [exp.children[1], exp]
122
+ )
123
+
124
+ def test_use_cache(self):
125
+ @pg.functor()
126
+ def test_inputs(num_examples: int = 10):
127
+ return [
128
+ pg.Dict(
129
+ x=i // 2, y=(i // 2) ** 2,
130
+ groundtruth=(i // 2 + (i // 2) ** 2)
131
+ ) for i in range(num_examples)
132
+ ]
133
+
134
+ exp = eval_test_helper.TestEvaluation(
135
+ inputs=test_inputs(num_examples=pg.oneof([2, 4]))
136
+ )
137
+ # Global cache.
138
+ root_dir = os.path.join(tempfile.mkdtemp(), 'global_cache')
139
+ run = exp.run(
140
+ root_dir, 'new', runner='sequential', use_cache='global', plugins=[]
141
+ )
142
+ self.assertTrue(pg.io.path_exists(run.output_path_for(exp, 'cache.json')))
143
+ self.assertEqual(exp.usage_summary.cached.total.num_requests, 4)
144
+ self.assertEqual(exp.usage_summary.uncached.total.num_requests, 2)
145
+
146
+ # Per-dataset cache.
147
+ root_dir = os.path.join(tempfile.mkdtemp(), 'per_dataset')
148
+ run = exp.run(
149
+ root_dir, 'new', runner='sequential',
150
+ use_cache='per_dataset', plugins=[]
151
+ )
152
+ for leaf in exp.leaf_nodes:
153
+ self.assertTrue(
154
+ pg.io.path_exists(run.output_path_for(leaf, 'cache.json'))
155
+ )
156
+ self.assertEqual(exp.usage_summary.cached.total.num_requests, 3)
157
+ self.assertEqual(exp.usage_summary.uncached.total.num_requests, 3)
158
+
159
+ # No cache.
160
+ root_dir = os.path.join(tempfile.mkdtemp(), 'no')
161
+ run = exp.run(root_dir, runner='sequential', use_cache='no', plugins=[])
162
+ self.assertFalse(pg.io.path_exists(run.output_path_for(exp, 'cache.json')))
163
+ for leaf in exp.leaf_nodes:
164
+ self.assertFalse(
165
+ pg.io.path_exists(run.output_path_for(leaf, 'cache.json'))
166
+ )
167
+ self.assertEqual(exp.usage_summary.cached.total.num_requests, 0)
168
+ self.assertEqual(exp.usage_summary.uncached.total.num_requests, 6)
169
+
170
+
171
+ if __name__ == '__main__':
172
+ unittest.main()
langfun/core/langfunc.py CHANGED
@@ -32,146 +32,43 @@ _TLS_LFUN_CALL_STACK = '_langfunc_callstack'
32
32
  # NOTE(daiyip): Only the template string belongs to the positional arguments,
33
33
  # all others are keyword-only for clarity.
34
34
  @pg.use_init_args(['template_str'])
35
- class LangFunc(
36
- template_lib.Template,
37
- ):
38
- r"""Base class for natural-language driven component.
39
-
40
- ``LangFunc`` is a language-driven component that enables users to
41
- seamlessly interact with Language Models (LLMs) using a blend of natural
42
- language and code. It empowers users to easily modularize prompt/execution
43
- logics, compose them, and simplify the creation of Language Model (LLM)-based
44
- components and applications.
45
-
46
- LangFunc can be conceptualized as a string template with embeddable code,
47
- but it distinguishes itself from traditional template systems in four key
48
- ways.
49
-
50
- Firstly, it enables easy modularization of templates along with the required
51
- values with OO principles, providing a reusable way for LLM-based content
52
- generation. For example:
53
-
54
- ```
55
- class FewshotExamples(lf.LangFunc):
56
- '''Base for fewshot prompt.
57
-
58
- {% for example in examples %}
59
- {{ example }}
60
- {% endfor %}
61
- '''
62
-
63
- # Usage 1: __init__ time binding.
64
- assert FewshotPrompt(examples=['foo', 'bar'])() == 'foo\nbar'
65
-
66
- # Usage 2: __call__ time binding.
67
- assert FewshotPrompt()(examples=['foo', 'bar']) == 'foo\nbar'
68
-
69
- class ToolDescription(lf.LangFunc):
70
- '''Tool descriptions.
71
-
72
- {% for tool in tools %}
73
- {{ tool.description }}
74
- {% endfor %}
75
- '''
76
- # We want to constrain tools to be a list of `Tool` objects.
77
- tools: list[Tool]
78
-
79
- # Raises: runtime type checking will fail on [1, 2, 3].
80
- ToolDescription(tools=[1, 2, 3])
81
- ```
82
-
83
- Secondly, it has the capability to compose multiple LangFuncs together,
84
- enabling the accomplishment of complex language tasks with maximum reuse.
85
- It allows users to provide program inputs to all the LangFuncs within a
86
- composition at the top level, significantly simplifying the process of
87
- providing context for users. For example:
88
-
89
- ```
90
- class ReAct(lf.LangFunc):
91
- '''ReAct prompt for tool-use.
92
-
93
- {{ preamble }}
94
- {{ tool_description }}
95
- {{ tool_examples }}
96
- {{ user_input }}
97
- '''
98
- # Default preamble, which could be overriden from subclass
99
- # or parsed from the `__init__` argument.
100
- preamble = 'Please help me on my task based on the following tools.',
101
-
102
- react = ReAct(
103
- tool_description=ToolDescription()
104
- tool_examples=FewshotExamples(),
105
- # Partially bind `tools` and `examples`.
106
- tools=my_tools,
107
- examples=[t.examples for t in my_tools]
108
- )
109
-
110
- # Late bind `user_input` at __call__ time.
111
- react(user_input='Help me get a lunch to go, veggie please.' )
112
- ```
113
-
114
- Thirdly, it allows the flexibility to encapsulate complex compositions to
115
- reusable classes and modify them. For example:
116
-
117
- ```
118
- # The compound decorator converts a function into a LangFunc.
119
- @lf.compound
120
- def react_with_tools(preamble, tools: list[Tool]):
121
- return ReAct(
122
- preamble=preamble,
123
- tool_description=ToolDescription()
124
- tool_examples=FewshotExamples(),
125
- # Partially bind `tools` and `examples`.
126
- tools=my_tools,
127
- examples=[t.examples for t in my_tools]
128
- )
35
+ class LangFunc(template_lib.Template):
36
+ r"""Base class for Language-based functions.
129
37
 
130
- # Actually, the entire chat application is a LangFunc.
131
- class Chat(lt.LangFunc):
132
- '''LLM-based Chat application.
38
+ LangFunc represents a function powered by a language model. It is a subclass
39
+ of `lf.Template` and can be thought of as a `lf.Template` augmented with an LM
40
+ and an output transformation. Calling a `lf.LangFunc` is equivalent to calling
41
+ the LM with the rendered prompt and transforming the output.
133
42
 
134
- llm({{ prompt }})
135
- '''
43
+ LangFunc can be directly constructed and used.
136
44
 
137
- chat = Chat(
138
- llm=Bard24B(),
139
- prompt=react_with_tools(
140
- preamble=(
141
- f'Please help me solve my problem using tools. '
142
- f'Current time is {{datetime.datetime.now()}}'),
143
- tools=my_tools))
45
+ ```python
46
+ import langfun as lf
144
47
 
145
- chat(user_input='Help me get a lunch to go, veggie please.')
146
- ```
48
+ func = lf.LangFunc("Hello, {{name}}!")
49
+ print(func(name="Gemini", lm=lf.llms.Gemini25Flash()))
50
+ # Output: Hello, how are you today?
51
+ ```
147
52
 
148
- Fourthly, LangFunc is built on top of PyGlove symbolic programming power,
149
- it could be manipulated programmatically, turned into a space for data
150
- sampling, or even tuned by AutoML. For example:
53
+ Or it can be subclassed:
151
54
 
152
- ```
153
- import pyglove as pg
55
+ ```python
56
+ import langfun as lf
154
57
 
155
- prompt_space = react_with_tools(
156
- preamble=pg.oneof([
157
- 'Help me solve my problem using the following tools:',
158
- 'Help me with the tools below:',
159
- ...
160
- ])
161
- # Choose any two of the tools for generating data.
162
- tools=pg.manyof(2, [
163
- google_search(...),
164
- doordash(...),
165
- ...
166
- ])
58
+ class Compute(lf.LangFunc):
59
+ '''Compute a simple arithmetic expression.
167
60
 
168
- for prompt in pg.random_sample(prompt_space):
169
- print(prompt(user_input='Help me book a conf room please.'))
61
+ {{expression}} = ?
62
+ '''
63
+ expression: str
170
64
 
171
- ```
65
+ def transform_output(self, lm_output: lf.Message) -> lf.Message:
66
+ lm_output.metadata.result = float(lm_output.text)
67
+ return lm_output
172
68
 
173
- For more capabilities on symbolic programming with PyGlove, please checkout
174
- https://pyglove.readthedocs.io/en/latest/.
69
+ r = Compute(expression="1 + 1")(lm=lf.llms.Gemini25Flash())
70
+ print(r.result)
71
+ # Output: 2.0
175
72
 
176
73
  Final note: always include these capitalized words if you don't want to treat
177
74
  the docstr as the template str: THIS IS NOT A TEMPLATE. So as a result, this
@@ -305,6 +202,24 @@ class LangFunc(
305
202
  message_cls: Type[message_lib.Message] = message_lib.UserMessage,
306
203
  **kwargs,
307
204
  ) -> message_lib.Message:
205
+ """Renders the template and transforms it as LM input message.
206
+
207
+ Args:
208
+ allow_partial: If True, allows partial rendering, which leaves unresolved
209
+ variables in place in the output text. Otherwise, raises error when
210
+ there are unresolved variables.
211
+ implicit: If True, reuse the rendering output if a parent `lf.Template`
212
+ is rendering current `lf.Template` multiple times. This is important
213
+ for making sure all references to the same `lf.Template` within a single
214
+ top-level rendering would return the same result. If False, every call
215
+ to `render` will trigger the actual rendering process.
216
+ message_cls: The message class used for creating the return value.
217
+ **kwargs: Values for template variables, which override values from
218
+ member attributes or context.
219
+
220
+ Returns:
221
+ A Message object containing the rendered result.
222
+ """
308
223
  lm_input = super().render(
309
224
  allow_partial=allow_partial,
310
225
  implicit=implicit,
@@ -82,7 +82,7 @@ class LangFuncCallTest(unittest.TestCase):
82
82
 
83
83
  i = l.render()
84
84
  self.assertEqual(i, 'Hello')
85
- self.assertEqual(i, message.UserMessage('Hello'))
85
+ self.assertEqual(i, message.UserMessage('Hello', __template_input__={}))
86
86
  self.assertEqual(i.tags, ['rendered'])
87
87
 
88
88
  r = l()
@@ -96,7 +96,9 @@ class LangFuncCallTest(unittest.TestCase):
96
96
  self.assertEqual(r.tags, ['lm-response', 'lm-output'])
97
97
  self.assertEqual(
98
98
  r.source,
99
- message.UserMessage('Hello', metadata=dict(cache_seed=0))
99
+ message.UserMessage(
100
+ 'Hello', metadata=dict(cache_seed=0, __template_input__={})
101
+ )
100
102
  )
101
103
  self.assertEqual(r.source.tags, ['rendered', 'lm-input'])
102
104
 
@@ -107,9 +109,9 @@ class LangFuncCallTest(unittest.TestCase):
107
109
  ' lm=ExcitedEchoer(sampling_options=LMSamplingOptions(temperature=None,'
108
110
  ' max_tokens=None, n=1, top_k=40, top_p=None, stop=None,'
109
111
  ' random_seed=None, logprobs=False, top_logprobs=None,'
110
- ' max_thinking_tokens=None, reasoning_effort=None), cache=None,'
111
- ' max_concurrency=None, timeout=120.0, max_attempts=5,'
112
- ' retry_interval=(5, 60), exponential_backoff=True,'
112
+ ' max_thinking_tokens=None, thinking_level=None, reasoning_effort=None,'
113
+ ' extras={}), cache=None, max_concurrency=None, timeout=120.0,'
114
+ ' max_attempts=5, retry_interval=(5, 60), exponential_backoff=True,'
113
115
  ' max_retry_interval=300, debug=False))',
114
116
  )
115
117