langfun 0.1.2.dev202511030805__py3-none-any.whl → 0.1.2.dev202511050805__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 (77) hide show
  1. langfun/core/agentic/action.py +76 -9
  2. langfun/core/agentic/action_eval.py +9 -2
  3. langfun/core/async_support.py +32 -3
  4. langfun/core/coding/python/correction.py +19 -9
  5. langfun/core/coding/python/execution.py +14 -12
  6. langfun/core/coding/python/generation.py +21 -16
  7. langfun/core/coding/python/sandboxing.py +23 -3
  8. langfun/core/component.py +42 -3
  9. langfun/core/concurrent.py +70 -6
  10. langfun/core/console.py +1 -1
  11. langfun/core/data/conversion/anthropic.py +10 -3
  12. langfun/core/data/conversion/gemini.py +9 -2
  13. langfun/core/data/conversion/openai.py +17 -7
  14. langfun/core/eval/base.py +46 -42
  15. langfun/core/eval/matching.py +5 -2
  16. langfun/core/eval/patching.py +3 -3
  17. langfun/core/eval/scoring.py +4 -3
  18. langfun/core/eval/v2/checkpointing.py +30 -4
  19. langfun/core/eval/v2/evaluation.py +59 -13
  20. langfun/core/eval/v2/example.py +22 -11
  21. langfun/core/eval/v2/experiment.py +51 -8
  22. langfun/core/eval/v2/metric_values.py +23 -3
  23. langfun/core/eval/v2/metrics.py +33 -4
  24. langfun/core/eval/v2/progress.py +9 -1
  25. langfun/core/eval/v2/reporting.py +15 -1
  26. langfun/core/eval/v2/runners.py +27 -7
  27. langfun/core/langfunc.py +45 -130
  28. langfun/core/language_model.py +88 -10
  29. langfun/core/llms/anthropic.py +27 -2
  30. langfun/core/llms/azure_openai.py +29 -17
  31. langfun/core/llms/cache/base.py +22 -2
  32. langfun/core/llms/cache/in_memory.py +48 -7
  33. langfun/core/llms/compositional.py +25 -1
  34. langfun/core/llms/deepseek.py +29 -1
  35. langfun/core/llms/fake.py +32 -1
  36. langfun/core/llms/gemini.py +9 -1
  37. langfun/core/llms/google_genai.py +29 -1
  38. langfun/core/llms/groq.py +27 -2
  39. langfun/core/llms/llama_cpp.py +22 -3
  40. langfun/core/llms/openai.py +29 -1
  41. langfun/core/llms/openai_compatible.py +18 -6
  42. langfun/core/llms/rest.py +12 -1
  43. langfun/core/llms/vertexai.py +39 -6
  44. langfun/core/logging.py +1 -1
  45. langfun/core/mcp/client.py +77 -22
  46. langfun/core/mcp/session.py +90 -10
  47. langfun/core/mcp/tool.py +83 -23
  48. langfun/core/memory.py +1 -0
  49. langfun/core/message.py +75 -11
  50. langfun/core/message_test.py +9 -0
  51. langfun/core/modalities/audio.py +21 -1
  52. langfun/core/modalities/image.py +19 -1
  53. langfun/core/modalities/mime.py +54 -4
  54. langfun/core/modalities/pdf.py +19 -1
  55. langfun/core/modalities/video.py +21 -1
  56. langfun/core/modality.py +66 -5
  57. langfun/core/natural_language.py +1 -1
  58. langfun/core/sampling.py +4 -4
  59. langfun/core/structured/completion.py +32 -37
  60. langfun/core/structured/description.py +54 -50
  61. langfun/core/structured/function_generation.py +29 -12
  62. langfun/core/structured/mapping.py +70 -15
  63. langfun/core/structured/parsing.py +90 -74
  64. langfun/core/structured/parsing_test.py +0 -3
  65. langfun/core/structured/querying.py +201 -130
  66. langfun/core/structured/schema.py +70 -10
  67. langfun/core/structured/schema_generation.py +33 -14
  68. langfun/core/structured/scoring.py +45 -34
  69. langfun/core/structured/tokenization.py +24 -9
  70. langfun/core/subscription.py +2 -2
  71. langfun/core/template.py +139 -40
  72. langfun/core/template_test.py +40 -0
  73. {langfun-0.1.2.dev202511030805.dist-info → langfun-0.1.2.dev202511050805.dist-info}/METADATA +1 -1
  74. {langfun-0.1.2.dev202511030805.dist-info → langfun-0.1.2.dev202511050805.dist-info}/RECORD +77 -77
  75. {langfun-0.1.2.dev202511030805.dist-info → langfun-0.1.2.dev202511050805.dist-info}/WHEEL +0 -0
  76. {langfun-0.1.2.dev202511030805.dist-info → langfun-0.1.2.dev202511050805.dist-info}/licenses/LICENSE +0 -0
  77. {langfun-0.1.2.dev202511030805.dist-info → langfun-0.1.2.dev202511050805.dist-info}/top_level.txt +0 -0
