langroid 0.52.9__py3-none-any.whl → 0.53.1__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.
- langroid/agent/tools/mcp/__init__.py +22 -0
- langroid/agent/tools/mcp/decorators.py +33 -0
- langroid/agent/tools/mcp/fastmcp_client.py +289 -0
- langroid/language_models/model_info.py +1 -1
- {langroid-0.52.9.dist-info → langroid-0.53.1.dist-info}/METADATA +2 -1
- {langroid-0.52.9.dist-info → langroid-0.53.1.dist-info}/RECORD +8 -5
- {langroid-0.52.9.dist-info → langroid-0.53.1.dist-info}/WHEEL +0 -0
- {langroid-0.52.9.dist-info → langroid-0.53.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
from .decorators import mcp_tool
|
2
|
+
from .fastmcp_client import (
|
3
|
+
FastMCPClient,
|
4
|
+
get_langroid_tool,
|
5
|
+
get_langroid_tool_async,
|
6
|
+
get_langroid_tools,
|
7
|
+
get_langroid_tools_async,
|
8
|
+
get_mcp_tool_async,
|
9
|
+
get_mcp_tools_async,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"mcp_tool",
|
15
|
+
"FastMCPClient",
|
16
|
+
"get_langroid_tool",
|
17
|
+
"get_langroid_tool_async",
|
18
|
+
"get_langroid_tools",
|
19
|
+
"get_langroid_tools_async",
|
20
|
+
"get_mcp_tool_async",
|
21
|
+
"get_mcp_tools_async",
|
22
|
+
]
|
@@ -0,0 +1,33 @@
|
|
1
|
+
from typing import Callable, Type
|
2
|
+
|
3
|
+
from langroid.agent.tool_message import ToolMessage
|
4
|
+
from langroid.agent.tools.mcp.fastmcp_client import get_langroid_tool
|
5
|
+
|
6
|
+
|
7
|
+
def mcp_tool(
|
8
|
+
server: str, tool_name: str
|
9
|
+
) -> Callable[[Type[ToolMessage]], Type[ToolMessage]]:
|
10
|
+
"""Decorator: declare a ToolMessage class bound to a FastMCP tool.
|
11
|
+
|
12
|
+
Usage:
|
13
|
+
@fastmcp_tool("/path/to/server.py", "get_weather")
|
14
|
+
class WeatherTool:
|
15
|
+
def pretty(self) -> str:
|
16
|
+
return f"Temp is {self.temperature}"
|
17
|
+
"""
|
18
|
+
|
19
|
+
def decorator(user_cls: Type[ToolMessage]) -> Type[ToolMessage]:
|
20
|
+
# build the “real” ToolMessage subclass for this server/tool
|
21
|
+
RealTool: Type[ToolMessage] = get_langroid_tool(server, tool_name)
|
22
|
+
|
23
|
+
# copy user‐defined methods / attributes onto RealTool
|
24
|
+
for name, attr in user_cls.__dict__.items():
|
25
|
+
if name.startswith("__") and name.endswith("__"):
|
26
|
+
continue
|
27
|
+
setattr(RealTool, name, attr)
|
28
|
+
|
29
|
+
# preserve the user’s original name if you like:
|
30
|
+
RealTool.__name__ = user_cls.__name__
|
31
|
+
return RealTool
|
32
|
+
|
33
|
+
return decorator
|
@@ -0,0 +1,289 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, cast
|
4
|
+
|
5
|
+
from dotenv import load_dotenv
|
6
|
+
from fastmcp.client import Client
|
7
|
+
from fastmcp.client.transports import ClientTransport
|
8
|
+
from fastmcp.server import FastMCP
|
9
|
+
from mcp.types import CallToolResult, TextContent, Tool
|
10
|
+
|
11
|
+
from langroid.agent.tool_message import ToolMessage
|
12
|
+
from langroid.pydantic_v1 import BaseModel, Field, create_model
|
13
|
+
|
14
|
+
load_dotenv() # load environment variables from .env
|
15
|
+
|
16
|
+
|
17
|
+
class FastMCPClient:
|
18
|
+
"""A client for interacting with a FastMCP server.
|
19
|
+
|
20
|
+
Provides async context manager functionality to safely manage resources.
|
21
|
+
"""
|
22
|
+
|
23
|
+
logger = logging.getLogger(__name__)
|
24
|
+
_cm: Optional[Client] = None
|
25
|
+
client: Optional[Client] = None
|
26
|
+
|
27
|
+
def __init__(self, server: str | FastMCP[Any] | ClientTransport) -> None:
|
28
|
+
"""Initialize the FastMCPClient.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
server: FastMCP server or path to such a server
|
32
|
+
"""
|
33
|
+
self.server = server
|
34
|
+
self.client = None
|
35
|
+
self._cm = None
|
36
|
+
|
37
|
+
async def __aenter__(self) -> "FastMCPClient":
|
38
|
+
"""Enter the async context manager and connect inner client."""
|
39
|
+
# create inner client context manager
|
40
|
+
self._cm = Client(self.server)
|
41
|
+
# actually enter it (opens the session)
|
42
|
+
self.client = await self._cm.__aenter__() # type: ignore
|
43
|
+
return self
|
44
|
+
|
45
|
+
async def connect(self) -> None:
|
46
|
+
"""Open the underlying session."""
|
47
|
+
await self.__aenter__()
|
48
|
+
|
49
|
+
async def close(self) -> None:
|
50
|
+
"""Close the underlying session."""
|
51
|
+
await self.__aexit__(None, None, None)
|
52
|
+
|
53
|
+
async def __aexit__(
|
54
|
+
self,
|
55
|
+
exc_type: Optional[type[Exception]],
|
56
|
+
exc_val: Optional[Exception],
|
57
|
+
exc_tb: Optional[Any],
|
58
|
+
) -> None:
|
59
|
+
"""Exit the async context manager and close inner client."""
|
60
|
+
# exit and close the inner fastmcp.Client
|
61
|
+
if hasattr(self, "_cm"):
|
62
|
+
if self._cm is not None:
|
63
|
+
await self._cm.__aexit__(exc_type, exc_val, exc_tb) # type: ignore
|
64
|
+
self.client = None
|
65
|
+
self._cm = None
|
66
|
+
|
67
|
+
def _schema_to_field(
|
68
|
+
self, name: str, schema: Dict[str, Any], prefix: str
|
69
|
+
) -> Tuple[Any, Any]:
|
70
|
+
"""Convert a JSON Schema snippet into a (type, Field) tuple.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
name: Name of the field.
|
74
|
+
schema: JSON Schema for this field.
|
75
|
+
prefix: Prefix to use for nested model names.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
A tuple of (python_type, Field(...)) for create_model.
|
79
|
+
"""
|
80
|
+
t = schema.get("type")
|
81
|
+
default = schema.get("default", ...)
|
82
|
+
desc = schema.get("description")
|
83
|
+
# Object → nested BaseModel
|
84
|
+
if t == "object" and "properties" in schema:
|
85
|
+
sub_name = f"{prefix}_{name.capitalize()}"
|
86
|
+
sub_fields: Dict[str, Tuple[type, Any]] = {}
|
87
|
+
for k, sub_s in schema["properties"].items():
|
88
|
+
ftype, fld = self._schema_to_field(sub_name + k, sub_s, sub_name)
|
89
|
+
sub_fields[k] = (ftype, fld)
|
90
|
+
submodel = create_model( # type: ignore
|
91
|
+
sub_name,
|
92
|
+
__base__=BaseModel,
|
93
|
+
**sub_fields,
|
94
|
+
)
|
95
|
+
return submodel, Field(default=default, description=desc) # type: ignore
|
96
|
+
# Array → List of items
|
97
|
+
if t == "array" and "items" in schema:
|
98
|
+
item_type, _ = self._schema_to_field(name, schema["items"], prefix)
|
99
|
+
return List[item_type], Field(default=default, description=desc) # type: ignore
|
100
|
+
# Primitive types
|
101
|
+
if t == "string":
|
102
|
+
return str, Field(default=default, description=desc)
|
103
|
+
if t == "integer":
|
104
|
+
return int, Field(default=default, description=desc)
|
105
|
+
if t == "number":
|
106
|
+
return float, Field(default=default, description=desc)
|
107
|
+
if t == "boolean":
|
108
|
+
return bool, Field(default=default, description=desc)
|
109
|
+
# Fallback or unions
|
110
|
+
if any(key in schema for key in ("oneOf", "anyOf", "allOf")):
|
111
|
+
self.logger.warning("Unsupported union schema in field %s; using Any", name)
|
112
|
+
return Any, Field(default=default, description=desc)
|
113
|
+
# Default fallback
|
114
|
+
return Any, Field(default=default, description=desc)
|
115
|
+
|
116
|
+
async def get_langroid_tool(self, tool_name: str) -> Type[ToolMessage]:
|
117
|
+
"""
|
118
|
+
Create a Langroid ToolMessage subclass from the MCP Tool
|
119
|
+
with the given `tool_name`.
|
120
|
+
"""
|
121
|
+
if not self.client:
|
122
|
+
raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
|
123
|
+
target = await self.get_mcp_tool_async(tool_name)
|
124
|
+
if target is None:
|
125
|
+
raise ValueError(f"No tool named {tool_name}")
|
126
|
+
props = target.inputSchema.get("properties", {})
|
127
|
+
fields: Dict[str, Tuple[type, Any]] = {}
|
128
|
+
for fname, schema in props.items():
|
129
|
+
ftype, fld = self._schema_to_field(fname, schema, target.name)
|
130
|
+
fields[fname] = (ftype, fld)
|
131
|
+
|
132
|
+
# Convert target.name to CamelCase and add Tool suffix
|
133
|
+
parts = target.name.replace("-", "_").split("_")
|
134
|
+
camel_case = "".join(part.capitalize() for part in parts)
|
135
|
+
model_name = f"{camel_case}Tool"
|
136
|
+
|
137
|
+
# create Langroid ToolMessage subclass, with expected fields.
|
138
|
+
tool_model = cast(
|
139
|
+
Type[ToolMessage],
|
140
|
+
create_model( # type: ignore[call-overload]
|
141
|
+
model_name,
|
142
|
+
request=(str, target.name),
|
143
|
+
purpose=(str, target.description or f"Use the tool {target.name}"),
|
144
|
+
__base__=ToolMessage,
|
145
|
+
**fields,
|
146
|
+
),
|
147
|
+
)
|
148
|
+
tool_model._server = self.server # type: ignore[attr-defined]
|
149
|
+
|
150
|
+
# 2) define an arg-free call_tool_async()
|
151
|
+
async def call_tool_async(self: ToolMessage) -> Any:
|
152
|
+
from langroid.agent.tools.mcp.fastmcp_client import FastMCPClient
|
153
|
+
|
154
|
+
# pack up the payload
|
155
|
+
payload = self.dict(exclude=self.Config.schema_extra["exclude"])
|
156
|
+
# open a fresh client, call the tool, then close
|
157
|
+
async with FastMCPClient(self.__class__._server) as client: # type: ignore
|
158
|
+
return await client.call_mcp_tool(self.request, payload)
|
159
|
+
|
160
|
+
tool_model.call_tool_async = call_tool_async # type: ignore
|
161
|
+
|
162
|
+
if not hasattr(tool_model, "handle_async"):
|
163
|
+
# 3) define an arg-free handle_async() method
|
164
|
+
# if the tool model doesn't already have one
|
165
|
+
async def handle_async(self: ToolMessage) -> Any:
|
166
|
+
return await self.call_tool_async() # type: ignore[attr-defined]
|
167
|
+
|
168
|
+
# add the handle_async() method to the tool model
|
169
|
+
tool_model.handle_async = handle_async # type: ignore
|
170
|
+
|
171
|
+
return tool_model
|
172
|
+
|
173
|
+
async def get_langroid_tools(self) -> List[Type[ToolMessage]]:
|
174
|
+
"""
|
175
|
+
Get all available tools as Langroid ToolMessage classes,
|
176
|
+
handling nested schemas, with `handle_async` methods
|
177
|
+
"""
|
178
|
+
if not self.client:
|
179
|
+
raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
|
180
|
+
resp = await self.client.list_tools()
|
181
|
+
tools: List[Type[ToolMessage]] = []
|
182
|
+
for t in resp:
|
183
|
+
tools.append(await self.get_langroid_tool(t.name))
|
184
|
+
return tools
|
185
|
+
|
186
|
+
async def get_mcp_tool_async(self, name: str) -> Optional[Tool]:
|
187
|
+
"""Find the "original" MCP Tool (i.e. of type mcp.types.Tool) on the server
|
188
|
+
matching `name`, or None if missing.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
name: Name of the tool to look up.
|
192
|
+
|
193
|
+
Returns:
|
194
|
+
The raw Tool object from the server, or None.
|
195
|
+
"""
|
196
|
+
if not self.client:
|
197
|
+
raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
|
198
|
+
resp: List[Tool] = await self.client.list_tools()
|
199
|
+
return next((t for t in resp if t.name == name), None)
|
200
|
+
|
201
|
+
def _convert_tool_result(
|
202
|
+
self,
|
203
|
+
tool_name: str,
|
204
|
+
result: CallToolResult,
|
205
|
+
) -> List[str] | str | None:
|
206
|
+
if result.isError:
|
207
|
+
self.logger.error(f"Error calling MCP tool {tool_name}")
|
208
|
+
return None
|
209
|
+
has_nontext_results = any(
|
210
|
+
not isinstance(item, TextContent) for item in result.content
|
211
|
+
)
|
212
|
+
if has_nontext_results:
|
213
|
+
self.logger.warning(
|
214
|
+
f"""
|
215
|
+
MCP Tool {tool_name} returned non-text results,
|
216
|
+
which will be skipped.
|
217
|
+
""",
|
218
|
+
)
|
219
|
+
results = [
|
220
|
+
item.text for item in result.content if isinstance(item, TextContent)
|
221
|
+
]
|
222
|
+
if len(results) == 1:
|
223
|
+
return results[0]
|
224
|
+
return results
|
225
|
+
|
226
|
+
async def call_mcp_tool(
|
227
|
+
self, tool_name: str, arguments: Dict[str, Any]
|
228
|
+
) -> str | List[str] | None:
|
229
|
+
"""Call an MCP tool with the given arguments.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
tool_name: Name of the tool to call.
|
233
|
+
arguments: Arguments to pass to the tool.
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
The result of the tool call.
|
237
|
+
"""
|
238
|
+
if not self.client:
|
239
|
+
raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
|
240
|
+
result: CallToolResult = await self.client.session.call_tool(
|
241
|
+
tool_name,
|
242
|
+
arguments,
|
243
|
+
)
|
244
|
+
return self._convert_tool_result(tool_name, result)
|
245
|
+
|
246
|
+
|
247
|
+
async def get_langroid_tool_async(
|
248
|
+
server: str | ClientTransport,
|
249
|
+
tool_name: str,
|
250
|
+
) -> Type[ToolMessage]:
|
251
|
+
async with FastMCPClient(server) as client:
|
252
|
+
return await client.get_langroid_tool(tool_name)
|
253
|
+
|
254
|
+
|
255
|
+
def get_langroid_tool(
|
256
|
+
server: str | ClientTransport,
|
257
|
+
tool_name: str,
|
258
|
+
) -> Type[ToolMessage]:
|
259
|
+
return asyncio.run(get_langroid_tool_async(server, tool_name))
|
260
|
+
|
261
|
+
|
262
|
+
async def get_langroid_tools_async(
|
263
|
+
server: str | ClientTransport,
|
264
|
+
) -> List[Type[ToolMessage]]:
|
265
|
+
async with FastMCPClient(server) as client:
|
266
|
+
return await client.get_langroid_tools()
|
267
|
+
|
268
|
+
|
269
|
+
def get_langroid_tools(
|
270
|
+
server: str | ClientTransport,
|
271
|
+
) -> List[Type[ToolMessage]]:
|
272
|
+
return asyncio.run(get_langroid_tools_async(server))
|
273
|
+
|
274
|
+
|
275
|
+
async def get_mcp_tool_async(
|
276
|
+
server: str | ClientTransport,
|
277
|
+
name: str,
|
278
|
+
) -> Optional[Tool]:
|
279
|
+
async with FastMCPClient(server) as client:
|
280
|
+
return await client.get_mcp_tool_async(name)
|
281
|
+
|
282
|
+
|
283
|
+
async def get_mcp_tools_async(
|
284
|
+
server: str | ClientTransport,
|
285
|
+
) -> List[Tool]:
|
286
|
+
async with FastMCPClient(server) as client:
|
287
|
+
if not client.client:
|
288
|
+
raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
|
289
|
+
return await client.client.list_tools()
|
@@ -23,7 +23,7 @@ class ModelName(str, Enum):
|
|
23
23
|
class OpenAIChatModel(ModelName):
|
24
24
|
"""Enum for OpenAI Chat models"""
|
25
25
|
|
26
|
-
GPT3_5_TURBO = "gpt-3.5-turbo
|
26
|
+
GPT3_5_TURBO = "gpt-3.5-turbo"
|
27
27
|
GPT4 = "gpt-4o" # avoid deprecated gpt-4
|
28
28
|
GPT4_TURBO = "gpt-4-turbo"
|
29
29
|
GPT4o = "gpt-4o"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: langroid
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.53.1
|
4
4
|
Summary: Harness LLMs with Multi-Agent Programming
|
5
5
|
Author-email: Prasad Chalasani <pchalasani@gmail.com>
|
6
6
|
License: MIT
|
@@ -17,6 +17,7 @@ Requires-Dist: duckduckgo-search<7.0.0,>=6.0.0
|
|
17
17
|
Requires-Dist: exa-py>=1.8.7
|
18
18
|
Requires-Dist: faker<19.0.0,>=18.9.0
|
19
19
|
Requires-Dist: fakeredis<3.0.0,>=2.12.1
|
20
|
+
Requires-Dist: fastmcp>=2.2.5
|
20
21
|
Requires-Dist: fire<1.0.0,>=0.5.0
|
21
22
|
Requires-Dist: gitpython<4.0.0,>=3.1.43
|
22
23
|
Requires-Dist: google-api-python-client<3.0.0,>=2.95.0
|
@@ -54,6 +54,9 @@ langroid/agent/tools/retrieval_tool.py,sha256=zcAV20PP_6VzSd-UE-IJcabaBseFL_QNz5
|
|
54
54
|
langroid/agent/tools/rewind_tool.py,sha256=XAXL3BpNhCmBGYq_qi_sZfHJuIw7NY2jp4wnojJ7WRs,5606
|
55
55
|
langroid/agent/tools/segment_extract_tool.py,sha256=__srZ_VGYLVOdPrITUM8S0HpmX4q7r5FHWMDdHdEv8w,1440
|
56
56
|
langroid/agent/tools/tavily_search_tool.py,sha256=soI-j0HdgVQLf09wRQScaEK4b5RpAX9C4cwOivRFWWI,1903
|
57
|
+
langroid/agent/tools/mcp/__init__.py,sha256=cQb3gYxXk0YZ23QCqbVNMbMeCeWCJj6w3gqGnvyqv7w,459
|
58
|
+
langroid/agent/tools/mcp/decorators.py,sha256=mWnlTjyI9PMNi750PWzC_2B6V5K_XdxH0Co9kE2yAj0,1145
|
59
|
+
langroid/agent/tools/mcp/fastmcp_client.py,sha256=KUe73yqyKMTT7PxZfUaC0x23aRai0O09bbuyhHZfVo0,10630
|
57
60
|
langroid/cachedb/__init__.py,sha256=G2KyNnk3Qkhv7OKyxTOnpsxfDycx3NY0O_wXkJlalNY,96
|
58
61
|
langroid/cachedb/base.py,sha256=ztVjB1DtN6pLCujCWnR6xruHxwVj3XkYniRTYAKKqk0,1354
|
59
62
|
langroid/cachedb/redis_cachedb.py,sha256=7kgnbf4b5CKsCrlL97mHWKvdvlLt8zgn7lc528jEpiE,5141
|
@@ -71,7 +74,7 @@ langroid/language_models/azure_openai.py,sha256=SW0Fp_y6HpERr9l6TtF6CYsKgKwjUf_h
|
|
71
74
|
langroid/language_models/base.py,sha256=Axj8U9o9r7ovpCYqhNJ4SaVYLvufLRQXnr51IyIYJKY,28493
|
72
75
|
langroid/language_models/config.py,sha256=9Q8wk5a7RQr8LGMT_0WkpjY8S4ywK06SalVRjXlfCiI,378
|
73
76
|
langroid/language_models/mock_lm.py,sha256=5BgHKDVRWFbUwDT_PFgTZXz9-k8wJSA2e3PZmyDgQ1k,4022
|
74
|
-
langroid/language_models/model_info.py,sha256=
|
77
|
+
langroid/language_models/model_info.py,sha256=7Fv5YByZjsRXKhkaa6okOM8jhDVpWZu6xlYAN3WTSCk,14453
|
75
78
|
langroid/language_models/openai_gpt.py,sha256=F28jqTEerN32m14q3K0oc3vnvBT8J7Q9xqXGZNKUjKU,85938
|
76
79
|
langroid/language_models/utils.py,sha256=n55Oe2_V_4VNGhytvPWLYC-0tFS07RTjN83KWl-p_MI,6032
|
77
80
|
langroid/language_models/prompt_formatter/__init__.py,sha256=2-5cdE24XoFDhifOLl8yiscohil1ogbP1ECkYdBlBsk,372
|
@@ -129,7 +132,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
|
|
129
132
|
langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
|
130
133
|
langroid/vector_store/qdrantdb.py,sha256=O6dSBoDZ0jzfeVBd7LLvsXu083xs2fxXtPa9gGX3JX4,18443
|
131
134
|
langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
|
132
|
-
langroid-0.
|
133
|
-
langroid-0.
|
134
|
-
langroid-0.
|
135
|
-
langroid-0.
|
135
|
+
langroid-0.53.1.dist-info/METADATA,sha256=A36mOdb9KvdWGTTWAxCzr0jLyLU4IGoXUk8bO7CmCnc,63549
|
136
|
+
langroid-0.53.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
137
|
+
langroid-0.53.1.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
|
138
|
+
langroid-0.53.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|