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.
@@ -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-1106"
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.52.9
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=vOaTi-XFKnz-BvHUvgjnt0XfOtl21Apev3Zy7Rhckbw,14458
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.52.9.dist-info/METADATA,sha256=PzgwxODyztMuoTxbKg3DSZ1JaMwECyb9wykyvwxyXLg,63519
133
- langroid-0.52.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
- langroid-0.52.9.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
135
- langroid-0.52.9.dist-info/RECORD,,
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,,