@@ -35,38 +35,50 @@ def score(
35
35
  return_scoring_results: bool = False,
36
36
  **kwargs,
37
37
  ) -> list[float] | list[lf.LMScoringResult]:
38
- """Scores the outputs based on the prompt.
39
-
40
- Examples:
41
- ```
42
- # Example 1: Scoring text output based on the user prompt.
43
- scores = lf.score('{{x}} + {{y}} =', ['1', '2', '3'], lm=lm, x=1, y=2)
44
- assert len(scores) == 3
45
-
46
- # Example 2: Scoring int output based on the formulated OOP prompt.
47
- scores = lf.score('1 + 1 =', [1, 2, 3], lm=lm)
48
- assert len(scores) == 3
49
-
50
- class Answer(pg.Object):
51
- result: int
52
-
53
- # Example 3: Scoring object output based on the formulated OOP prompt.
54
- scores = lf.score('1 + 1 =', [Answer(1), Answer(2), Answer(3)], lm=lm)
55
- assert len(scores) == 3
56
-
57
- # Example 4: Scoring object field value based on the formulated OOP prompt
58
- # and the generated tokens before the first `pg.oneof`.
59
- scores = lf.score('1 + 1 =', [Answer(pg.oneof([1, 2, 3]))], lm=lm)
60
- assert len(scores) == 3
61
-
62
- # Example 5: Scoring multiple prompt/completion pairs.
63
- scores = lf.score(
64
- ['1 + 1=', '2 + 3='],
65
- ['2', '4'],
66
- lm=lm
67
- )
68
- assert len(scores) == 2
69
- ```
38
+ """Scores completions based on a prompt using a language model.
39
+
40
+ `lf.score` computes the likelihood of each completion being generated given
41
+ a prompt, according to the specified language model. It can score text
42
+ completions or structured objects. If `schema` is provided, Langfun
43
+ formats the prompt and completions appropriately before scoring.
44
+
45
+ **Example 1: Score text completions**
46
+ ```python
47
+ import langfun as lf
48
+ scores = lf.score(
49
+ '1 + 1 =',
50
+ ['2', '3', '4'],
51
+ lm=lf.llms.Gemini25Flash())
52
+ print([f'{s:.3f}' for s in scores])
53
+ # Output: ['-0.001', '-2.345', '-3.456']
54
+ ```
55
+
56
+ **Example 2: Score structured completions**
57
+ ```python
58
+ import langfun as lf
59
+ import pyglove as pg
60
+
61
+ class Answer(pg.Object):
62
+ result: int
63
+
64
+ scores = lf.score(
65
+ '1 + 1 =',
66
+ [Answer(result=2), Answer(result=3), Answer(result=4)],
67
+ lm=lf.llms.Gemini25Flash())
68
+ print([f'{s:.3f}' for s in scores])
69
+ # Output: ['-0.001', '-2.345', '-3.456']
70
+ ```
71
+
72
+ **Example 3: Score multiple prompt/completion pairs**
73
+ ```python
74
+ import langfun as lf
75
+ scores = lf.score(
76
+ ['1 + 1 =', '2 + 2 ='],
77
+ ['2', '4'],
78
+ lm=lf.llms.Gemini25Flash())
79
+ print([f'{s:.3f}' for s in scores])
80
+ # Output: ['-0.001', '-0.002']
81
+ ```
70
82
 
71
83
  Args:
72
84
  prompt: The prompt(s) based on which each completion will be scored.
