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/message.py CHANGED
@@ -20,7 +20,7 @@ import contextlib
20
20
  import functools
21
21
  import inspect
22
22
  import io
23
- from typing import Annotated, Any, ClassVar, Optional, Type, Union
23
+ from typing import Annotated, Any, Callable, ClassVar, Optional, Type, Union
24
24
 
25
25
  from langfun.core import modality
26
26
  from langfun.core import natural_language
@@ -32,15 +32,49 @@ class Message(
32
32
  pg.Object,
33
33
  pg.views.HtmlTreeView.Extension
34
34
  ):
35
- """Message.
35
+ """Message between users, LLMs and tools.
36
36
 
37
- ``Message`` is the protocol for users and the system to interact with
38
- LLMs. It consists of a text in the form of natural language,
39
- an identifier of the sender, and a dictionary of Python values as structured
40
- meta-data.
37
+ `lf.Message` is the fundamental unit of communication in Langfun. It
38
+ standardizes interactions with LLMs by encapsulating not only text but also
39
+ multi-modal content, as well as the sender's role and structured metadata.
41
40
 
42
- The subclasses of ``Message`` represent messages sent from different roles.
43
- Agents may use the roles to decide the orchastration logic.
41
+ **Key Components:**
42
+
43
+ * **`text`**: The natural language content of the message.
44
+ * **`sender`**: An identifier for the message originator (e.g., 'User',
45
+ 'AI', 'System').
46
+ * **`metadata`**: A dictionary for structured data, such as tool inputs/
47
+ outputs, scores, or other contextual information.
48
+ * **`referred_modalities`**: A dictionary of modality objects (e.g.,
49
+ `lf.Image`, `lf.Audio`) referenced within the message text via placeholders
50
+ like `<<[[image_id]]>>`.
51
+
52
+ Subclasses like `lf.UserMessage`, `lf.AIMessage`, and `lf.ToolMessage`
53
+ represent messages from specific roles, enabling more complex conversational
54
+ flows and agentic behaviors.
55
+
56
+ **Example:**
57
+
58
+ ```python
59
+ import langfun as lf
60
+
61
+ # Creating a user message with an image
62
+ image = lf.Image.from_path('/path/to/image.png')
63
+ user_message = lf.UserMessage(
64
+ f'What is in this image <<[[{image.id}]]>>?',
65
+ referred_modalities=[image])
66
+
67
+ # Creating an AI message with structured results
68
+ ai_message = lf.AIMessage(
69
+ 'It is a cat.',
70
+ metadata=dict(result=dict(label='cat', confidence=0.9)))
71
+
72
+ print(user_message.chunk())
73
+ # Output: ['What is in this image', <lf.Image object>, '?']
74
+
75
+ print(ai_message.result)
76
+ # Output: {'label': 'cat', 'confidence': 0.9}
77
+ ```
44
78
  """
45
79
 
46
80
  #
@@ -86,6 +120,11 @@ class Message(
86
120
 
87
121
  sender: Annotated[str, 'The sender of the message.']
88
122
 
123
+ referred_modalities: Annotated[
124
+ dict[str, pg.Ref[modality.Modality]],
125
+ 'The modality objects referred in the message.'
126
+ ] = pg.Dict()
127
+
89
128
  metadata: Annotated[
90
129
  dict[str, Any],
91
130
  (
@@ -111,6 +150,11 @@ class Message(
111
150
  *,
112
151
  # Default sender is specified in subclasses.
113
152
  sender: str | pg.object_utils.MissingValue = pg.MISSING_VALUE,
153
+ referred_modalities: (
154
+ list[modality.Modality]
155
+ | dict[str, modality.Modality]
156
+ | None
157
+ ) = None,
114
158
  metadata: dict[str, Any] | None = None,
115
159
  tags: list[str] | None = None,
116
160
  source: Optional['Message'] = None,
@@ -125,6 +169,7 @@ class Message(
125
169
  Args:
126
170
  text: The text in the message.
127
171
  sender: The sender name of the message.
172
+ referred_modalities: The modality objects referred in the message.
128
173
  metadata: Structured meta-data associated with this message.
129
174
  tags: Tags for the message.
130
175
  source: The source message of the current message.
@@ -138,9 +183,13 @@ class Message(
138
183
  """
139
184
  metadata = metadata or {}
