langfun 0.1.2.dev202510160805__py3-none-any.whl → 0.1.2.dev202510180803__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/__init__.py +1 -1
- langfun/core/__init__.py +5 -1
- langfun/core/async_support.py +73 -3
- langfun/core/async_support_test.py +23 -0
- langfun/core/concurrent_test.py +8 -2
- langfun/core/eval/v2/progress_tracking_test.py +2 -2
- langfun/core/mcp/__init__.py +10 -0
- langfun/core/mcp/client.py +122 -0
- langfun/core/mcp/client_test.py +98 -0
- langfun/core/mcp/session.py +176 -0
- langfun/core/mcp/testing/simple_mcp_client.py +33 -0
- langfun/core/mcp/testing/simple_mcp_server.py +33 -0
- langfun/core/mcp/tool.py +125 -0
- {langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/METADATA +7 -3
- {langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/RECORD +18 -11
- {langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/WHEEL +0 -0
- {langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/licenses/LICENSE +0 -0
- {langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/top_level.txt +0 -0
langfun/__init__.py
CHANGED
langfun/core/__init__.py
CHANGED
|
@@ -40,9 +40,13 @@ from langfun.core.component import context
|
|
|
40
40
|
as_context = context
|
|
41
41
|
use_context = context
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Support for async IO.
|
|
44
44
|
from langfun.core.async_support import invoke_async
|
|
45
45
|
|
|
46
|
+
# Adaptors for async function/context manager to sync versions.
|
|
47
|
+
from langfun.core.async_support import invoke_sync
|
|
48
|
+
from langfun.core.async_support import sync_context_manager
|
|
49
|
+
|
|
46
50
|
# Shortcut function for overriding components attributes, usually for
|
|
47
51
|
# override settings.
|
|
48
52
|
from langfun.core.component import use_settings
|
langfun/core/async_support.py
CHANGED
|
@@ -14,15 +14,85 @@
|
|
|
14
14
|
"""Utility for async IO in Langfun."""
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
-
|
|
17
|
+
import contextlib
|
|
18
|
+
from typing import Any, Awaitable, Callable, Iterator
|
|
19
|
+
import anyio
|
|
18
20
|
import pyglove as pg
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
async def invoke_async(
|
|
22
|
-
|
|
24
|
+
sync_callable: Callable[..., Any], *args, **kwargs
|
|
23
25
|
) -> Any:
|
|
24
26
|
"""Invokes a callable asynchronously with `lf.context` manager enabled."""
|
|
25
27
|
return await asyncio.to_thread(
|
|
26
28
|
# Enable `lf.context` manager for async calls.
|
|
27
|
-
pg.with_contextual_override(
|
|
29
|
+
pg.with_contextual_override(sync_callable), *args, **kwargs
|
|
28
30
|
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def invoke_sync(
|
|
34
|
+
async_callable: Callable[..., Awaitable[Any]],
|
|
35
|
+
*args,
|
|
36
|
+
**kwargs
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Invokes a async callable synchronously."""
|
|
39
|
+
async def _invoke():
|
|
40
|
+
return await async_callable(*args, **kwargs)
|
|
41
|
+
invoke_fn = pg.with_contextual_override(_invoke)
|
|
42
|
+
blocking_portal = pg.utils.thread_local_get('__blocking_portal__', None)
|
|
43
|
+
if blocking_portal is None:
|
|
44
|
+
return anyio.run(invoke_fn)
|
|
45
|
+
return blocking_portal.call(invoke_fn)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@contextlib.contextmanager
|
|
49
|
+
def sync_context_manager(
|
|
50
|
+
async_context_manager: contextlib.AbstractAsyncContextManager[Any]
|
|
51
|
+
) -> Iterator[Any]:
|
|
52
|
+
"""Adapts an async context manager to a sync context manager.
|
|
53
|
+
|
|
54
|
+
sync_context_manager installs a blocking portal in current thread to run the
|
|
55
|
+
async context manager in a blocking way. It's useful for running async code in
|
|
56
|
+
sync context managers, e.g. `sync_context_manager` can be nested and share the
|
|
57
|
+
same event loop.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
@contextlib.asynccontextmanager
|
|
63
|
+
async def foo(x):
|
|
64
|
+
try:
|
|
65
|
+
yield x
|
|
66
|
+
finally:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
with lf.sync_context_manager(foo(x)) as x
|
|
70
|
+
with lf.sync_context_manager(foo(y)) as y:
|
|
71
|
+
...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
async_context_manager: The async context manager to adapt.
|
|
76
|
+
|
|
77
|
+
Yields:
|
|
78
|
+
The value yielded by the async context manager.
|
|
79
|
+
"""
|
|
80
|
+
blocking_portal = pg.utils.thread_local_get('__blocking_portal__', None)
|
|
81
|
+
portal_exit_stack = None
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
if blocking_portal is None:
|
|
85
|
+
portal_exit_stack = contextlib.ExitStack()
|
|
86
|
+
blocking_portal = portal_exit_stack.enter_context(
|
|
87
|
+
anyio.from_thread.start_blocking_portal()
|
|
88
|
+
)
|
|
89
|
+
pg.utils.thread_local_set('__blocking_portal__', blocking_portal)
|
|
90
|
+
context_manager = blocking_portal.wrap_async_context_manager(
|
|
91
|
+
async_context_manager
|
|
92
|
+
)
|
|
93
|
+
with context_manager as value:
|
|
94
|
+
yield value
|
|
95
|
+
finally:
|
|
96
|
+
if portal_exit_stack is not None:
|
|
97
|
+
portal_exit_stack.close()
|
|
98
|
+
pg.utils.thread_local_del('__blocking_portal__')
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
|
+
import contextlib
|
|
16
17
|
import time
|
|
17
18
|
import unittest
|
|
18
19
|
|
|
@@ -34,6 +35,28 @@ class AsyncSupportTest(unittest.TestCase):
|
|
|
34
35
|
with pg.contextual_override(z=3):
|
|
35
36
|
self.assertEqual(asyncio.run(r), 6)
|
|
36
37
|
|
|
38
|
+
def test_invoke_sync(self):
|
|
39
|
+
@contextlib.asynccontextmanager
|
|
40
|
+
async def bar(x):
|
|
41
|
+
try:
|
|
42
|
+
yield x
|
|
43
|
+
finally:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
async def foo(x, *, y):
|
|
47
|
+
time.sleep(2)
|
|
48
|
+
return x + y + pg.contextual_value('z', 0)
|
|
49
|
+
|
|
50
|
+
with pg.contextual_override(z=3):
|
|
51
|
+
with async_support.sync_context_manager(bar(1)) as x:
|
|
52
|
+
self.assertEqual(x, 1)
|
|
53
|
+
with async_support.sync_context_manager(bar(2)) as y:
|
|
54
|
+
self.assertEqual(y, 2)
|
|
55
|
+
self.assertEqual(async_support.invoke_sync(foo, 1, y=2), 6)
|
|
56
|
+
|
|
57
|
+
with pg.contextual_override(z=2):
|
|
58
|
+
self.assertEqual(async_support.invoke_sync(foo, 1, y=2), 5)
|
|
59
|
+
|
|
37
60
|
|
|
38
61
|
if __name__ == '__main__':
|
|
39
62
|
unittest.main()
|
langfun/core/concurrent_test.py
CHANGED
|
@@ -262,6 +262,7 @@ class ProgressControlTest(unittest.TestCase):
|
|
|
262
262
|
with contextlib.redirect_stderr(string_io):
|
|
263
263
|
ctrl.update(1)
|
|
264
264
|
ctrl.refresh()
|
|
265
|
+
sys.stderr.flush()
|
|
265
266
|
self.assertEqual(string_io.getvalue(), '')
|
|
266
267
|
concurrent.progress_bar = 'tqdm'
|
|
267
268
|
|
|
@@ -274,6 +275,7 @@ class ProgressControlTest(unittest.TestCase):
|
|
|
274
275
|
ctrl.set_status('bar')
|
|
275
276
|
ctrl.update(10)
|
|
276
277
|
ctrl.refresh()
|
|
278
|
+
sys.stderr.flush()
|
|
277
279
|
self.assertEqual(
|
|
278
280
|
string_io.getvalue(),
|
|
279
281
|
'\x1b[1m\x1b[31mfoo\x1b[0m: \x1b[34m10% (10/100)\x1b[0m : bar\n'
|
|
@@ -288,6 +290,7 @@ class ProgressControlTest(unittest.TestCase):
|
|
|
288
290
|
self.assertIsInstance(ctrl, concurrent._TqdmProgressControl)
|
|
289
291
|
ctrl.update(10)
|
|
290
292
|
ctrl.refresh()
|
|
293
|
+
sys.stderr.flush()
|
|
291
294
|
self.assertIn('10/100', string_io.getvalue())
|
|
292
295
|
|
|
293
296
|
tqdm = concurrent.tqdm
|
|
@@ -316,6 +319,7 @@ class ProgressBarTest(unittest.TestCase):
|
|
|
316
319
|
for _ in concurrent.concurrent_execute(fun, range(5)):
|
|
317
320
|
concurrent.ProgressBar.refresh()
|
|
318
321
|
concurrent.ProgressBar.uninstall(bar_id)
|
|
322
|
+
sys.stderr.flush()
|
|
319
323
|
output_str = string_io.getvalue()
|
|
320
324
|
self.assertIn('100%', output_str)
|
|
321
325
|
self.assertIn('5/5', output_str)
|
|
@@ -332,7 +336,7 @@ class ProgressBarTest(unittest.TestCase):
|
|
|
332
336
|
concurrent.ProgressBar.update(bar_id, 0, status=1)
|
|
333
337
|
concurrent.ProgressBar.uninstall(bar_id)
|
|
334
338
|
sys.stderr.flush()
|
|
335
|
-
|
|
339
|
+
time.sleep(1)
|
|
336
340
|
self.assertIn('1/4', string_io.getvalue())
|
|
337
341
|
# TODO(daiyip): Re-enable once flakiness is fixed.
|
|
338
342
|
# self.assertIn('2/4', string_io.getvalue())
|
|
@@ -564,7 +568,8 @@ class ConcurrentMapTest(unittest.TestCase):
|
|
|
564
568
|
fun, [1, 2, 3], timeout=1.5, max_workers=1, show_progress=True
|
|
565
569
|
)
|
|
566
570
|
], key=lambda x: x[0])
|
|
567
|
-
|
|
571
|
+
sys.stderr.flush()
|
|
572
|
+
|
|
568
573
|
self.assertEqual( # pylint: disable=g-generic-assert
|
|
569
574
|
output,
|
|
570
575
|
[
|
|
@@ -592,6 +597,7 @@ class ConcurrentMapTest(unittest.TestCase):
|
|
|
592
597
|
show_progress=bar_id, status_fn=lambda p: dict(x=1, y=1)
|
|
593
598
|
)
|
|
594
599
|
], key=lambda x: x[0])
|
|
600
|
+
sys.stderr.flush()
|
|
595
601
|
|
|
596
602
|
self.assertEqual( # pylint: disable=g-generic-assert
|
|
597
603
|
output,
|
|
@@ -50,7 +50,7 @@ class TqdmProgressTrackerTest(unittest.TestCase):
|
|
|
50
50
|
string_io = io.StringIO()
|
|
51
51
|
with contextlib.redirect_stderr(string_io):
|
|
52
52
|
_ = experiment.run(root_dir, 'new', plugins=[])
|
|
53
|
-
|
|
53
|
+
sys.stderr.flush()
|
|
54
54
|
self.assertIn('All: 100%', string_io.getvalue())
|
|
55
55
|
|
|
56
56
|
def test_with_example_ids(self):
|
|
@@ -61,7 +61,7 @@ class TqdmProgressTrackerTest(unittest.TestCase):
|
|
|
61
61
|
string_io = io.StringIO()
|
|
62
62
|
with contextlib.redirect_stderr(string_io):
|
|
63
63
|
_ = experiment.run(root_dir, 'new', example_ids=[1], plugins=[])
|
|
64
|
-
|
|
64
|
+
sys.stderr.flush()
|
|
65
65
|
self.assertIn('All: 100%', string_io.getvalue())
|
|
66
66
|
|
|
67
67
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Langfun MCP support."""
|
|
2
|
+
|
|
3
|
+
# pylint: disable=g-importing-member
|
|
4
|
+
|
|
5
|
+
from langfun.core.mcp.client import McpClient
|
|
6
|
+
from langfun.core.mcp.session import McpSession
|
|
7
|
+
from langfun.core.mcp.tool import McpTool
|
|
8
|
+
from langfun.core.mcp.tool import McpToolInput
|
|
9
|
+
|
|
10
|
+
# pylint: enable=g-importing-member
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
"""MCP client."""
|
|
15
|
+
|
|
16
|
+
import abc
|
|
17
|
+
from typing import Annotated, Type
|
|
18
|
+
|
|
19
|
+
from langfun.core.mcp import session as mcp_session
|
|
20
|
+
from langfun.core.mcp import tool as mcp_tool
|
|
21
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
22
|
+
import pyglove as pg
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class McpClient(pg.Object):
|
|
26
|
+
"""Base class for MCP client.
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
|
|
32
|
+
def tool_use():
|
|
33
|
+
client = lf.mcp.McpClient.from_command('<MCP_CMD>', ['<ARG1>', 'ARG2'])
|
|
34
|
+
tools = client.list_tools()
|
|
35
|
+
tool_cls = tools['<TOOL_NAME>']
|
|
36
|
+
|
|
37
|
+
# Print the python definition of the tool.
|
|
38
|
+
print(tool_cls.python_definition())
|
|
39
|
+
|
|
40
|
+
with client.session() as session:
|
|
41
|
+
return tool_cls(x=1, y=2)(session)
|
|
42
|
+
|
|
43
|
+
async def tool_use_async_version():
|
|
44
|
+
client = lf.mcp.McpClient.from_url('http://localhost:8000/mcp')
|
|
45
|
+
tools = client.list_tools()
|
|
46
|
+
tool_cls = tools['<TOOL_NAME>']
|
|
47
|
+
|
|
48
|
+
# Print the python definition of the tool.
|
|
49
|
+
print(tool_cls.python_definition())
|
|
50
|
+
|
|
51
|
+
async with client.session() as session:
|
|
52
|
+
return await tool_cls(x=1, y=2).acall(session)
|
|
53
|
+
```
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def _on_bound(self):
|
|
57
|
+
super()._on_bound()
|
|
58
|
+
self._tools = None
|
|
59
|
+
|
|
60
|
+
def list_tools(
|
|
61
|
+
self, refresh: bool = False
|
|
62
|
+
) -> dict[str, Type[mcp_tool.McpTool]]:
|
|
63
|
+
"""Lists all MCP tools."""
|
|
64
|
+
if self._tools is None or refresh:
|
|
65
|
+
with self.session() as session:
|
|
66
|
+
self._tools = session.list_tools()
|
|
67
|
+
return self._tools
|
|
68
|
+
|
|
69
|
+
@abc.abstractmethod
|
|
70
|
+
def session(self) -> mcp_session.McpSession:
|
|
71
|
+
"""Creates a MCP session."""
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_command(cls, command: str, args: list[str]) -> 'McpClient':
|
|
75
|
+
"""Creates a MCP client from a tool."""
|
|
76
|
+
return _StdioMcpClient(command=command, args=args)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_url(
|
|
80
|
+
cls,
|
|
81
|
+
url: str,
|
|
82
|
+
headers: dict[str, str] | None = None
|
|
83
|
+
) -> 'McpClient':
|
|
84
|
+
"""Creates a MCP client from a URL."""
|
|
85
|
+
return _HttpMcpClient(url=url, headers=headers or {})
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_fastmcp(cls, fastmcp: fastmcp_lib.FastMCP) -> 'McpClient':
|
|
89
|
+
"""Creates a MCP client from a MCP server."""
|
|
90
|
+
return _InMemoryFastMcpClient(fastmcp=fastmcp)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class _StdioMcpClient(McpClient):
|
|
94
|
+
"""Stdio-based MCP client."""
|
|
95
|
+
|
|
96
|
+
command: Annotated[str, 'Command to execute.']
|
|
97
|
+
args: Annotated[list[str], 'Arguments to pass to the command.']
|
|
98
|
+
|
|
99
|
+
def session(self) -> mcp_session.McpSession:
|
|
100
|
+
"""Creates a MCP session."""
|
|
101
|
+
return mcp_session.McpSession.from_command(self.command, self.args)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class _HttpMcpClient(McpClient):
|
|
105
|
+
"""Server-Sent Events (SSE)/Streamable HTTP-based MCP client."""
|
|
106
|
+
|
|
107
|
+
url: Annotated[str, 'URL to connect to.']
|
|
108
|
+
headers: Annotated[dict[str, str], 'Headers to send with the request.'] = {}
|
|
109
|
+
|
|
110
|
+
def session(self) -> mcp_session.McpSession:
|
|
111
|
+
"""Creates a MCP session."""
|
|
112
|
+
return mcp_session.McpSession.from_url(self.url, self.headers)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _InMemoryFastMcpClient(McpClient):
|
|
116
|
+
"""In-memory MCP client."""
|
|
117
|
+
|
|
118
|
+
fastmcp: Annotated[fastmcp_lib.FastMCP, 'MCP server to connect to.']
|
|
119
|
+
|
|
120
|
+
def session(self) -> mcp_session.McpSession:
|
|
121
|
+
"""Creates a MCP session."""
|
|
122
|
+
return mcp_session.McpSession.from_fastmcp(self.fastmcp)
|
|
@@ -0,0 +1,98 @@
|
|
|
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 client."""
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
import unittest
|
|
18
|
+
from langfun.core import async_support
|
|
19
|
+
from langfun.core import mcp as lf_mcp
|
|
20
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
21
|
+
|
|
22
|
+
mcp = fastmcp_lib.FastMCP(host='0.0.0.0', port=1234)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
async def add(a: int, b: int) -> int:
|
|
27
|
+
"""Adds two integers and returns their sum.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
a: The first integer.
|
|
31
|
+
b: The second integer.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The sum of the two integers.
|
|
35
|
+
"""
|
|
36
|
+
return a + b
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class McpTest(unittest.TestCase):
|
|
40
|
+
|
|
41
|
+
def test_sync_usages(self):
|
|
42
|
+
client = lf_mcp.McpClient.from_fastmcp(mcp)
|
|
43
|
+
tools = client.list_tools()
|
|
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
|
+
with client.session() as session:
|
|
75
|
+
self.assertEqual(
|
|
76
|
+
tool_cls(a=1, b=2)(session), 3
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def test_async_usages(self):
|
|
80
|
+
async def _test():
|
|
81
|
+
client = lf_mcp.McpClient.from_fastmcp(mcp)
|
|
82
|
+
tools = client.list_tools()
|
|
83
|
+
self.assertEqual(len(tools), 1)
|
|
84
|
+
tool_cls = tools['add']
|
|
85
|
+
self.assertEqual(tool_cls.__name__, 'Add')
|
|
86
|
+
self.assertEqual(tool_cls.TOOL_NAME, 'add')
|
|
87
|
+
async with client.session() as session:
|
|
88
|
+
self.assertEqual(
|
|
89
|
+
(await tool_cls(a=1, b=2).acall(
|
|
90
|
+
session, returns_call_result=True))
|
|
91
|
+
.structuredContent['result'],
|
|
92
|
+
3
|
|
93
|
+
)
|
|
94
|
+
async_support.invoke_sync(_test)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == '__main__':
|
|
98
|
+
unittest.main()
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
"""MCP session."""
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
from typing import Any, Type
|
|
18
|
+
import anyio
|
|
19
|
+
from langfun.core import async_support
|
|
20
|
+
from langfun.core.mcp import tool as mcp_tool
|
|
21
|
+
import mcp
|
|
22
|
+
from mcp.client import sse
|
|
23
|
+
from mcp.client import streamable_http
|
|
24
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
25
|
+
from mcp.shared import memory
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpSession:
|
|
29
|
+
"""Langfun's MCP session.
|
|
30
|
+
|
|
31
|
+
Compared to the standard mcp.ClientSession, Langfun's MCP session could be
|
|
32
|
+
used both synchronously and asynchronously.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, stream) -> None:
|
|
36
|
+
self._stream = stream
|
|
37
|
+
self._session = None
|
|
38
|
+
self._session_exit_stack = None
|
|
39
|
+
self._in_session = False
|
|
40
|
+
|
|
41
|
+
# For supporting sync context manager.
|
|
42
|
+
self._sync_context_manager_exit_stack = None
|
|
43
|
+
|
|
44
|
+
def __enter__(self) -> 'McpSession':
|
|
45
|
+
exit_stack = contextlib.ExitStack()
|
|
46
|
+
exit_stack.enter_context(async_support.sync_context_manager(self))
|
|
47
|
+
self._sync_context_manager_exit_stack = exit_stack
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
51
|
+
assert self._sync_context_manager_exit_stack is not None
|
|
52
|
+
self._sync_context_manager_exit_stack.close()
|
|
53
|
+
|
|
54
|
+
async def __aenter__(self) -> 'McpSession':
|
|
55
|
+
assert self._session_exit_stack is None, 'Session cannot be re-entered.'
|
|
56
|
+
|
|
57
|
+
self._session_exit_stack = contextlib.AsyncExitStack()
|
|
58
|
+
stream_output = await self._session_exit_stack.enter_async_context(
|
|
59
|
+
self._stream
|
|
60
|
+
)
|
|
61
|
+
assert isinstance(stream_output, tuple) and len(stream_output) in (2, 3)
|
|
62
|
+
read, write = stream_output[:2]
|
|
63
|
+
self._session = mcp.ClientSession(read, write)
|
|
64
|
+
await self._session_exit_stack.enter_async_context(self._session)
|
|
65
|
+
await self._session.initialize()
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
69
|
+
del exc_type, exc_val, exc_tb
|
|
70
|
+
if self._session is None:
|
|
71
|
+
return
|
|
72
|
+
assert self._session_exit_stack is not None
|
|
73
|
+
await self._session_exit_stack.aclose()
|
|
74
|
+
self._session = None
|
|
75
|
+
|
|
76
|
+
def list_tools(self) -> dict[str, Type[mcp_tool.McpTool]]:
|
|
77
|
+
"""Lists all MCP tools synchronously."""
|
|
78
|
+
return async_support.invoke_sync(self.alist_tools)
|
|
79
|
+
|
|
80
|
+
async def alist_tools(self) -> dict[str, Type[mcp_tool.McpTool]]:
|
|
81
|
+
"""Lists all MCP tools asynchronously."""
|
|
82
|
+
assert self._session is not None, 'MCP session is not entered.'
|
|
83
|
+
return {
|
|
84
|
+
t.name: mcp_tool.McpTool.make_class(t)
|
|
85
|
+
for t in (await self._session.list_tools()).tools
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def call_tool(
|
|
89
|
+
self,
|
|
90
|
+
tool: mcp_tool.McpTool,
|
|
91
|
+
*,
|
|
92
|
+
returns_call_result: bool = False
|
|
93
|
+
) -> 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
|
+
)
|
|
100
|
+
|
|
101
|
+
async def acall_tool(
|
|
102
|
+
self,
|
|
103
|
+
tool: mcp_tool.McpTool,
|
|
104
|
+
*,
|
|
105
|
+
returns_call_result: bool = False
|
|
106
|
+
) -> 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
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_command(
|
|
123
|
+
cls,
|
|
124
|
+
command: str,
|
|
125
|
+
args: list[str] | None = None
|
|
126
|
+
) -> 'McpSession':
|
|
127
|
+
"""Creates a MCP session from a command."""
|
|
128
|
+
return cls(
|
|
129
|
+
mcp.stdio_client(
|
|
130
|
+
mcp.StdioServerParameters(command=command, args=args or [])
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_url(
|
|
136
|
+
cls,
|
|
137
|
+
url: str,
|
|
138
|
+
headers: dict[str, str] | None = None
|
|
139
|
+
) -> 'McpSession':
|
|
140
|
+
"""Creates a MCP session from a URL."""
|
|
141
|
+
transport = url.removesuffix('/').split('/')[-1].lower()
|
|
142
|
+
if transport == 'mcp':
|
|
143
|
+
return cls(streamable_http.streamablehttp_client(url, headers or {}))
|
|
144
|
+
elif transport == 'sse':
|
|
145
|
+
return cls(sse.sse_client(url, headers or {}))
|
|
146
|
+
else:
|
|
147
|
+
raise ValueError(f'Unsupported transport: {transport}')
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_fastmcp(
|
|
151
|
+
cls,
|
|
152
|
+
fastmcp: fastmcp_lib.FastMCP
|
|
153
|
+
):
|
|
154
|
+
return cls(_client_streams_from_fastmcp(fastmcp))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@contextlib.asynccontextmanager
|
|
158
|
+
async def _client_streams_from_fastmcp(fastmcp: fastmcp_lib.FastMCP):
|
|
159
|
+
"""Creates client streams from a MCP server."""
|
|
160
|
+
server = fastmcp._mcp_server # pylint: disable=protected-access
|
|
161
|
+
async with memory.create_client_server_memory_streams(
|
|
162
|
+
) as (client_streams, server_streams):
|
|
163
|
+
client_read, client_write = client_streams
|
|
164
|
+
server_read, server_write = server_streams
|
|
165
|
+
|
|
166
|
+
# Create a cancel scope for the server task
|
|
167
|
+
async with anyio.create_task_group() as tg:
|
|
168
|
+
tg.start_soon(
|
|
169
|
+
lambda: server.run(
|
|
170
|
+
server_read,
|
|
171
|
+
server_write,
|
|
172
|
+
server.create_initialization_options(),
|
|
173
|
+
raise_exceptions=True,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
yield client_read, client_write
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
"""A simple MCP client for testing."""
|
|
15
|
+
|
|
16
|
+
from absl import app
|
|
17
|
+
from absl import flags
|
|
18
|
+
from langfun.core import mcp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_URL = flags.DEFINE_string(
|
|
22
|
+
'url',
|
|
23
|
+
'http://localhost:8000/mcp',
|
|
24
|
+
'URL of the MCP server.',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(_):
|
|
29
|
+
print(mcp.McpClient.from_url(url=_URL.value).list_tools())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
app.run(main)
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
"""Simple MCP server for testing."""
|
|
15
|
+
|
|
16
|
+
from absl import app as absl_app
|
|
17
|
+
from mcp.server import fastmcp as fastmcp_lib
|
|
18
|
+
|
|
19
|
+
mcp = fastmcp_lib.FastMCP(host='0.0.0.0', port=8000)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
async def add(a: int, b: int) -> int:
|
|
24
|
+
"""Adds two integers and returns their sum."""
|
|
25
|
+
return a + b
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(_):
|
|
29
|
+
mcp.run(transport='streamable-http', mount_path='/mcp')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
absl_app.run(main)
|
langfun/core/mcp/tool.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
"""MCP tool."""
|
|
15
|
+
|
|
16
|
+
from typing import Annotated, Any, ClassVar
|
|
17
|
+
|
|
18
|
+
from langfun.core.structured import schema as lf_schema
|
|
19
|
+
import mcp
|
|
20
|
+
import pyglove as pg
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _McpToolMeta(pg.symbolic.ObjectMeta):
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return f'<tool-class \'{self.__name__}\'>'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class McpTool(pg.Object, metaclass=_McpToolMeta):
|
|
30
|
+
"""Base class for MCP tools."""
|
|
31
|
+
|
|
32
|
+
TOOL_NAME: Annotated[
|
|
33
|
+
ClassVar[str],
|
|
34
|
+
'Tool name.'
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
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
|
+
)
|
|
43
|
+
|
|
44
|
+
def __call__(
|
|
45
|
+
self,
|
|
46
|
+
session,
|
|
47
|
+
*,
|
|
48
|
+
returns_call_result: bool = False) -> Any:
|
|
49
|
+
"""Calls a MCP tool synchronously.
|
|
50
|
+
|
|
51
|
+
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.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The call result, or the result from structured content, or content.
|
|
58
|
+
"""
|
|
59
|
+
return session.call_tool(
|
|
60
|
+
self,
|
|
61
|
+
returns_call_result=returns_call_result
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def acall(
|
|
65
|
+
self,
|
|
66
|
+
session,
|
|
67
|
+
*,
|
|
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)
|
|
71
|
+
|
|
72
|
+
def input_parameters(self) -> dict[str, Any]:
|
|
73
|
+
"""Returns the input parameters of the tool."""
|
|
74
|
+
json = self.to_json()
|
|
75
|
+
def _transform(path: pg.KeyPath, x: Any) -> Any:
|
|
76
|
+
del path
|
|
77
|
+
if isinstance(x, dict):
|
|
78
|
+
x.pop(pg.JSONConvertible.TYPE_NAME_KEY, None)
|
|
79
|
+
return x
|
|
80
|
+
return pg.utils.transform(json, _transform)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def make_class(cls, tool_definition: mcp.Tool) -> type['McpTool']:
|
|
84
|
+
"""Makes a MCP tool class from tool definition."""
|
|
85
|
+
|
|
86
|
+
class _McpTool(cls):
|
|
87
|
+
auto_schema = False
|
|
88
|
+
|
|
89
|
+
tool_cls = _McpTool
|
|
90
|
+
tool_cls.TOOL_NAME = tool_definition.name
|
|
91
|
+
tool_cls.__name__ = _snake_to_camel(tool_definition.name)
|
|
92
|
+
tool_cls.__doc__ = tool_definition.description
|
|
93
|
+
schema = pg.Schema.from_json_schema(
|
|
94
|
+
tool_definition.inputSchema, class_fn=McpToolInput.make_class
|
|
95
|
+
)
|
|
96
|
+
tool_cls.apply_schema(schema)
|
|
97
|
+
return tool_cls
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _McpToolInputMeta(pg.symbolic.ObjectMeta):
|
|
101
|
+
|
|
102
|
+
def __repr__(self) -> str:
|
|
103
|
+
return f'<input-class \'{self.__name__}\'>'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class McpToolInput(pg.Object, metaclass=_McpToolInputMeta):
|
|
107
|
+
"""Base class for MCP tool inputs."""
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def make_class(cls, name: str, schema: pg.Schema):
|
|
111
|
+
"""Converts a schema to an input class."""
|
|
112
|
+
|
|
113
|
+
class _McpToolInput(cls):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
input_cls = _McpToolInput
|
|
117
|
+
input_cls.__name__ = _snake_to_camel(name)
|
|
118
|
+
input_cls.__doc__ = schema.description
|
|
119
|
+
input_cls.apply_schema(schema)
|
|
120
|
+
return input_cls
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _snake_to_camel(name: str) -> str:
|
|
124
|
+
"""Converts a snake_case name to a CamelCase name."""
|
|
125
|
+
return ''.join(x.capitalize() for x in name.split('_'))
|
{langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langfun
|
|
3
|
-
Version: 0.1.2.
|
|
3
|
+
Version: 0.1.2.dev202510180803
|
|
4
4
|
Summary: Langfun: Language as Functions.
|
|
5
5
|
Home-page: https://github.com/google/langfun
|
|
6
6
|
Author: Langfun Authors
|
|
@@ -21,14 +21,18 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
+
Requires-Dist: anyio>=4.7.0
|
|
24
25
|
Requires-Dist: jinja2>=3.1.2
|
|
26
|
+
Requires-Dist: mcp>=1.17.0
|
|
25
27
|
Requires-Dist: puremagic>=1.20
|
|
26
|
-
Requires-Dist: pyglove>=0.
|
|
28
|
+
Requires-Dist: pyglove>=0.5.0.dev202510170226
|
|
27
29
|
Requires-Dist: requests>=2.31.0
|
|
28
30
|
Provides-Extra: all
|
|
31
|
+
Requires-Dist: anyio>=4.7.0; extra == "all"
|
|
29
32
|
Requires-Dist: jinja2>=3.1.2; extra == "all"
|
|
33
|
+
Requires-Dist: mcp>=1.17.0; extra == "all"
|
|
30
34
|
Requires-Dist: puremagic>=1.20; extra == "all"
|
|
31
|
-
Requires-Dist: pyglove>=0.
|
|
35
|
+
Requires-Dist: pyglove>=0.5.0.dev202510170226; extra == "all"
|
|
32
36
|
Requires-Dist: requests>=2.31.0; extra == "all"
|
|
33
37
|
Requires-Dist: google-auth>=2.16.0; extra == "all"
|
|
34
38
|
Requires-Dist: pillow>=10.0.0; extra == "all"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langfun/__init__.py,sha256=
|
|
1
|
+
langfun/__init__.py,sha256=Kx-9_ODBpqqXb3cY5oSS-Sr-ZfuyM59W511ca9VYdRY,2663
|
|
2
2
|
langfun/assistant/capabilities/gui/__init__.py,sha256=7-fWINsczHkEAT1hS4vVHnTW3JC_4PZW0E4HfjClOD8,1290
|
|
3
3
|
langfun/assistant/capabilities/gui/bounding_box_parser.py,sha256=wJSEJvt2WFH0o9C3z9uW_vK0qRAJWbkgX5gBAJU7w6k,5832
|
|
4
4
|
langfun/assistant/capabilities/gui/bounding_box_parser_test.py,sha256=9yBvt7fXT_BFVcOXUSm4K0mLdKRYwk2Y9zaCpYYgBnQ,10934
|
|
@@ -6,13 +6,13 @@ langfun/assistant/capabilities/gui/drawing.py,sha256=8wgol61P7HovLg5EaevRmDPXTFu
|
|
|
6
6
|
langfun/assistant/capabilities/gui/drawing_test.py,sha256=d6LQ1ctG78YRi58UVBdndwyEqTC_ITdk191oA3tASxM,3421
|
|
7
7
|
langfun/assistant/capabilities/gui/location.py,sha256=QYJlY5kUNEwiZFiPYRyFAO7bD2ez4jL5hYn1_AK1ulc,8643
|
|
8
8
|
langfun/assistant/capabilities/gui/location_test.py,sha256=pQUOH1sKuAwjTTYKu615RUnecc_lNrddfvkyxf9297w,9051
|
|
9
|
-
langfun/core/__init__.py,sha256=
|
|
10
|
-
langfun/core/async_support.py,sha256=
|
|
11
|
-
langfun/core/async_support_test.py,sha256=
|
|
9
|
+
langfun/core/__init__.py,sha256=GC4amBXybjSfYVrEQSLFb_KBhH75uD95GKkHNiJfwkY,5011
|
|
10
|
+
langfun/core/async_support.py,sha256=WF4sflm0Q-UHJ8lPtlEo9hSwqXqm1kfaAYVPTyVP3n0,3062
|
|
11
|
+
langfun/core/async_support_test.py,sha256=fMz1ulGrfUCuHp7RtYktuBwpbRN3kCBKnB4LFvaXSAA,1754
|
|
12
12
|
langfun/core/component.py,sha256=g1kQM0bryYYYWVDrSMnHfc74wIBbpfe5_B3s-UIP5GE,3028
|
|
13
13
|
langfun/core/component_test.py,sha256=0CxTgjAud3aj8wBauFhG2FHDqrxCTl4OI4gzQTad-40,9254
|
|
14
14
|
langfun/core/concurrent.py,sha256=zY-pXqlGqss_GI20tM1gXvyW8QepVPUuFNmutcIdhbI,32760
|
|
15
|
-
langfun/core/concurrent_test.py,sha256=
|
|
15
|
+
langfun/core/concurrent_test.py,sha256=KzXOlfR3i_-s_GKBLYrO5-ETCvHoFbFY2o9FEeOeXq4,17818
|
|
16
16
|
langfun/core/console.py,sha256=cLQEf84aDxItA9fStJV22xJch0TqFLNf9hLqwJ0RHmU,2652
|
|
17
17
|
langfun/core/console_test.py,sha256=pBOcuNMJdVELywvroptfcRtJMsegMm3wSlHAL2TdxVk,1679
|
|
18
18
|
langfun/core/langfunc.py,sha256=G50YgoVZ0y1GFw2ev41MlOqr6qa8YakbvNC0h_E0PiA,11140
|
|
@@ -85,7 +85,7 @@ langfun/core/eval/v2/metrics_test.py,sha256=LibZXvWEJDVRY-Mza_bQT-SbmbXCHUnFhL7Z
|
|
|
85
85
|
langfun/core/eval/v2/progress.py,sha256=azZgssQgNdv3IgjKEaQBuGI5ucFDNbdi02P4z_nQ8GE,10292
|
|
86
86
|
langfun/core/eval/v2/progress_test.py,sha256=YU7VHzmy5knPZwj9vpBN3rQQH2tukj9eKHkuBCI62h8,2540
|
|
87
87
|
langfun/core/eval/v2/progress_tracking.py,sha256=zNhNPGlnJnHELEfFpbTMCSXFn8d1IJ57OOYkfFaBFfM,6097
|
|
88
|
-
langfun/core/eval/v2/progress_tracking_test.py,sha256=
|
|
88
|
+
langfun/core/eval/v2/progress_tracking_test.py,sha256=MC7hD-KfxqSIwKK_BN7oGx7HA8O3_fY-Y3cYYAhZzxE,2343
|
|
89
89
|
langfun/core/eval/v2/reporting.py,sha256=yUIPCAMnp7InIzpv1DDWrcLO-75iiOUTpscj7smkfrA,8335
|
|
90
90
|
langfun/core/eval/v2/reporting_test.py,sha256=CMK-vwho8cNRJwlbkCqm_v5fykE7Y3V6SaIOCY0CDyA,5671
|
|
91
91
|
langfun/core/eval/v2/runners.py,sha256=bEniZDNu44AQgvqpwLsvBU4V_7WltAe-NPhYgIsLj1E,16848
|
|
@@ -121,6 +121,13 @@ langfun/core/llms/cache/__init__.py,sha256=QAo3InUMDM_YpteNnVCSejI4zOsnjSMWKJKzk
|
|
|
121
121
|
langfun/core/llms/cache/base.py,sha256=rt3zwmyw0y9jsSGW-ZbV1vAfLxQ7_3AVk0l2EySlse4,3918
|
|
122
122
|
langfun/core/llms/cache/in_memory.py,sha256=i58oiQL28RDsq37dwqgVpC2mBETJjIEFS20yHiV5MKU,5185
|
|
123
123
|
langfun/core/llms/cache/in_memory_test.py,sha256=V2UPeu5co5vUwSkjekCH1B1iLm9qQKPaacvP6VW3GTg,10388
|
|
124
|
+
langfun/core/mcp/__init__.py,sha256=cyVP_YTjOmbjhYg8BE7-RnE4Txt8wDukYC0anhHKpuo,286
|
|
125
|
+
langfun/core/mcp/client.py,sha256=NBo4W2fdvYyywlUHynAbyh7LNohILccTkCiisj-UEEg,3642
|
|
126
|
+
langfun/core/mcp/client_test.py,sha256=Vjbh7bXM8xXMMe_QOnFJ4rE0wITovAZbwk7Nu-cqf8g,2766
|
|
127
|
+
langfun/core/mcp/session.py,sha256=dyuNXpLvM0TgLoUmWEwy5XCRf-IK7H_BbcNI2oFa_ek,5626
|
|
128
|
+
langfun/core/mcp/tool.py,sha256=eap46OK4lrw5G5uCwPi4kUMNdaN1Bgy0cUi0bdLJI5Q,3607
|
|
129
|
+
langfun/core/mcp/testing/simple_mcp_client.py,sha256=U5pUXa0SnB2-DdRI2vPhrdVUv14dX_OeS4mPLFpllMc,924
|
|
130
|
+
langfun/core/mcp/testing/simple_mcp_server.py,sha256=dw4t6ERWMdmi6kDE38RU5oYu5MQbEX-GJ6CMxGcV-WE,994
|
|
124
131
|
langfun/core/memories/__init__.py,sha256=HpghfZ-w1NQqzJXBx8Lz0daRhB2rcy2r9Xm491SBhC4,773
|
|
125
132
|
langfun/core/memories/conversation_history.py,sha256=KR78PurXTSeqsRK9QG9xM7-f245rs20EvWO7Mm2n2Vw,1827
|
|
126
133
|
langfun/core/memories/conversation_history_test.py,sha256=2kzAq2pUrbR01Z9jhxviIao52JK4JVjr5iGP8pwGxlU,2156
|
|
@@ -181,8 +188,8 @@ langfun/env/event_handlers/event_logger.py,sha256=3dbPjBe53dBgntYHlyLlj_77hVecPS
|
|
|
181
188
|
langfun/env/event_handlers/event_logger_test.py,sha256=PGof3rPllNnyzs3Yp8kaOHLeTkVrzUgCJwlODTrVRKI,9111
|
|
182
189
|
langfun/env/event_handlers/metric_writer.py,sha256=NgJKsd6xWOtEd0IjYi7coGEaqGYkkPcDjXN9CQ3vxPU,18043
|
|
183
190
|
langfun/env/event_handlers/metric_writer_test.py,sha256=flRqK10wonhJk4idGD_8jjEjrfjgH0R-qcu-7Bj1G5s,5335
|
|
184
|
-
langfun-0.1.2.
|
|
185
|
-
langfun-0.1.2.
|
|
186
|
-
langfun-0.1.2.
|
|
187
|
-
langfun-0.1.2.
|
|
188
|
-
langfun-0.1.2.
|
|
191
|
+
langfun-0.1.2.dev202510180803.dist-info/licenses/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
|
|
192
|
+
langfun-0.1.2.dev202510180803.dist-info/METADATA,sha256=TKDSGp-jEDTFAdPMimMw8gFja3rQ3LQ4YHqko8-Ga7k,7522
|
|
193
|
+
langfun-0.1.2.dev202510180803.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
194
|
+
langfun-0.1.2.dev202510180803.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
|
|
195
|
+
langfun-0.1.2.dev202510180803.dist-info/RECORD,,
|
|
File without changes
|
{langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{langfun-0.1.2.dev202510160805.dist-info → langfun-0.1.2.dev202510180803.dist-info}/top_level.txt
RENAMED
|
File without changes
|