langfun 0.1.2.dev202510230805__py3-none-any.whl → 0.1.2.dev202511160804__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 (146) hide show
  1. langfun/core/__init__.py +1 -0
  2. langfun/core/agentic/action.py +107 -12
  3. langfun/core/agentic/action_eval.py +9 -2
  4. langfun/core/agentic/action_test.py +25 -0
  5. langfun/core/async_support.py +32 -3
  6. langfun/core/coding/python/correction.py +19 -9
  7. langfun/core/coding/python/execution.py +14 -12
  8. langfun/core/coding/python/generation.py +21 -16
  9. langfun/core/coding/python/sandboxing.py +23 -3
  10. langfun/core/component.py +42 -3
  11. langfun/core/concurrent.py +70 -6
  12. langfun/core/concurrent_test.py +1 -0
  13. langfun/core/console.py +1 -1
  14. langfun/core/data/conversion/anthropic.py +12 -3
  15. langfun/core/data/conversion/anthropic_test.py +8 -6
  16. langfun/core/data/conversion/gemini.py +9 -2
  17. langfun/core/data/conversion/gemini_test.py +12 -9
  18. langfun/core/data/conversion/openai.py +145 -31
  19. langfun/core/data/conversion/openai_test.py +161 -17
  20. langfun/core/eval/base.py +47 -43
  21. langfun/core/eval/base_test.py +4 -4
  22. langfun/core/eval/matching.py +5 -2
  23. langfun/core/eval/patching.py +3 -3
  24. langfun/core/eval/scoring.py +4 -3
  25. langfun/core/eval/v2/__init__.py +1 -0
  26. langfun/core/eval/v2/checkpointing.py +39 -5
  27. langfun/core/eval/v2/checkpointing_test.py +1 -1
  28. langfun/core/eval/v2/eval_test_helper.py +96 -0
  29. langfun/core/eval/v2/evaluation.py +87 -15
  30. langfun/core/eval/v2/evaluation_test.py +9 -3
  31. langfun/core/eval/v2/example.py +45 -39
  32. langfun/core/eval/v2/example_test.py +3 -3
  33. langfun/core/eval/v2/experiment.py +51 -8
  34. langfun/core/eval/v2/metric_values.py +31 -3
  35. langfun/core/eval/v2/metric_values_test.py +32 -0
  36. langfun/core/eval/v2/metrics.py +157 -44
  37. langfun/core/eval/v2/metrics_test.py +39 -18
  38. langfun/core/eval/v2/progress.py +30 -1
  39. langfun/core/eval/v2/progress_test.py +27 -0
  40. langfun/core/eval/v2/progress_tracking_test.py +3 -0
  41. langfun/core/eval/v2/reporting.py +90 -71
  42. langfun/core/eval/v2/reporting_test.py +20 -6
  43. langfun/core/eval/v2/runners/__init__.py +26 -0
  44. langfun/core/eval/v2/{runners.py → runners/base.py} +22 -124
  45. langfun/core/eval/v2/runners/debug.py +40 -0
  46. langfun/core/eval/v2/runners/debug_test.py +79 -0
  47. langfun/core/eval/v2/runners/parallel.py +100 -0
  48. langfun/core/eval/v2/runners/parallel_test.py +98 -0
  49. langfun/core/eval/v2/runners/sequential.py +47 -0
  50. langfun/core/eval/v2/runners/sequential_test.py +175 -0
  51. langfun/core/langfunc.py +45 -130
  52. langfun/core/langfunc_test.py +6 -4
  53. langfun/core/language_model.py +103 -16
  54. langfun/core/language_model_test.py +9 -3
  55. langfun/core/llms/__init__.py +7 -1
  56. langfun/core/llms/anthropic.py +157 -2
  57. langfun/core/llms/azure_openai.py +29 -17
  58. langfun/core/llms/cache/base.py +25 -3
  59. langfun/core/llms/cache/in_memory.py +48 -7
  60. langfun/core/llms/cache/in_memory_test.py +14 -4
  61. langfun/core/llms/compositional.py +25 -1
  62. langfun/core/llms/deepseek.py +30 -2
  63. langfun/core/llms/fake.py +32 -1
  64. langfun/core/llms/gemini.py +14 -9
  65. langfun/core/llms/google_genai.py +29 -1
  66. langfun/core/llms/groq.py +28 -3
  67. langfun/core/llms/llama_cpp.py +23 -4
  68. langfun/core/llms/openai.py +36 -3
  69. langfun/core/llms/openai_compatible.py +148 -27
  70. langfun/core/llms/openai_compatible_test.py +207 -20
  71. langfun/core/llms/openai_test.py +0 -2
  72. langfun/core/llms/rest.py +12 -1
  73. langfun/core/llms/vertexai.py +51 -8
  74. langfun/core/logging.py +1 -1
  75. langfun/core/mcp/client.py +77 -22
  76. langfun/core/mcp/client_test.py +8 -35
  77. langfun/core/mcp/session.py +94 -29
  78. langfun/core/mcp/session_test.py +54 -0
  79. langfun/core/mcp/tool.py +151 -22
  80. langfun/core/mcp/tool_test.py +197 -0
  81. langfun/core/memory.py +1 -0
  82. langfun/core/message.py +160 -55
  83. langfun/core/message_test.py +65 -81
  84. langfun/core/modalities/__init__.py +8 -0
  85. langfun/core/modalities/audio.py +21 -1
  86. langfun/core/modalities/image.py +19 -1
  87. langfun/core/modalities/mime.py +62 -3
  88. langfun/core/modalities/pdf.py +19 -1
  89. langfun/core/modalities/video.py +21 -1
  90. langfun/core/modality.py +167 -29
  91. langfun/core/modality_test.py +42 -12
  92. langfun/core/natural_language.py +1 -1
  93. langfun/core/sampling.py +4 -4
  94. langfun/core/sampling_test.py +20 -4
  95. langfun/core/structured/__init__.py +2 -24
  96. langfun/core/structured/completion.py +34 -44
  97. langfun/core/structured/completion_test.py +23 -43
  98. langfun/core/structured/description.py +54 -50
  99. langfun/core/structured/function_generation.py +29 -12
  100. langfun/core/structured/mapping.py +81 -37
  101. langfun/core/structured/parsing.py +95 -79
  102. langfun/core/structured/parsing_test.py +0 -3
  103. langfun/core/structured/querying.py +215 -142
  104. langfun/core/structured/querying_test.py +65 -29
  105. langfun/core/structured/schema/__init__.py +48 -0
  106. langfun/core/structured/schema/base.py +664 -0
  107. langfun/core/structured/schema/base_test.py +531 -0
  108. langfun/core/structured/schema/json.py +174 -0
  109. langfun/core/structured/schema/json_test.py +121 -0
  110. langfun/core/structured/schema/python.py +316 -0
  111. langfun/core/structured/schema/python_test.py +410 -0
  112. langfun/core/structured/schema_generation.py +33 -14
  113. langfun/core/structured/scoring.py +47 -36
  114. langfun/core/structured/tokenization.py +26 -11
  115. langfun/core/subscription.py +2 -2
  116. langfun/core/template.py +174 -49
  117. langfun/core/template_test.py +123 -17
  118. langfun/env/__init__.py +8 -2
  119. langfun/env/base_environment.py +320 -128
  120. langfun/env/base_environment_test.py +473 -0
  121. langfun/env/base_feature.py +92 -15
  122. langfun/env/base_feature_test.py +228 -0
  123. langfun/env/base_sandbox.py +84 -361
  124. langfun/env/base_sandbox_test.py +1235 -0
  125. langfun/env/event_handlers/__init__.py +1 -1
  126. langfun/env/event_handlers/chain.py +233 -0
  127. langfun/env/event_handlers/chain_test.py +253 -0
  128. langfun/env/event_handlers/event_logger.py +95 -98
  129. langfun/env/event_handlers/event_logger_test.py +21 -21
  130. langfun/env/event_handlers/metric_writer.py +225 -140
  131. langfun/env/event_handlers/metric_writer_test.py +23 -6
  132. langfun/env/interface.py +854 -40
  133. langfun/env/interface_test.py +112 -2
  134. langfun/env/load_balancers_test.py +23 -2
  135. langfun/env/test_utils.py +126 -84
  136. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/METADATA +1 -1
  137. langfun-0.1.2.dev202511160804.dist-info/RECORD +211 -0
  138. langfun/core/eval/v2/runners_test.py +0 -343
  139. langfun/core/structured/schema.py +0 -987
  140. langfun/core/structured/schema_test.py +0 -982
  141. langfun/env/base_test.py +0 -1481
  142. langfun/env/event_handlers/base.py +0 -350
  143. langfun-0.1.2.dev202510230805.dist-info/RECORD +0 -195
  144. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/WHEEL +0 -0
  145. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/licenses/LICENSE +0 -0
  146. {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/top_level.txt +0 -0
@@ -13,10 +13,10 @@
13
13
  # limitations under the License.
14
14
  """Tests for MCP client."""
15
15
 
16
- import inspect
17
16
  import unittest
18
17
  from langfun.core import async_support
19
18
  from langfun.core import mcp as lf_mcp
19
+ from langfun.core import message as lf_message
20
20
  from mcp.server import fastmcp as fastmcp_lib
21
21
 
22
22
  mcp = fastmcp_lib.FastMCP(host='0.0.0.0', port=1234)
@@ -42,38 +42,11 @@ class McpTest(unittest.TestCase):
42
42
  client = lf_mcp.McpClient.from_fastmcp(mcp)
43
43
  tools = client.list_tools()
44
44
  self.assertEqual(len(tools), 1)
45
- tool_cls = tools['add']
46
- print(tool_cls.python_definition())
47
- self.assertEqual(
48
- tool_cls.python_definition(),
49
- inspect.cleandoc(
50
- '''
51
- Add
52
-
53
- ```python
54
- class Add:
55
- """Adds two integers and returns their sum.
56
-
57
- Args:
58
- a: The first integer.
59
- b: The second integer.
60
-
61
- Returns:
62
- The sum of the two integers.
63
- """
64
- a: int
65
- b: int
66
- ```
67
- '''
68
- )
69
- )
70
- self.assertEqual(repr(tool_cls), '<tool-class \'Add\'>')
71
- self.assertEqual(tool_cls.__name__, 'Add')
72
- self.assertEqual(tool_cls.TOOL_NAME, 'add')
73
- self.assertEqual(tool_cls(a=1, b=2).input_parameters(), {'a': 1, 'b': 2})
74
45
  with client.session() as session:
75
46
  self.assertEqual(
76
- tool_cls(a=1, b=2)(session), 3
47
+ # Test `session.call_tool` method as `tool.__call__` is already tested
48
+ # in `tool_test.py`.
49
+ session.call_tool(tools['add'](a=1, b=2)), 3
77
50
  )
78
51
 
79
52
  def test_async_usages(self):
@@ -86,10 +59,10 @@ class McpTest(unittest.TestCase):
86
59
  self.assertEqual(tool_cls.TOOL_NAME, 'add')
87
60
  async with client.session() as session:
88
61
  self.assertEqual(
89
- (await tool_cls(a=1, b=2).acall(
90
- session, returns_call_result=True))
91
- .structuredContent['result'],
92
- 3
62
+ # Test `session.acall_tool` method as `tool.acall` is already
63
+ # tested in `tool_test.py`.
64
+ await session.acall_tool(tool_cls(a=1, b=2), returns_message=True),
65
+ lf_message.ToolMessage(text='3', result=3)
93
66
  )
94
67
  async_support.invoke_sync(_test)
95
68
 
@@ -26,10 +26,37 @@ from mcp.shared import memory
26
26
 
27
27
 
28
28
  class McpSession:
29
- """Langfun's MCP session.
29
+ """Represents a session for interacting with an MCP server.
30
30
 
31
- Compared to the standard mcp.ClientSession, Langfun's MCP session could be
32
- used both synchronously and asynchronously.
31
+ `McpSession` provides the context for making calls to tools hosted on an
32
+ MCP server. It wraps the standard `mcp.ClientSession` to offer both
33
+ synchronous and asynchronous usage patterns.
34
+
35
+ Sessions are created using `lf.mcp.McpClient.session()` and should be used
36
+ as context managers (either sync or async) to ensure proper initialization
37
+ and teardown of the connection to the server.
38
+
39
+ **Example Sync Usage:**
40
+
41
+ ```python
42
+ import langfun as lf
43
+
44
+ client = lf.mcp.McpClient.from_command(...)
45
+ with client.session() as session:
46
+ tools = session.list_tools()
47
+ result = tools['my_tool'](x=1)(session)
48
+ ```
49
+
50
+ **Example Async Usage:**
51
+
52
+ ```python
53
+ import langfun as lf
54
+
55
+ client = lf.mcp.McpClient.from_url(...)
56
+ async with client.session() as session:
57
+ tools = await session.alist_tools()
58
+ result = await tools['my_tool'](x=1).acall(session)
59
+ ```
33
60
  """
34
61
 
35
62
  def __init__(self, stream) -> None:
@@ -74,11 +101,19 @@ class McpSession:
74
101
  self._session = None
75
102
 
76
103
  def list_tools(self) -> dict[str, Type[mcp_tool.McpTool]]:
77
- """Lists all MCP tools synchronously."""
104
+ """Lists all available tools on the MCP server synchronously.
105
+
106
+ Returns:
107
+ A dictionary mapping tool names to their corresponding `McpTool` classes.
108
+ """
78
109
  return async_support.invoke_sync(self.alist_tools)
79
110
 
80
111
  async def alist_tools(self) -> dict[str, Type[mcp_tool.McpTool]]:
81
- """Lists all MCP tools asynchronously."""
112
+ """Lists all available tools on the MCP server asynchronously.
113
+
114
+ Returns:
115
+ A dictionary mapping tool names to their corresponding `McpTool` classes.
116
+ """
82
117
  assert self._session is not None, 'MCP session is not entered.'
83
118
  return {
84
119
  t.name: mcp_tool.McpTool.make_class(t)
@@ -89,34 +124,37 @@ class McpSession:
89
124
  self,
90
125
  tool: mcp_tool.McpTool,
91
126
  *,
92
- returns_call_result: bool = False
127
+ returns_message: bool = False
93
128
  ) -> Any:
94
- """Calls a MCP tool synchronously."""
95
- return async_support.invoke_sync(
96
- self.acall_tool,
97
- tool,
98
- returns_call_result=returns_call_result
99
- )
129
+ """Calls an MCP tool synchronously.
130
+
131
+ Args:
132
+ tool: The `McpTool` instance to call.
133
+ returns_message: If True, the tool call will return an `mcp.Message`
134
+ object; otherwise, it returns the tool's direct result.
135
+
136
+ Returns:
137
+ The result of the tool call.
138
+ """
139
+ return tool(self, returns_message=returns_message)
100
140
 
101
141
  async def acall_tool(
102
142
  self,
103
143
  tool: mcp_tool.McpTool,
104
144
  *,
105
- returns_call_result: bool = False
145
+ returns_message: bool = False
106
146
  ) -> Any:
107
- """Calls a MCP tool asynchronously."""
108
- assert self._session is not None, 'MCP session is not entered.'
109
- tool_call_result = await self._session.call_tool(
110
- tool.TOOL_NAME, tool.input_parameters()
111
- )
112
- if returns_call_result:
113
- return tool_call_result
114
- if (
115
- tool_call_result.structuredContent
116
- and 'result' in tool_call_result.structuredContent
117
- ):
118
- return tool_call_result.structuredContent['result']
119
- return tool_call_result.content
147
+ """Calls an MCP tool asynchronously.
148
+
149
+ Args:
150
+ tool: The `McpTool` instance to call.
151
+ returns_message: If True, the tool call will return an `mcp.Message`
152
+ object; otherwise, it returns the tool's direct result.
153
+
154
+ Returns:
155
+ The result of the tool call.
156
+ """
157
+ return await tool.acall(self, returns_message=returns_message)
120
158
 
121
159
  @classmethod
122
160
  def from_command(
@@ -124,7 +162,15 @@ class McpSession:
124
162
  command: str,
125
163
  args: list[str] | None = None
126
164
  ) -> 'McpSession':
127
- """Creates a MCP session from a command."""
165
+ """Creates an MCP session from a command-line executable.
166
+
167
+ Args:
168
+ command: The command to execute.
169
+ args: An optional list of arguments to pass to the command.
170
+
171
+ Returns:
172
+ An `McpSession` instance.
173
+ """
128
174
  return cls(
129
175
  mcp.stdio_client(
130
176
  mcp.StdioServerParameters(command=command, args=args or [])
@@ -137,7 +183,18 @@ class McpSession:
137
183
  url: str,
138
184
  headers: dict[str, str] | None = None
139
185
  ) -> 'McpSession':
140
- """Creates a MCP session from a URL."""
186
+ """Creates an MCP session from an HTTP URL.
187
+
188
+ The transport protocol (e.g., 'mcp' or 'sse') is inferred from the
189
+ last part of the URL path.
190
+
191
+ Args:
192
+ url: The URL of the MCP server.
193
+ headers: An optional dictionary of HTTP headers to include in requests.
194
+
195
+ Returns:
196
+ An `McpSession` instance.
197
+ """
141
198
  transport = url.removesuffix('/').split('/')[-1].lower()
142
199
  if transport == 'mcp':
143
200
  return cls(streamable_http.streamablehttp_client(url, headers or {}))
@@ -151,12 +208,20 @@ class McpSession:
151
208
  cls,
152
209
  fastmcp: fastmcp_lib.FastMCP
153
210
  ):
211
+ """Creates an MCP session from an in-memory FastMCP instance.
212
+
213
+ Args:
214
+ fastmcp: An instance of `fastmcp_lib.FastMCP`.
215
+
216
+ Returns:
217
+ An `McpSession` instance.
218
+ """
154
219
  return cls(_client_streams_from_fastmcp(fastmcp))
155
220
 
156
221
 
157
222
  @contextlib.asynccontextmanager
158
223
  async def _client_streams_from_fastmcp(fastmcp: fastmcp_lib.FastMCP):
159
- """Creates client streams from a MCP server."""
224
+ """Creates client streams from an in-memory FastMCP instance."""
160
225
  server = fastmcp._mcp_server # pylint: disable=protected-access
161
226
  async with memory.create_client_server_memory_streams(
162
227
  ) as (client_streams, server_streams):
@@ -0,0 +1,54 @@
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
+ """Tests for MCP session."""
15
+
16
+ import unittest
17
+ from unittest import mock
18
+
19
+ from langfun.core.mcp import session as mcp_session
20
+ import mcp
21
+ from mcp.client import sse
22
+ from mcp.client import streamable_http
23
+
24
+
25
+ class McpSessionTest(unittest.TestCase):
26
+
27
+ @mock.patch.object(mcp, 'stdio_client', autospec=True)
28
+ def test_from_command(self, mock_stdio_client):
29
+ mcp_session.McpSession.from_command('my-command', ['--foo'])
30
+ mock_stdio_client.assert_called_once_with(
31
+ mcp.StdioServerParameters(command='my-command', args=['--foo'])
32
+ )
33
+
34
+ @mock.patch.object(streamable_http, 'streamablehttp_client', autospec=True)
35
+ def test_from_url_mcp(self, mock_streamablehttp_client):
36
+ mcp_session.McpSession.from_url(
37
+ 'http://localhost/mcp', headers={'k': 'v'}
38
+ )
39
+ mock_streamablehttp_client.assert_called_once_with(
40
+ 'http://localhost/mcp', {'k': 'v'}
41
+ )
42
+
43
+ @mock.patch.object(sse, 'sse_client', autospec=True)
44
+ def test_from_url_sse(self, mock_sse_client):
45
+ mcp_session.McpSession.from_url('http://localhost/sse', headers={'k': 'v'})
46
+ mock_sse_client.assert_called_once_with('http://localhost/sse', {'k': 'v'})
47
+
48
+ def test_from_url_unsupported(self):
49
+ with self.assertRaisesRegex(ValueError, 'Unsupported transport: foo'):
50
+ mcp_session.McpSession.from_url('http://localhost/foo')
51
+
52
+
53
+ if __name__ == '__main__':
54
+ unittest.main()
langfun/core/mcp/tool.py CHANGED
@@ -13,8 +13,12 @@
13
13
  # limitations under the License.
14
14
  """MCP tool."""
15
15
 
16
+ import base64
16
17
  from typing import Annotated, Any, ClassVar
17
18
 
19
+ from langfun.core import async_support
20
+ from langfun.core import message as lf_message
21
+ from langfun.core import modalities as lf_modalities
18
22
  from langfun.core.structured import schema as lf_schema
19
23
  import mcp
20
24
  import pyglove as pg
@@ -27,7 +31,31 @@ class _McpToolMeta(pg.symbolic.ObjectMeta):
27
31
 
28
32
 
29
33
  class McpTool(pg.Object, metaclass=_McpToolMeta):
30
- """Base class for MCP tools."""
34
+ """Represents a tool available on an MCP server.
35
+
36
+ `McpTool` is the base class for all tool proxies generated from an MCP
37
+ server's tool definitions. Users do not typically subclass `McpTool` directly.
38
+ Instead, tool classes are obtained by calling `lf.mcp.McpClient.list_tools()`
39
+ or `lf.mcp.McpSession.list_tools()`.
40
+
41
+ Once a tool class is obtained, it can be instantiated with input parameters
42
+ and called via an `McpSession` to execute the tool on the server.
43
+
44
+ **Example Usage:**
45
+
46
+ ```python
47
+ import langfun as lf
48
+
49
+ client = lf.mcp.McpClient.from_command(...)
50
+ with client.session() as session:
51
+ # List tools and get the 'math' tool class.
52
+ math_tool_cls = session.list_tools()['math']
53
+
54
+ # Instantiate the tool with parameters and call it.
55
+ result = math_tool_cls(x=1, y=2, op='+')(session)
56
+ print(result)
57
+ ```
58
+ """
31
59
 
32
60
  TOOL_NAME: Annotated[
33
61
  ClassVar[str],
@@ -36,42 +64,122 @@ class McpTool(pg.Object, metaclass=_McpToolMeta):
36
64
 
37
65
  @classmethod
38
66
  def python_definition(cls, markdown: bool = True) -> str:
39
- """Returns the Python definition of the tool."""
40
- return lf_schema.Schema.from_value(cls).schema_str(
41
- protocol='python', markdown=markdown
42
- )
67
+ """Returns the Python definition of this tool's input schema.
68
+
69
+ This is useful for generating prompts that instruct a language model
70
+ on how to use the tool.
71
+
72
+ Args:
73
+ markdown: If True, formats the output as a Markdown code block.
74
+
75
+ Returns:
76
+ A string containing the Python definition of the tool's input schema.
77
+ """
78
+ return lf_schema.Schema.from_value(cls).schema_repr(markdown=markdown)
79
+
80
+ @classmethod
81
+ def result_to_message(
82
+ cls, result: mcp.types.CallToolResult
83
+ ) -> lf_message.ToolMessage:
84
+ """Converts an `mcp.types.CallToolResult` to an `lf.ToolMessage`.
85
+
86
+ This method translates results from the MCP protocol, including text,
87
+ image, and audio content, into a Langfun `ToolMessage`, making it easy
88
+ to integrate tool results into Langfun workflows.
89
+
90
+ Args:
91
+ result: The `mcp.types.CallToolResult` object to convert.
92
+
93
+ Returns:
94
+ An `lf.ToolMessage` instance representing the tool result.
95
+ """
96
+ chunks = []
97
+ for item in result.content:
98
+ if isinstance(item, mcp.types.TextContent):
99
+ chunk = item.text
100
+ elif isinstance(item, mcp.types.ImageContent):
101
+ chunk = lf_modalities.Image.from_bytes(_base64_decode(item.data))
102
+ elif isinstance(item, mcp.types.AudioContent):
103
+ chunk = lf_modalities.Audio.from_bytes(_base64_decode(item.data))
104
+ else:
105
+ raise ValueError(f'Unsupported item type: {type(item)}')
106
+ chunks.append(chunk)
107
+ message = lf_message.ToolMessage.from_chunks(chunks)
108
+ if result.structuredContent:
109
+ message.metadata.update(result.structuredContent)
110
+ return message
43
111
 
44
112
  def __call__(
45
113
  self,
46
114
  session,
47
115
  *,
48
- returns_call_result: bool = False) -> Any:
49
- """Calls a MCP tool synchronously.
116
+ returns_message: bool = False) -> Any:
117
+ """Calls the MCP tool synchronously within a given session.
50
118
 
51
119
  Args:
52
- session: A MCP session.
53
- returns_call_result: If True, returns the call result. Otherwise returns
54
- the result from structured content, or return content.
120
+ session: An `McpSession` object.
121
+ returns_message: If True, the raw `lf.ToolMessage` is returned.
122
+ If False(default), the method attempts to return a more specific result:
123
+ - The `result` field of the `ToolMessage` if it's populated.
124
+ - The `ToolMessage` itself if it contains multi-modal content.
125
+ - The `text` field of the `ToolMessage` otherwise.
55
126
 
56
127
  Returns:
57
- The call result, or the result from structured content, or content.
128
+ The result of the tool call, processed according to `returns_message`.
58
129
  """