140
185
  metadata.update(kwargs)
186
+ if isinstance(referred_modalities, list):
187
+ referred_modalities = {m.id: pg.Ref(m) for m in referred_modalities}
188
+
141
189
  super().__init__(
142
190
  text=text,
143
191
  metadata=metadata,
192
+ referred_modalities=referred_modalities or {},
144
193
  tags=tags or [],
145
194
  sender=sender,
146
195
  allow_partial=allow_partial,
@@ -186,7 +235,7 @@ class Message(
186
235
  A message created from the value.
187
236
  """
188
237
  if isinstance(value, modality.Modality):
189
- return cls('<<[[object]]>>', object=value)
238
+ return cls(f'<<[[{value.id}]]>>', referred_modalities=[value])
190
239
  if isinstance(value, Message):
191
240
  return value
192
241
  if isinstance(value, str):
@@ -224,6 +273,11 @@ class Message(
224
273
  """
225
274
  return MessageConverter.get(format_or_type, **kwargs).to_value(self)
226
275
 
276
+ @classmethod
277
+ def is_convertible(cls, format_or_type: str | Type[Any]) -> bool:
278
+ """Returns True if the value can be converted to a message."""
279
+ return MessageConverter.is_convertible(format_or_type)
280
+
227
281
  @classmethod
228
282
  def convertible_formats(cls) -> list[str]:
229
283
  """Returns supported format for message conversion."""
@@ -280,8 +334,7 @@ class Message(
280
334
  if key_path == Message.PATH_TEXT:
281
335
  return self.text
282
336
  else:
283
- v = self.metadata.sym_get(key_path, default, use_inferred=True)
284
- return v.value if isinstance(v, pg.Ref) else v
337
+ return self.metadata.sym_get(key_path, default, use_inferred=True)
285
338
 
286
339
  #
287
340
  # API for accessing the structured result and error.
@@ -361,46 +414,63 @@ class Message(
361
414
  # API for supporting modalities.
362
415
  #
363
416
 
417
+ def modalities(
418
+ self,
419
+ filter: ( # pylint: disable=redefined-builtin
420
+ Type[modality.Modality]
421
+ | Callable[[modality.Modality], bool]
422
+ | None
423
+ ) = None # pylint: disable=bad-whitespace
424
+ ) -> list[modality.Modality]:
425
+ """Returns the modality objects referred in the message."""
426
+ if inspect.isclass(filter) and issubclass(filter, modality.Modality):
427
+ filter_fn = lambda v: isinstance(v, filter) # pytype: disable=wrong-arg-types
428
+ elif filter is None:
429
+ filter_fn = lambda v: True
430
+ else:
431
+ filter_fn = filter
432
+ return [v for v in self.referred_modalities.values() if filter_fn(v)]
433
+
364
434
  @property
365
- def text_with_modality_hash(self) -> str:
366
- """Returns text with modality object placeheld by their 8-byte MD5 hash."""
367
- parts = [self.text]
368
- for name, modality_obj in self.referred_modalities().items():
369
- parts.append(
370
- f'<{name}>{modality_obj.hash}</{name}>'
371
- )
372
- return ''.join(parts)
435
+ def images(self) -> list[modality.Modality]:
436
+ """Returns the image objects referred in the message."""
437
+ assert False, 'Overridden in core/modalities/__init__.py'
438
+
439
+ @property
440
+ def videos(self) -> list[modality.Modality]:
441
+ """Returns the video objects referred in the message."""
442
+ assert False, 'Overridden in core/modalities/__init__.py'
443
+
444
+ @property
445
+ def audios(self) -> list[modality.Modality]:
446
+ """Returns the audio objects referred in the message."""
447
+ assert False, 'Overridden in core/modalities/__init__.py'
373
448
 
374
449
  def get_modality(
375
- self, var_name: str, default: Any = None, from_message_chain: bool = True
450
+ self,
451
+ var_name: str,
452
+ default: Any = None
376
453
  ) -> modality.Modality | None:
377
- """Gets the modality object referred in the message.
454
+ """Returns modality object referred in the message by its variable name.
378
455
 
379
456
  Args:
380
457
  var_name: The referred variable name for the modality object.
381
458
  default: default value.
382
- from_message_chain: If True, the look up will be performed from the
383
- message chain. Otherwise it will be performed in current message.
384
459
 
385
460
  Returns:
386
461
  A modality object if found, otherwise None.
387
462
  """
388
- obj = self.get(var_name, None)
389
- if isinstance(obj, modality.Modality):
390
- return obj
391
- elif obj is None and self.source is not None:
392
- return self.source.get_modality(var_name, default, from_message_chain)
393
- return default
394
-
395
- def referred_modalities(self) -> dict[str, modality.Modality]:
396
- """Returns modality objects attached on this message."""
397
- chunks = self.chunk()
398
- return {
399
- m.referred_name: m for m in chunks if isinstance(m, modality.Modality)
400
- }
463
+ return self.referred_modalities.get(var_name, default)
401
464
 
402
465
  def chunk(self, text: str | None = None) -> list[str | modality.Modality]:
403
- """Chunk a message into a list of str or modality objects."""
466
+ """Chunks message into a list of text and modality chunks.
467
+
468
+ Args:
469
+ text: The text to chunk. If None, use `self.text`.
470
+
471
+ Returns:
472
+ A list of text and modality chunks.
473
+ """
404
474
  chunks = []
405
475
 
406
476
  def add_text_chunk(text_piece: str) -> None:
@@ -425,20 +495,25 @@ class Message(
425
495
 
426
496
  var_name = text[var_start:ref_end].strip()
427
497
  var_value = self.get_modality(var_name)
428
- if var_value is not None:
429
- add_text_chunk(text[chunk_start:ref_start].strip(' '))
430
- chunks.append(var_value)
431
- chunk_start = ref_end + len(modality.Modality.REF_END)
498
+ if var_value is None:
499
+ raise ValueError(
500
+ f'Unknown modality reference: {var_name!r}. '
501
+ 'Please make sure the modality object is present in '
502
+ f'`referred_modalities` when creating {self.__class__.__name__}.'
503
+ )
504
+ add_text_chunk(text[chunk_start:ref_start].strip(' '))
505
+ chunks.append(var_value)
506
+ chunk_start = ref_end + len(modality.Modality.REF_END)
432
507
  return chunks
433
508
 
434
509
  @classmethod
435
510
  def from_chunks(
436
511
  cls, chunks: list[str | modality.Modality], separator: str = ' '
437
512
  ) -> 'Message':
438
- """Assembly a message from a list of string or modality objects."""
513
+ """Assembles a message from a list of string or modality objects."""
439
514
  fused_text = io.StringIO()
440
- ref_index = 0
441
515
  metadata = dict()
516
+ referred_modalities = dict()
442
517
  last_char = None
443
518
  for i, chunk in enumerate(chunks):
444
519
  if i > 0 and last_char not in ('\t', ' ', '\n', None):
@@ -451,14 +526,16 @@ class Message(
451
526
  last_char = None
452
527
  else:
453
528
  assert isinstance(chunk, modality.Modality), chunk
454
- var_name = f'obj{ref_index}'
455
- fused_text.write(modality.Modality.text_marker(var_name))
529
+ fused_text.write(modality.Modality.text_marker(chunk.id))
456
530
  last_char = modality.Modality.REF_END[-1]
457
531
  # Make a reference if the chunk is already owned by another object
458
532
  # to avoid copy.
459
- metadata[var_name] = pg.maybe_ref(chunk)
460
- ref_index += 1
461
- return cls(fused_text.getvalue().strip(), metadata=metadata)
533
+ referred_modalities[chunk.id] = pg.Ref(chunk)
534
+ return cls(
535
+ fused_text.getvalue().strip(),
536
+ referred_modalities=referred_modalities,
537
+ metadata=metadata,
538
+ )
462
539
 
463
540
  #
464
541
  # Tagging
@@ -523,7 +600,7 @@ class Message(
523
600
  return self.trace(Message.TAG_LM_OUTPUT)
524
601
 
525
602
  def last(self, tag: str) -> Optional['Message']:
526
- """Return the last message wih certain tag."""
603
+ """Returns the last message with a given tag."""
527
604
  current = self
528
605
  while current is not None:
529
606
  if tag in current.tags:
@@ -551,6 +628,11 @@ class Message(
551
628
  #
552
629
 
553
630
  def natural_language_format(self) -> str:
631
+ """Returns the natural language format representation."""
632
+ # Propagate the modality references to parent context if any.
633
+ if capture_context := modality.get_modality_capture_context():
634
+ for v in self.referred_modalities.values():
635
+ capture_context.capture(v)
554
636
  return self.text
555
637
 
556
638
  def __eq__(self, other: Any) -> bool:
@@ -568,8 +650,7 @@ class Message(
568
650
  def __getattr__(self, key: str) -> Any:
569
651
  if key not in self.metadata:
570
652
  raise AttributeError(key)
571
- v = self.metadata[key]
572
- return v.value if isinstance(v, pg.Ref) else v
653
+ return self.metadata[key]
573
654
 
574
655
  def _html_tree_view_content(
575
656
  self,
@@ -646,15 +727,14 @@ class Message(
646
727
  s.write(s.escape(chunk))
647
728
  else:
648
729
  assert isinstance(chunk, modality.Modality), chunk
649
- child_path = pg.KeyPath(['metadata', chunk.referred_name], root_path)
650
730
  s.write(
651
731
  pg.Html.element(
652
732
  'div',
653
733
  [
654
734
  view.render(
655
735
  chunk,
656
- name=chunk.referred_name,
657
- root_path=child_path,
736
+ name=chunk.id,
737
+ root_path=chunk.sym_path,
658
738
  collapse_level=(
659
739
  0 if collapse_modalities_in_text else 1
660
740
  ),
@@ -667,7 +747,7 @@ class Message(
667
747
  css_classes=['modality-in-text'],
668
748
  )
669
749
  )
670
- referred_chunks[chunk.referred_name] = chunk
750
+ referred_chunks[chunk.id] = chunk
671
751
  s.write('</div>')
672
752
  return s
673
753
 
@@ -874,6 +954,12 @@ class _MessageConverterRegistry:
874
954
  if converter.OUTPUT_TYPE is not None:
875
955
  self._type_to_converters[converter.OUTPUT_TYPE].append(converter)
876
956
 
957
+ def unregister(self, converter: Type['MessageConverter']) -> None:
958
+ """Unregisters a message converter."""
959
+ self._name_to_converter.pop(converter.FORMAT_ID, None)
960
+ if converter.OUTPUT_TYPE is not None:
961
+ self._type_to_converters[converter.OUTPUT_TYPE].remove(converter)
962
+
877
963
  def get_by_type(self, t: Type[Any], **kwargs) -> 'MessageConverter':
878
964
  """Returns a message converter for the given type."""
879
965
  t = self._type_to_converters[t]
@@ -904,6 +990,13 @@ class _MessageConverterRegistry:
904
990
  assert isinstance(format_or_type, type), format_or_type
905
991
  return self.get_by_type(format_or_type, **kwargs)
906
992
 
993
+ def is_convertible(self, format_or_type: str | Type[Any]) -> bool:
994
+ """Returns whether the message is convertible to the given format or type."""
995
+ if isinstance(format_or_type, str):
996
+ return format_or_type in self._name_to_converter
997
+ assert isinstance(format_or_type, type), format_or_type
998
+ return bool(self._type_to_converters.get(format_or_type))
999
+
907
1000
  def convertible_formats(self) -> list[str]:
908
1001
  """Returns a list of converter names."""
909
1002
  return sorted(list(self._name_to_converter.keys()))
@@ -995,6 +1088,11 @@ class MessageConverter(pg.Object):
995
1088
  """Returns a message converter for the given type."""
996
1089
  return cls._REGISTRY.get_by_type(t, **kwargs)
997
1090
 
1091
+ @classmethod
1092
+ def is_convertible(cls, format_or_type: str | Type[Any]) -> bool:
1093
+ """Returns whether the message is convertible to the given format or type."""
1094
+ return cls._REGISTRY.is_convertible(format_or_type)
1095
+
998
1096
  @classmethod
999
1097
  def convertible_formats(cls) -> list[str]:
1000
1098
  """Returns a list of converter names."""
@@ -1036,3 +1134,10 @@ class MemoryRecord(Message):
1036
1134
  """Message used as a memory record."""
1037
1135
 
1038
1136
  sender = 'Memory'
1137
+
1138
+
1139
+ @pg.use_init_args(['text', 'sender', 'metadata'])
1140
+ class ToolMessage(Message):
1141
+ """Message used as a tool call."""
1142
+
1143
+ sender = 'Tool'