@@ -74,8 +86,7 @@ def score(
74
86
  schema: The schema as the output type. If None, it will be inferred from
75
87
  the completions.
76
88
  lm: The language model used for scoring.
77
- examples: Fewshot exemplars used together with the prompt in getting the
78
- completions.
89
+ examples: Few-shot examples used to construct the prompt for scoring.
79
90
  protocol: The protocol for formulating the prompt based on objects.
80
91
  return_scoring_results: If True, returns a list of `lf.LMScoringResult`,
81
92
  otherwise returns a list of floats as the scores of each completion.
@@ -23,7 +23,7 @@ import pyglove as pg
23
23
 
24
24
 
25
25
  def tokenize(
26
- prompt: Union[str, pg.Symbolic] | list[str | pg.Symbolic],
26
+ prompt: Union[str, pg.Symbolic, list[str | pg.Symbolic]],
27
27
  schema: Union[
28
28
  schema_lib.Schema, Type[Any], list[Type[Any]], dict[str, Any], None
29
29
  ] = None,
@@ -33,20 +33,35 @@ def tokenize(
33
33
  protocol: schema_lib.SchemaProtocol = 'python',
34
34
  **kwargs,
35
35
  ) -> list[tuple[str | bytes, int]]:
36
- """Tokenize the prompt for `lf.query`.
36
+ """Renders a prompt and tokenizes it using a language model.
37
+
38
+ `lf.tokenize` first renders a prompt based on the provided `prompt`,
39
+ `schema`, and `examples`, similar to `lf.query`, and then uses the
40
+ specified language model (`lm`) to tokenize the resulting message.
41
+ This is useful for understanding how a prompt is seen by the model or
42
+ for estimating token counts before sending requests.
43
+
44
+ **Example:**
45
+
46
+ ```python
47
+ import langfun as lf
48
+ tokens = lf.tokenize('Hello world!', lm=lf.llms.Gpt4())
49
+ print(tokens)
50
+ # Output might look like: [('Hello', 15339), (' world', 1917), ('!', 0)]
51
+ ```
37
52
 
38
53
  Args:
39
- prompt: The prompt(s) based on which each completion will be scored.
40
- schema: The schema as the output type. If None, it will be inferred from
41
- the completions.
42
- lm: The language model used for scoring.
43
- examples: Fewshot exemplars used together with the prompt in getting the
44
- completions.
54
+ prompt: The prompt to render and tokenize. Can be a string, `pg.Symbolic`,
55
+ or `lf.Template`.
56
+ schema: The schema for formatting the prompt, if `prompt` is structured or
57
+ if schema-based formatting is needed.
58
+ lm: The language model to use for tokenization.
59
+ examples: Few-shot examples to include in the rendered prompt.
45
60
  protocol: The protocol for formulating the prompt based on objects.
46
61
  **kwargs: Keyword arguments that are referred by the prompt.
47
62
 
48
63
  Returns:
49
- A list of (text, token_id) tuples.
64
+ A list of (token_str, token_id) tuples representing the tokenized prompt.
50
65
  """
51
66
  input_message = querying.query_prompt(
52
67
  prompt,
@@ -35,7 +35,7 @@ EventType = TypeVar('EventType')
35
35
 
36
36
 
37
37
  class EventHandler(Generic[EventType], metaclass=abc.ABCMeta):
38
- """Interface for event subscriber."""
38
+ """Interface for event handler."""
39
39
 
40
40
  @classmethod
41
41
  @functools.cache
@@ -51,7 +51,7 @@ class EventHandler(Generic[EventType], metaclass=abc.ABCMeta):
51
51
 
52
52
  @classmethod
53
53
  def accepts(cls, event: Event[Any]) -> bool:
54
- """Returns True if current event handler class can accepts an event."""
54
+ """Returns True if current event handler class can accept an event."""
55
55
  return isinstance(event, cls.event_type())
56
56
 
57
57
  @abc.abstractmethod
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
  ]
@@ -196,12 +270,14 @@ class Template(
196
270
  """Returns referred variables.
197
271
 
198
272
  Args:
199
- specified: If True, include only variables that are specified. If False,
200
- include only variables that are not specified. If None, include both.
201
- closure: If True, include variables from referred LangFuncs recursively.
202
- Otherwise, include the immediate used variables.
203
- leaf: If True, include only the non-LangFunc variables. If False, include
204
- 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.
205
281
 
206
282
  Returns:
207
283
  A list of variable names that match the criteria.
@@ -230,17 +306,17 @@ class Template(
230
306
 
231
307
  @property
232
308
  def missing_vars(self) -> Set[str]:
233
- """Returns the missing variable names."""
309
+ """Returns missing variable names from this and referred templates."""
234
310
  return self.vars(closure=True, specified=False)
235
311
 
236
312
  @classmethod
237
313
  def raw_str(cls, text: str) -> str:
238
- """Returns a template string that preserve the text as original."""
314
+ """Returns a template string that preserves the text as original."""
239
315
  return '{% raw %}' + text + '{% endraw %}'
240
316
 
241
317
  @classmethod
242
318
  def from_raw_str(cls, text: str) -> 'Template':
243
- """Returns a template that preserve the text as original."""
319
+ """Returns a template that preserves the text as original."""
244
320
  return cls(cls.raw_str(text), clean=False)
245
321
 
246
322
  def render(
@@ -254,18 +330,20 @@ class Template(
254
330
  """Renders the template with variables from the context.
255
331
 
256
332
  Args:
257
- allow_partial: Allow partial rendering, this means that unresolved
258
- variables are allowed and remain in the output text.
259
- implicit: If True, reuse the rendering output if a parent LangFunc
260
- is rendering current LangFunc multiple times. This is important
261
- 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
262
339
  top-level rendering would return the same result. If False, every call
263
340
  to `render` will trigger the actual rendering process.
264
341
  message_cls: The message class used for creating the return value.
265
- **kwargs: Values for template variables.
342
+ **kwargs: Values for template variables, which override values from
343
+ member attributes or context.
266
344
 
267
345
  Returns:
268
- An Message object as the rendered result.
346
+ A Message object containing the rendered result.
269
347
  """
270
348
  try:
271
349
  pg.object_utils.thread_local_push(_TLS_RENDER_STACK, self)
@@ -310,6 +388,17 @@ class Template(
310
388
  f'The value for template variable {var_name!r} is not '
311
389
  f'provided. Template: {self.template_str!r}'
312
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)
313
402
  inputs[var_name] = var_value
314
403
 
315
404
  # Enable Python format for builtin types during template rendering,
@@ -389,7 +478,14 @@ class Template(
389
478
  assert top is self, (top, self)
390
479
 
391
480
  def additional_metadata(self) -> dict[str, Any]:
392
- """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
+ """
393
489
  metadata = {}
394
490
  # Carry metadata from `lf.context`.
395
491
  for k, v in component.all_contextual_values().items():
@@ -414,7 +510,7 @@ class Template(
414
510
  child_transform: Callable[[pg.KeyPath, pg.typing.Field, Any], Any]
415
511
  | None = None,
416
512
  ) -> Tuple[bool, Any]:
417
- """Makes it applicable to pg.typing.Str()."""
513
+ """Makes template applicable to `pg.typing.Str()`."""
418
514
  del allow_partial
419
515
  del child_transform
420
516
 
@@ -448,10 +544,11 @@ class Template(
448
544
 
449
545
  @property
450
546
  def DEFAULT(self) -> 'Template':
451
- """Referring to the default value used for this template.
547
+ """Refers to the default value used for this template.
452
548
 
453
- This method is intended to be used in template for referring to the default
454
- 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:
455
552
 
456
553
  Scenario 1: Use instance-level template_str to override the class default.
457
554
 
@@ -535,22 +632,24 @@ class Template(
535
632
  value: Union[str, message_lib.Message, 'Template'],
536
633
  **kwargs
537
634
  ) -> 'Template':
538
- """Create a template object from a string or template."""
635
+ """Creates a template object from a value."""
539
636
  if isinstance(value, cls):
540
637
  return value.clone(override=kwargs) if kwargs else value # pylint: disable=no-value-for-parameter
541
638
  if isinstance(value, str):
542
639
  return cls(template_str=value, **kwargs)
640
+ if isinstance(value, Template):
641
+ lfun = cls(template_str=value.template_str, **kwargs) # pylint: disable=attribute-error
642
+ # So lfun could acccess all attributes from value.
643
+ lfun.sym_setparent(value)
644
+ return lfun
645
+ if message_lib.Message.is_convertible(type(value)):
646
+ value = message_lib.Message.from_value(value)
543
647
  if isinstance(value, message_lib.Message):
544
648
  for k, v in value.metadata.sym_items(): # pylint: disable=attribute-error
545
649
  kwargs[_ADDITIONAL_METADATA_PREFIX + k] = v
546
650
  t = cls(template_str=value.text, **kwargs)
547
651
  t._referred_modalities = value.referred_modalities
548
652
  return t
549
- if isinstance(value, Template):
550
- lfun = cls(template_str=value.template_str, **kwargs) # pylint: disable=attribute-error
551
- # So lfun could acccess all attributes from value.
552
- lfun.sym_setparent(value)
553
- return lfun
554
653
  return cls(template_str='{{input}}', input=value, **kwargs)
555
654
 
556
655
  def _html_tree_view_content(
@@ -198,6 +198,24 @@ class FromValueTest(unittest.TestCase):
198
198
  )
199
199
  self.assertEqual(message.metadata, m.metadata)
200
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
+
201
219
  def test_from_same_template(self):
202
220
  t = Template('Hello {{x}}', x=1)
203
221
  t2 = Template.from_value(t)
@@ -584,6 +602,28 @@ class RenderTest(unittest.TestCase):
584
602
  )
585
603
  )
586
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')
626
+
587
627
 
588
628
  class TemplateRenderEventTest(unittest.TestCase):
589
629
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langfun
3
- Version: 0.1.2.dev202511030805
3
+ Version: 0.1.2.dev202511050805
4
4
  Summary: Langfun: Language as Functions.
5
5
  Home-page: https://github.com/google/langfun
6
6
  Author: Langfun Authors