59
- return session.call_tool(
60
- self,
61
- returns_call_result=returns_call_result
130
+ return async_support.invoke_sync(
131
+ self.acall,
132
+ session,
133
+ returns_message=returns_message
62
134
  )
63
135
 
64
136
  async def acall(
65
137
  self,
66
138
  session,
67
139
  *,
68
- returns_call_result: bool = False) -> Any:
69
- """Calls a MCP tool asynchronously."""
70
- return await session.acall_tool(self, returns_call_result=returns_call_result)
140
+ returns_message: bool = False
141
+ ) -> Any:
142
+ """Calls the MCP tool asynchronously within a given session.
143
+
144
+ Args:
145
+ session: An `McpSession` object or an `mcp.ClientSession`.
146
+ returns_message: If True, the raw `lf.ToolMessage` is returned.
147
+ If False(default), the method attempts to return a more specific result:
148
+ - The `result` field of the `ToolMessage` if it's populated.
149
+ - The `ToolMessage` itself if it contains multi-modal content.
150
+ - The `text` field of the `ToolMessage` otherwise.
151
+
152
+ Returns:
153
+ The result of the tool call, processed according to `returns_message`.
154
+ """
155
+ if not isinstance(session, mcp.ClientSession):
156
+ session = getattr(session, '_session', None)
157
+ assert session is not None, 'MCP session is not entered.'
158
+ tool_call_result = await session.call_tool(
159
+ self.TOOL_NAME, self.input_parameters()
160
+ )
161
+ message = self.result_to_message(tool_call_result)
162
+ if returns_message:
163
+ return message
164
+ if message.result:
165
+ return message.result
166
+ if message.referred_modalities:
167
+ return message
168
+ return message.text
71
169
 
