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.
- langfun/core/__init__.py +1 -0
- langfun/core/agentic/action.py +107 -12
- langfun/core/agentic/action_eval.py +9 -2
- langfun/core/agentic/action_test.py +25 -0
- langfun/core/async_support.py +32 -3
- langfun/core/coding/python/correction.py +19 -9
- langfun/core/coding/python/execution.py +14 -12
- langfun/core/coding/python/generation.py +21 -16
- langfun/core/coding/python/sandboxing.py +23 -3
- langfun/core/component.py +42 -3
- langfun/core/concurrent.py +70 -6
- langfun/core/concurrent_test.py +1 -0
- langfun/core/console.py +1 -1
- langfun/core/data/conversion/anthropic.py +12 -3
- langfun/core/data/conversion/anthropic_test.py +8 -6
- langfun/core/data/conversion/gemini.py +9 -2
- langfun/core/data/conversion/gemini_test.py +12 -9
- langfun/core/data/conversion/openai.py +145 -31
- langfun/core/data/conversion/openai_test.py +161 -17
- langfun/core/eval/base.py +47 -43
- langfun/core/eval/base_test.py +4 -4
- langfun/core/eval/matching.py +5 -2
- langfun/core/eval/patching.py +3 -3
- langfun/core/eval/scoring.py +4 -3
- langfun/core/eval/v2/__init__.py +1 -0
- langfun/core/eval/v2/checkpointing.py +39 -5
- langfun/core/eval/v2/checkpointing_test.py +1 -1
- langfun/core/eval/v2/eval_test_helper.py +96 -0
- langfun/core/eval/v2/evaluation.py +87 -15
- langfun/core/eval/v2/evaluation_test.py +9 -3
- langfun/core/eval/v2/example.py +45 -39
- langfun/core/eval/v2/example_test.py +3 -3
- langfun/core/eval/v2/experiment.py +51 -8
- langfun/core/eval/v2/metric_values.py +31 -3
- langfun/core/eval/v2/metric_values_test.py +32 -0
- langfun/core/eval/v2/metrics.py +157 -44
- langfun/core/eval/v2/metrics_test.py +39 -18
- langfun/core/eval/v2/progress.py +30 -1
- langfun/core/eval/v2/progress_test.py +27 -0
- langfun/core/eval/v2/progress_tracking_test.py +3 -0
- langfun/core/eval/v2/reporting.py +90 -71
- langfun/core/eval/v2/reporting_test.py +20 -6
- langfun/core/eval/v2/runners/__init__.py +26 -0
- langfun/core/eval/v2/{runners.py → runners/base.py} +22 -124
- langfun/core/eval/v2/runners/debug.py +40 -0
- langfun/core/eval/v2/runners/debug_test.py +79 -0
- langfun/core/eval/v2/runners/parallel.py +100 -0
- langfun/core/eval/v2/runners/parallel_test.py +98 -0
- langfun/core/eval/v2/runners/sequential.py +47 -0
- langfun/core/eval/v2/runners/sequential_test.py +175 -0
- langfun/core/langfunc.py +45 -130
- langfun/core/langfunc_test.py +6 -4
- langfun/core/language_model.py +103 -16
- langfun/core/language_model_test.py +9 -3
- langfun/core/llms/__init__.py +7 -1
- langfun/core/llms/anthropic.py +157 -2
- langfun/core/llms/azure_openai.py +29 -17
- langfun/core/llms/cache/base.py +25 -3
- langfun/core/llms/cache/in_memory.py +48 -7
- langfun/core/llms/cache/in_memory_test.py +14 -4
- langfun/core/llms/compositional.py +25 -1
- langfun/core/llms/deepseek.py +30 -2
- langfun/core/llms/fake.py +32 -1
- langfun/core/llms/gemini.py +14 -9
- langfun/core/llms/google_genai.py +29 -1
- langfun/core/llms/groq.py +28 -3
- langfun/core/llms/llama_cpp.py +23 -4
- langfun/core/llms/openai.py +36 -3
- langfun/core/llms/openai_compatible.py +148 -27
- langfun/core/llms/openai_compatible_test.py +207 -20
- langfun/core/llms/openai_test.py +0 -2
- langfun/core/llms/rest.py +12 -1
- langfun/core/llms/vertexai.py +51 -8
- langfun/core/logging.py +1 -1
- langfun/core/mcp/client.py +77 -22
- langfun/core/mcp/client_test.py +8 -35
- langfun/core/mcp/session.py +94 -29
- langfun/core/mcp/session_test.py +54 -0
- langfun/core/mcp/tool.py +151 -22
- langfun/core/mcp/tool_test.py +197 -0
- langfun/core/memory.py +1 -0
- langfun/core/message.py +160 -55
- langfun/core/message_test.py +65 -81
- langfun/core/modalities/__init__.py +8 -0
- langfun/core/modalities/audio.py +21 -1
- langfun/core/modalities/image.py +19 -1
- langfun/core/modalities/mime.py +62 -3
- langfun/core/modalities/pdf.py +19 -1
- langfun/core/modalities/video.py +21 -1
- langfun/core/modality.py +167 -29
- langfun/core/modality_test.py +42 -12
- langfun/core/natural_language.py +1 -1
- langfun/core/sampling.py +4 -4
- langfun/core/sampling_test.py +20 -4
- langfun/core/structured/__init__.py +2 -24
- langfun/core/structured/completion.py +34 -44
- langfun/core/structured/completion_test.py +23 -43
- langfun/core/structured/description.py +54 -50
- langfun/core/structured/function_generation.py +29 -12
- langfun/core/structured/mapping.py +81 -37
- langfun/core/structured/parsing.py +95 -79
- langfun/core/structured/parsing_test.py +0 -3
- langfun/core/structured/querying.py +215 -142
- langfun/core/structured/querying_test.py +65 -29
- langfun/core/structured/schema/__init__.py +48 -0
- langfun/core/structured/schema/base.py +664 -0
- langfun/core/structured/schema/base_test.py +531 -0
- langfun/core/structured/schema/json.py +174 -0
- langfun/core/structured/schema/json_test.py +121 -0
- langfun/core/structured/schema/python.py +316 -0
- langfun/core/structured/schema/python_test.py +410 -0
- langfun/core/structured/schema_generation.py +33 -14
- langfun/core/structured/scoring.py +47 -36
- langfun/core/structured/tokenization.py +26 -11
- langfun/core/subscription.py +2 -2
- langfun/core/template.py +174 -49
- langfun/core/template_test.py +123 -17
- langfun/env/__init__.py +8 -2
- langfun/env/base_environment.py +320 -128
- langfun/env/base_environment_test.py +473 -0
- langfun/env/base_feature.py +92 -15
- langfun/env/base_feature_test.py +228 -0
- langfun/env/base_sandbox.py +84 -361
- langfun/env/base_sandbox_test.py +1235 -0
- langfun/env/event_handlers/__init__.py +1 -1
- langfun/env/event_handlers/chain.py +233 -0
- langfun/env/event_handlers/chain_test.py +253 -0
- langfun/env/event_handlers/event_logger.py +95 -98
- langfun/env/event_handlers/event_logger_test.py +21 -21
- langfun/env/event_handlers/metric_writer.py +225 -140
- langfun/env/event_handlers/metric_writer_test.py +23 -6
- langfun/env/interface.py +854 -40
- langfun/env/interface_test.py +112 -2
- langfun/env/load_balancers_test.py +23 -2
- langfun/env/test_utils.py +126 -84
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/METADATA +1 -1
- langfun-0.1.2.dev202511160804.dist-info/RECORD +211 -0
- langfun/core/eval/v2/runners_test.py +0 -343
- langfun/core/structured/schema.py +0 -987
- langfun/core/structured/schema_test.py +0 -982
- langfun/env/base_test.py +0 -1481
- langfun/env/event_handlers/base.py +0 -350
- langfun-0.1.2.dev202510230805.dist-info/RECORD +0 -195
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510230805.dist-info → langfun-0.1.2.dev202511160804.dist-info}/top_level.txt +0 -0
langfun/core/mcp/client_test.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
.
|
|
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
|
|
langfun/core/mcp/session.py
CHANGED
|
@@ -26,10 +26,37 @@ from mcp.shared import memory
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class McpSession:
|
|
29
|
-
"""
|
|
29
|
+
"""Represents a session for interacting with an MCP server.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
127
|
+
returns_message: bool = False
|
|
93
128
|
) -> Any:
|
|
94
|
-
"""Calls
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
145
|
+
returns_message: bool = False
|
|
106
146
|
) -> Any:
|
|
107
|
-
"""Calls
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
"""Calls
|
|
116
|
+
returns_message: bool = False) -> Any:
|
|
117
|
+
"""Calls the MCP tool synchronously within a given session.
|
|
50
118
|
|
|
51
119
|
Args:
|
|
52
|
-
session:
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
128
|
+
The result of the tool call, processed according to `returns_message`.
|
|
58
129
|
"""
|
|
59
|
-
return
|
|
60
|
-
self,
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
74
|
-
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
"""
|
|
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'))
|