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
langfun/core/template.py CHANGED
@@ -49,20 +49,94 @@ class Template(
49
49
  pg.typing.CustomTyping,
50
50
  pg.views.HtmlTreeView.Extension
51
51
  ):
52
- """Langfun string template.
53
-
54
- Langfun uses jinja2 as its template engine. Pleaes check out
55
- https://jinja.palletsprojects.com/en/3.1.x/templates/ for detailed
56
- explanation on the template language.
52
+ r"""Langfun string template.
53
+
54
+ `lf.Template` provides a flexible way to define and render prompts using
55
+ the Jinja2 templating engine. It supports variable substitution, composition
56
+ of templates, multi-modal content, and easy reuse through subclassing.
57
+
58
+ **Key Features:**
59
+
60
+ * **Jinja2 Syntax**: Leverages the full power of Jinja2 for
61
+ expressions, loops, and conditional logic within templates.
62
+ (See [Jinja2 documentation](
63
+ https://jinja.palletsprojects.com/en/3.1.x/templates/))
64
+ * **Composition**: Templates can include other `lf.Template`, `lf.Message`
65
+ and message-convertible objects, allowing complex prompts to be built from
66
+ reusable components.
67
+ * **Multi-Modal Support**: Seamlessly integrates multi-modal objects
68
+ (like `lf.Image`, `lf.Audio`) into templates.
69
+ * **Subclassing**: Define reusable prompt structures by subclassing
70
+ `lf.Template` and setting defaults.
71
+ * **Context Awareness**: Variables can be resolved from `render()`
72
+ arguments, template attributes, or the surrounding `lf.context`.
73
+
74
+ **1. Basic Usage:**
75
+ Variables are indicated by `{{variable_name}}`.
76
+
77
+ ```python
78
+ t = lf.Template("Hello, {{name}}!", name="World")
79
+ print(t.render())
80
+ # Output: Hello, World!
81
+ ```
82
+
83
+ **2. Providing Variables at Render Time:**
84
+ Variables can be provided when calling `render()`:
85
+
86
+ ```python
87
+ t = lf.Template("Hello, {{name}}!")
88
+ print(t.render(name="Alice"))
89
+ # Output: Hello, Alice!
90
+ ```
91
+
92
+ **3. Composition:**
93
+ Embed templates within other templates for modularity:
94
+
95
+ ```python
96
+ t = lf.Template(
97
+ "{{greeting}}, {{question}}",
98
+ greeting=lf.Template("Hello {{user}}", user="Bob"),
99
+ question=lf.UserMessage("How are you?")
100
+ )
101
+ print(t.render())
102
+ # Output: Hello Bob, How are you?
103
+ ```
104
+
105
+ **4. Multi-Modal Content:**
106
+ Reference modality objects in the template string:
107
+
108
+ ```python
109
+ image = lf.Image.from_path(...)
110
+ t = lf.Template("Describe this image: <<[[{{image.id}}]]>>", image=image)
111
+ # When rendered, 't' can be passed to a multi-modal model.
112
+ ```
113
+
114
+ **5. Subclassing for Reusability:**
115
+ Define a reusable template via subclassing. The docstring can serve as the
116
+ default template string if it doesn't contain "THIS IS NOT A TEMPLATE".
117
+
118
+ ```python
119
+ class Greeting(lf.Template):
120
+ '''Greeting prompt.
121
+
122
+ Hello, {{user}}!
123
+ '''
124
+ user: str = 'guest'
125
+
126
+ print(Greeting().render())
127
+ # Output: Hello, guest!
128
+ print(Greeting(user='Admin').render())
129
+ # Output: Hello, Admin!
130
+ ```
57
131
  """
58
132
 
59
133
  template_str: Annotated[
60
134
  str,
61
135
  (
62
- 'A template string in jinja2 syntax. During `render`, the variables '
63
- 'will be resolved from 1) the `kwargs` dict passed to the `render` '
64
- 'method; 2) the attributes of this object; and 3) the attributes of '
65
- 'its containing objects.'
136
+ 'A template string in jinja2 syntax. During `render`, variables '
137
+ 'will be resolved from: 1) keyword arguments passed to `render`; '
138
+ '2) attributes of this object; and 3) attributes of its containing '
139
+ 'objects.'
66
140
  ),
67
141
  ]
68
142
 
@@ -70,16 +144,16 @@ class Template(
70
144
  bool,
71
145
  (
72
146
  'If True, `inspect.cleandoc` will be applied on `template_str` to '
73
- 'additional indention, etc. Otherwise, the original form of the '
74
- 'template string will be used.'
147
+ 'remove leading/trailing spaces and fix indentation. Otherwise, '
148
+ 'the `template_str` will be used as is.'
75
149
  ),
76
150
  ] = True
77
151
 
78
152
  __kwargs__: Annotated[
79
153
  Any,
80
154
  (
81
- 'Wildcard keyword arguments for `__init__` that can be referred in '
82
- 'the template string. This allows modularization of prompt with '
155
+ 'Wildcard keyword arguments for `__init__` that can be referred to '
156
+ 'in the template string. This allows modularizing prompts with '
83
157
  'fully or partially bound variables.'
84
158
  ),
85
159
  ]
@@ -171,6 +245,7 @@ class Template(
171
245
 
172
246
  # Last render output.
173
247
  self._cached_render_output = None
248
+ self._referred_modalities = None
174
249
 
175
250
  @property
176
251
  def render_output(self) -> message_lib.Message | None:
@@ -195,12 +270,14 @@ class Template(
195
270
  """Returns referred variables.
196
271
 
197
272
  Args:
198
- specified: If True, include only variables that are specified. If False,
199
- include only variables that are not specified. If None, include both.
200
- closure: If True, include variables from referred LangFuncs recursively.
201
- Otherwise, include the immediate used variables.
202
- leaf: If True, include only the non-LangFunc variables. If False, include
203
- LangFunc variables. If None, include both.
273
+ specified: If True, include only variables specified with a value.
274
+ If False, include only variables that are not specified.
275
+ If None, include both specified and unspecified variables.
276
+ closure: If True, include variables from referred `lf.Template` objects
277
+ recursively. Otherwise, only include variables used in this template.
278
+ leaf: If True, include only variables that are not `lf.Template` objects.
279
+ If False, include only variables that are `lf.Template` objects.
280
+ If None, include both.
204
281
 
205
282
  Returns:
206
283
  A list of variable names that match the criteria.
@@ -229,17 +306,17 @@ class Template(
229
306
 
230
307
  @property
231
308
  def missing_vars(self) -> Set[str]:
232
- """Returns the missing variable names."""
309
+ """Returns missing variable names from this and referred templates."""
233
310
  return self.vars(closure=True, specified=False)
234
311
 
235
312
  @classmethod
236
313
  def raw_str(cls, text: str) -> str:
237
- """Returns a template string that preserve the text as original."""
314
+ """Returns a template string that preserves the text as original."""
238
315
  return '{% raw %}' + text + '{% endraw %}'
239
316
 
240
317
  @classmethod
241
318
  def from_raw_str(cls, text: str) -> 'Template':
242
- """Returns a template that preserve the text as original."""
319
+ """Returns a template that preserves the text as original."""
243
320
  return cls(cls.raw_str(text), clean=False)
244
321
 
245
322
  def render(
@@ -253,18 +330,20 @@ class Template(
253
330
  """Renders the template with variables from the context.
254
331
 
255
332
  Args:
256
- allow_partial: Allow partial rendering, this means that unresolved
257
- variables are allowed and remain in the output text.
258
- implicit: If True, reuse the rendering output if a parent LangFunc
259
- is rendering current LangFunc multiple times. This is important
260
- for making sure all references to the same LangFunc within a single
333
+ allow_partial: If True, allows partial rendering, which leaves unresolved
334
+ variables in place in the output text. Otherwise, raises error when
335
+ there are unresolved variables.
336
+ implicit: If True, reuse the rendering output if a parent `lf.Template`
337
+ is rendering current `lf.Template` multiple times. This is important
338
+ for making sure all references to the same `lf.Template` within a single
261
339
  top-level rendering would return the same result. If False, every call
262
340
  to `render` will trigger the actual rendering process.
263
341
  message_cls: The message class used for creating the return value.
264
- **kwargs: Values for template variables.
342
+ **kwargs: Values for template variables, which override values from
343
+ member attributes or context.
265
344
 
266
345
  Returns:
267
- An Message object as the rendered result.
346
+ A Message object containing the rendered result.
268
347
  """
269
348
  try:
270
349
  pg.object_utils.thread_local_push(_TLS_RENDER_STACK, self)
@@ -309,6 +388,17 @@ class Template(
309
388
  f'The value for template variable {var_name!r} is not '
310
389
  f'provided. Template: {self.template_str!r}'
311
390
  )
391
+ # Short-circuit common types that do not need conversions.
392
+ elif not isinstance(
393
+ var_value, (
394
+ bool, int, float, str, list, dict,
395
+ Template, message_lib.Message, modality.Modality
396
+ )
397
+ ) and message_lib.Message.is_convertible(type(var_value)):
398
+ # Automatically convert the value to a message if it is
399
+ # convertible. This allows users to drop message convertible
400
+ # objects into template for nested rendering.
401
+ var_value = message_lib.Message.from_value(var_value)
312
402
  inputs[var_name] = var_value
313
403
 
314
404
  # Enable Python format for builtin types during template rendering,
@@ -322,24 +412,46 @@ class Template(
322
412
  compact=True,
323
413
  python_format=True,
324
414
  ):
325
- # Natural language formattable objects will be returned in natural
326
- # language when they are directly returned as rendering elements
415
+
416
+ # Capture the modality objects whose references are being
417
+ # rendered
327
418
  # in the template.
328
- with modality.format_modality_as_ref():
329
- rendered_text = self._template.render(**inputs)
419
+ with modality.capture_rendered_modalities() as modality_refs:
420
+
421
+ # Natural language formattable objects will be returned in
422
+ # natural language when they are directly returned as rendering
423
+ # elements in the template.
424
+ with modality.format_modality_as_ref():
425
+ rendered_text = self._template.render(**inputs)
330
426
 
331
- # Carry additional metadata.
332
- metadata = self.additional_metadata()
427
+ # Carry the modality references passed from the constructor.
428
+ # This is to support modality objects that is already rendered
429
+ # in the template string.
430
+ if self._referred_modalities:
431
+ modality_refs.update(self._referred_modalities)
333
432
 
334
433
  if self.clean:
335
434
  rendered_text = rendered_text.strip()
336
435
 
337
- metadata.update(
338
- {k: pg.Ref(v) for k, v in inputs.items() if not inspect.ismethod(v)}
339
- )
436
+ # Fill message metadata.
437
+ metadata = {
438
+ '__template_input__': {
439
+ k: pg.Ref(v) for k, v in inputs.items()
440
+ if not inspect.ismethod(v)
441
+ },
442
+ }
443
+
444
+ # Carry additional metadata.
445
+ # TODO(daiyip): Consider to put additional metadata into a separate
446
+ # key under `metadata`.
447
+ metadata.update(self.additional_metadata())
340
448
 
341
449
  # Fill the variables for rendering the template as metadata.
342
- message = message_cls(text=rendered_text, metadata=metadata)
450
+ message = message_cls(
451
+ text=rendered_text,
452
+ referred_modalities=modality_refs,
453
+ metadata=metadata
454
+ )
343
455
 
344
456
  # Tag input as rendered message.
345
457
  message.tag(message_lib.Message.TAG_RENDERED)
@@ -366,7 +478,14 @@ class Template(
366
478
  assert top is self, (top, self)
367
479
 
368
480
  def additional_metadata(self) -> dict[str, Any]:
369
- """Returns additional metadta to be carried in the rendered message."""
481
+ """Returns additional metadata to be carried in the rendered message.
482
+
483
+ Subclasses can override this method to inject additional metadata based on
484
+ their own logic.
485
+
486
+ Returns:
487
+ A dict of metadata to be added to the output message.
488
+ """
370
489
  metadata = {}
371
490
  # Carry metadata from `lf.context`.
372
491
  for k, v in component.all_contextual_values().items():
@@ -374,7 +493,7 @@ class Template(
374
493
  metadata[k.removeprefix(_ADDITIONAL_METADATA_PREFIX)] = v
375
494
 
376
495
  # Carry metadata from fields.
377
- for k, v in self.sym_init_args.items():
496
+ for k, v in self.sym_init_args.sym_items():
378
497
  if k.startswith(_ADDITIONAL_METADATA_PREFIX):
379
498
  metadata[k.removeprefix(_ADDITIONAL_METADATA_PREFIX)] = v
380
499
  return metadata
@@ -391,7 +510,7 @@ class Template(
391
510
  child_transform: Callable[[pg.KeyPath, pg.typing.Field, Any], Any]
392
511
  | None = None,
393
512
  ) -> Tuple[bool, Any]:
394
- """Makes it applicable to pg.typing.Str()."""
513
+ """Makes template applicable to `pg.typing.Str()`."""
395
514
  del allow_partial
396
515
  del child_transform
397
516
 
@@ -425,10 +544,11 @@ class Template(
425
544
 
426
545
  @property
427
546
  def DEFAULT(self) -> 'Template':
428
- """Referring to the default value used for this template.
547
+ """Refers to the default value used for this template.
429
548
 
430
- This method is intended to be used in template for referring to the default
431
- value of current template. There are two scenarios:
549
+ This property is intended to be used in template string for referring to
550
+ the default value of current template, useful for wrapping the default
551
+ template. For example:
432
552
 
433
553
  Scenario 1: Use instance-level template_str to override the class default.
434
554
 
@@ -512,19 +632,24 @@ class Template(
512
632
  value: Union[str, message_lib.Message, 'Template'],
513
633
  **kwargs
514
634
  ) -> 'Template':
515
- """Create a template object from a string or template."""
635
+ """Creates a template object from a value."""
516
636
  if isinstance(value, cls):
517
637
  return value.clone(override=kwargs) if kwargs else value # pylint: disable=no-value-for-parameter
518
638
  if isinstance(value, str):
519
639
  return cls(template_str=value, **kwargs)
520
- if isinstance(value, message_lib.Message):
521
- kwargs.update(value.metadata)
522
- return cls(template_str=value.text, **kwargs)
523
640
  if isinstance(value, Template):
524
- lfun = cls(template_str=value.template_str, **kwargs)
641
+ lfun = cls(template_str=value.template_str, **kwargs) # pylint: disable=attribute-error
525
642
  # So lfun could acccess all attributes from value.
526
643
  lfun.sym_setparent(value)
527
644
  return lfun
645
+ if message_lib.Message.is_convertible(type(value)):
646
+ value = message_lib.Message.from_value(value)
647
+ if isinstance(value, message_lib.Message):
648
+ for k, v in value.metadata.sym_items(): # pylint: disable=attribute-error
649
+ kwargs[_ADDITIONAL_METADATA_PREFIX + k] = v
650
+ t = cls(template_str=value.text, **kwargs)
651
+ t._referred_modalities = value.referred_modalities
652
+ return t
528
653
  return cls(template_str='{{input}}', input=value, **kwargs)
529
654
 
530
655
  def _html_tree_view_content(
@@ -176,6 +176,69 @@ class DefinitionTest(unittest.TestCase):
176
176
  Template('{{x=1')
177
177
 
178
178
 
179
+ class FromValueTest(unittest.TestCase):
180
+
181
+ def test_from_str(self):
182
+ t = Template.from_value('Hello {{x}}', x=1)
183
+ self.assertTrue(pg.eq(t, Template('Hello {{x}}', x=1)))
184
+ self.assertEqual(t.render(), 'Hello 1')
185
+
186
+ def test_from_message(self):
187
+ class CustomModality(modality.Modality):
188
+ content: str
189
+
190
+ def to_bytes(self):
191
+ return self.content.encode()
192
+
193
+ message = Template('{{image}}', image=CustomModality('foo')).render()
194
+ t = Template.from_value(message)
195
+ m = t.render()
196
+ self.assertTrue(
197
+ pg.eq(m.modalities(), [CustomModality('foo')])
198
+ )
199
+ self.assertEqual(message.metadata, m.metadata)
200
+
201
+ def test_from_message_convertible(self):
202
+
203
+ class MyFormat(pg.Object):
204
+ text: str
205
+
206
+ class MyConverter(message_lib.MessageConverter): # pylint: disable=unused-variable
207
+ FORMAT_ID = 'int'
208
+ OUTPUT_TYPE = MyFormat
209
+
210
+ def to_value(self, m: message_lib.Message) -> MyFormat:
211
+ return MyFormat(text=m.text)
212
+
213
+ def from_value(self, value: MyFormat) -> message_lib.Message:
214
+ return message_lib.UserMessage(value.text)
215
+
216
+ t = Template.from_value(MyFormat(text='1'))
217
+ self.assertEqual(t.render(), '1')
218
+
219
+ def test_from_same_template(self):
220
+ t = Template('Hello {{x}}', x=1)
221
+ t2 = Template.from_value(t)
222
+ self.assertTrue(pg.eq(t, t2))
223
+ self.assertEqual(t2.x, 1)
224
+
225
+ def test_from_different_template(self):
226
+ t = Template('Hello {{x}}', x=1)
227
+
228
+ class MyTemplate(Template):
229
+ pass
230
+
231
+ t2 = MyTemplate.from_value(t, x=2)
232
+ self.assertIsInstance(t2, MyTemplate)
233
+ self.assertEqual(t2.template_str, t.template_str)
234
+ self.assertEqual(t2.x, 2)
235
+
236
+ def test_from_python_object(self):
237
+ t = Template.from_value(pg.Dict(x=1, y=2))
238
+ self.assertEqual(t.template_str, '{{input}}')
239
+ self.assertEqual(t.input, pg.Dict(x=1, y=2))
240
+
241
+
179
242
  class VarsTest(unittest.TestCase):
180
243
 
181
244
  def assert_missing_vars(self, t: Template, missing_vars: set[str]):
@@ -251,6 +314,10 @@ class VarsTest(unittest.TestCase):
251
314
 
252
315
  class RenderTest(unittest.TestCase):
253
316
 
317
+ def setUp(self):
318
+ super().setUp()
319
+ self.maxDiff = None
320
+
254
321
  def test_clean(self):
255
322
  l = Template('\n Hello\n ')
256
323
  self.assertEqual(l.render(), 'Hello')
@@ -279,19 +346,20 @@ class RenderTest(unittest.TestCase):
279
346
  )
280
347
  v = l.render()
281
348
  self.assertEqual(v, 'How are you 1')
282
- self.assertIs(v.x, l.x)
283
- self.assertEqual(v.x.render_output, 'are you 1')
284
- self.assertIs(v.x.y, l.x.y)
285
- self.assertEqual(v.x.y.render_output, 'you 1')
286
- self.assertEqual(v.x.y.n, 1)
349
+ self.assertIs(v.__template_input__.x, l.x)
350
+ self.assertEqual(v.__template_input__.x.render_output, 'are you 1')
351
+ self.assertIs(v.__template_input__.x.y, l.x.y)
352
+ self.assertIs(v.__template_input__.x.y.n, l.x.y.n)
353
+ self.assertEqual(v.__template_input__.x.y.render_output, 'you 1')
354
+ self.assertEqual(v.__template_input__.x.y.n, 1)
287
355
 
288
356
  def test_render_with_call_args(self):
289
357
  l = Template('Hello {{x}} and {{y}}', x=1, y=Template('{{z}}'), z=3)
290
358
  v = l.render(x=2)
291
359
  self.assertEqual(v, 'Hello 2 and 3')
292
- self.assertEqual(v.x, 2)
293
- self.assertEqual(v.y, '3')
294
- self.assertEqual(v.y.z, 3)
360
+ self.assertEqual(v.__template_input__.x, 2)
361
+ self.assertEqual(v.__template_input__.y, '3')
362
+ self.assertEqual(v.__template_input__.y.z, 3)
295
363
 
296
364
  def test_render_cache(self):
297
365
  class DynamicContent(Template):
@@ -321,11 +389,17 @@ class RenderTest(unittest.TestCase):
321
389
  def to_bytes(self):
322
390
  return self.content.encode()
323
391
 
392
+ foo = CustomModality('foo')
393
+ message = Template('This is {{ x }} and {{ a }}', x=1, a=foo).render()
324
394
  self.assertEqual(
325
- Template(
326
- 'This is {{ x }} and {{ a }}', x=1, a=CustomModality('foo')
327
- ).render(),
328
- 'This is 1 and <<[[a]]>>',
395
+ message,
396
+ 'This is 1 and <<[[custom_modality:acbd18db]]>>',
397
+ )
398
+ self.assertIn(
399
+ 'custom_modality:acbd18db', message.referred_modalities
400
+ )
401
+ self.assertIs(
402
+ message.referred_modalities['custom_modality:acbd18db'], foo
329
403
  )
330
404
 
331
405
  def test_render_with_default(self):
@@ -511,12 +585,44 @@ class RenderTest(unittest.TestCase):
511
585
  self.assert_partial(Template('Hello {{len(x)}}'), 'Hello {{len(x)}}')
512
586
 
513
587
  def test_additional_metadata(self):
514
- t = Template('hi', metadata_weights=1.0, y=2)
515
- self.assertEqual(t.render(), message_lib.UserMessage('hi', weights=1.0))
588
+ t = Template('hi {{y}}', metadata_weights=1.0, y=2, z=1)
589
+ self.assertEqual(
590
+ t.render(),
591
+ message_lib.UserMessage(
592
+ 'hi 2', weights=1.0, __template_input__={'y': 2}
593
+ )
594
+ )
516
595
 
517
- t = Template('hi')
518
- with component.context(metadata_weights=1.0, y=2):
519
- self.assertEqual(t.render(), message_lib.UserMessage('hi', weights=1.0))
596
+ t = Template('hi {{y}}')
597
+ with component.context(metadata_weights=1.0, y=3):
598
+ self.assertEqual(
599
+ t.render(z=2),
600
+ message_lib.UserMessage(
601
+ 'hi 3', weights=1.0, __template_input__={'y': 3}
602
+ )
603
+ )
604
+
605
+ def test_render_with_message_convertible_type(self):
606
+ class MyFormat(pg.Object):
607
+ text: str
608
+
609
+ class MyConverter(message_lib.MessageConverter): # pylint: disable=unused-variable
610
+ FORMAT_ID = 'another-format'
611
+ OUTPUT_TYPE = MyFormat
612
+
613
+ def to_value(self, m: message_lib.Message) -> MyFormat:
614
+ return MyFormat(text=m.text)
615
+
616
+ def from_value(self, value: MyFormat) -> message_lib.Message:
617
+ return message_lib.UserMessage(value.text)
618
+
619
+ t = Template('Hello {{x}}', x=MyFormat(text='world'))
620
+ rendered_message = t.render()
621
+ self.assertEqual(rendered_message, 'Hello world')
622
+ self.assertIsInstance(
623
+ rendered_message.__template_input__['x'], message_lib.UserMessage
624
+ )
625
+ self.assertEqual(rendered_message.__template_input__['x'].text, 'world')
520
626
 
521
627
 
522
628
  class TemplateRenderEventTest(unittest.TestCase):
@@ -0,0 +1,43 @@
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
+ """Environment for LLM agents."""
15
+
16
+ # pylint: disable=g-importing-member, g-bad-import-order, g-import-not-at-top
17
+ from langfun.env.interface import EnvironmentError # pylint: disable=redefined-builtin
18
+ from langfun.env.interface import EnvironmentOutageError
19
+ from langfun.env.interface import EnvironmentOverloadError
20
+ from langfun.env.interface import SandboxError
21
+ from langfun.env.interface import SandboxStateError
22
+
23
+ from langfun.env.interface import Environment
24
+ from langfun.env.interface import Sandbox
25
+ from langfun.env.interface import Feature
26
+
27
+ from langfun.env.interface import EventHandler
28
+
29
+ # Decorators for sandbox/feature methods.
30
+ from langfun.env.interface import treat_as_sandbox_state_error
31
+ from langfun.env.interface import log_activity
32
+
33
+ from langfun.env.base_environment import BaseEnvironment
34
+ from langfun.env.base_sandbox import BaseSandbox
35
+ from langfun.env.base_feature import BaseFeature
36
+
37
+ from langfun.env import load_balancers
38
+ from langfun.env.load_balancers import LoadBalancer
39
+
40
+ # Import all event handlers.
41
+ from langfun.env.event_handlers import *
42
+
43
+ # Google-internal imports.