72
170
  def input_parameters(self) -> dict[str, Any]:
73
- """Returns the input parameters of the tool."""
74
- json = self.to_json()
171
+ """Returns the input parameters for the tool call.
172
+
173
+ Returns:
174
+ A dictionary containing the input parameters, formatted for an
175
+ MCP `call_tool` request.
176
+ """
177
+ # Optional fields are represented as fields with default values. Therefore,
178
+ # we need to remove the default values from the JSON representation of the
179
+ # tool.
180
+ json = self.to_json(hide_default_values=True)
181
+
182
+ # Remove the type name key from the JSON representation of the tool.
75
183
  def _transform(path: pg.KeyPath, x: Any) -> Any:
76
184
  del path
77
185
  if isinstance(x, dict):
@@ -81,7 +189,15 @@ class McpTool(pg.Object, metaclass=_McpToolMeta):
81
189
 
82
190
  @classmethod
83
191
  def make_class(cls, tool_definition: mcp.Tool) -> type['McpTool']:
84
- """Makes a MCP tool class from tool definition."""
192
+ """Creates an `McpTool` subclass from an MCP tool definition.
193
+
194
+ Args:
195
+ tool_definition: An `mcp.Tool` object containing the tool's metadata.
196
+
197
+ Returns:
198
+ A dynamically generated class that inherits from `McpTool` and
199
+ represents the defined tool.
200
+ """
85
201
 
86
202
  class _McpTool(cls):
87
203
  auto_schema = False
@@ -104,11 +220,19 @@ class _McpToolInputMeta(pg.symbolic.ObjectMeta):
104
220
 
105
221
 
106
222
  class McpToolInput(pg.Object, metaclass=_McpToolInputMeta):
107
- """Base class for MCP tool inputs."""
223
+ """Base class for generated MCP tool input schemas."""
108
224
 
109
225
  @classmethod
110
226
  def make_class(cls, name: str, schema: pg.Schema):
111
- """Converts a schema to an input class."""
227
+ """Creates an `McpToolInput` subclass from a schema.
228
+
229
+ Args:
230
+ name: The name of the input class to generate.
231
+ schema: A `pg.Schema` object defining the input fields.
232
+
233
+ Returns:
234
+ A dynamically generated class that inherits from `McpToolInput`.
235
+ """
112
236
 
113
237
  class _McpToolInput(cls):
114
238
  pass
@@ -123,3 +247,8 @@ class McpToolInput(pg.Object, metaclass=_McpToolInputMeta):
123
247
  def _snake_to_camel(name: str) -> str:
124
248
  """Converts a snake_case name to a CamelCase name."""
125
249
  return ''.join(x.capitalize() for x in name.split('_'))
250
+
251
+
252
+ def _base64_decode(data: str) -> bytes:
253
+ """Decodes a base64 string."""
254
+ return base64.b64decode(data.encode('utf-8'))