langroid 0.52.8__py3-none-any.whl → 0.53.0__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,10 @@
1
+ from .decorators import mcp_tool
2
+ from .fastmcp_client import FastMCPClient, make_mcp_tool_sync, make_mcp_tool
3
+
4
+
5
+ __all__ = [
6
+ "mcp_tool",
7
+ "FastMCPClient",
8
+ "make_mcp_tool_sync",
9
+ "make_mcp_tool",
10
+ ]
@@ -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 make_mcp_tool_sync
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] = make_mcp_tool_sync(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,249 @@
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 make_tool(self, tool_name: str) -> Type[ToolMessage]:
117
+ """Create a Langroid ToolMessage subclass for the given tool name."""
118
+ if not self.client:
119
+ raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
120
+ target = await self.find_mcp_tool(tool_name)
121
+ if target is None:
122
+ raise ValueError(f"No tool named {tool_name}")
123
+ props = target.inputSchema.get("properties", {})
124
+ fields: Dict[str, Tuple[type, Any]] = {}
125
+ for fname, schema in props.items():
126
+ ftype, fld = self._schema_to_field(fname, schema, target.name)
127
+ fields[fname] = (ftype, fld)
128
+
129
+ # Convert target.name to CamelCase and add Tool suffix
130
+ parts = target.name.replace("-", "_").split("_")
131
+ camel_case = "".join(part.capitalize() for part in parts)
132
+ model_name = f"{camel_case}Tool"
133
+
134
+ # create Langroid ToolMessage subclass, with expected fields.
135
+ tool_model = cast(
136
+ Type[ToolMessage],
137
+ create_model( # type: ignore[call-overload]
138
+ model_name,
139
+ request=(str, target.name),
140
+ purpose=(str, target.description or f"Use the tool {target.name}"),
141
+ __base__=ToolMessage,
142
+ **fields,
143
+ ),
144
+ )
145
+ tool_model._server = self.server # type: ignore[attr-defined]
146
+
147
+ # 2) define an arg-free call_tool_async()
148
+ async def call_tool_async(self: ToolMessage) -> Any:
149
+ from langroid.agent.tools.mcp.fastmcp_client import FastMCPClient
150
+
151
+ # pack up the payload
152
+ payload = self.dict(exclude=self.Config.schema_extra["exclude"])
153
+ # open a fresh client, call the tool, then close
154
+ async with FastMCPClient(self.__class__._server) as client: # type: ignore
155
+ return await client.call_mcp_tool(self.request, payload)
156
+
157
+ tool_model.call_tool_async = call_tool_async # type: ignore
158
+
159
+ if not hasattr(tool_model, "handle_async"):
160
+ # 3) define an arg-free handle_async() method
161
+ # if the tool model doesn't already have one
162
+ async def handle_async(self: ToolMessage) -> Any:
163
+ return await self.call_tool_async() # type: ignore[attr-defined]
164
+
165
+ # add the handle_async() method to the tool model
166
+ tool_model.handle_async = handle_async # type: ignore
167
+
168
+ return tool_model
169
+
170
+ async def get_tools(self) -> List[Type[ToolMessage]]:
171
+ """
172
+ Get all available tools as Langroid ToolMessage classes,
173
+ handling nested schemas, with `handle_async` methods
174
+ """
175
+ if not self.client:
176
+ raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
177
+ resp = await self.client.list_tools()
178
+ tools: List[Type[ToolMessage]] = []
179
+ for t in resp:
180
+ tools.append(await self.make_tool(t.name))
181
+ return tools
182
+
183
+ async def find_mcp_tool(self, name: str) -> Optional[Tool]:
184
+ """Find the MCP Tool matching `name`, or None if missing.
185
+
186
+ Args:
187
+ name: Name of the tool to look up.
188
+
189
+ Returns:
190
+ The raw Tool object from the server, or None.
191
+ """
192
+ if not self.client:
193
+ raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
194
+ resp: List[Tool] = await self.client.list_tools()
195
+ return next((t for t in resp if t.name == name), None)
196
+
197
+ def _convert_tool_result(
198
+ self,
199
+ tool_name: str,
200
+ result: CallToolResult,
201
+ ) -> List[str] | str | None:
202
+ if result.isError:
203
+ self.logger.error(f"Error calling MCP tool {tool_name}")
204
+ return None
205
+ has_nontext_results = any(
206
+ not isinstance(item, TextContent) for item in result.content
207
+ )
208
+ if has_nontext_results:
209
+ self.logger.warning(
210
+ f"""
211
+ MCP Tool {tool_name} returned non-text results,
212
+ which will be skipped.
213
+ """,
214
+ )
215
+ results = [
216
+ item.text for item in result.content if isinstance(item, TextContent)
217
+ ]
218
+ if len(results) == 1:
219
+ return results[0]
220
+ return results
221
+
222
+ async def call_mcp_tool(
223
+ self, tool_name: str, arguments: Dict[str, Any]
224
+ ) -> str | List[str] | None:
225
+ """Call an MCP tool with the given arguments.
226
+
227
+ Args:
228
+ tool_name: Name of the tool to call.
229
+ arguments: Arguments to pass to the tool.
230
+
231
+ Returns:
232
+ The result of the tool call.
233
+ """
234
+ if not self.client:
235
+ raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
236
+ result: CallToolResult = await self.client.session.call_tool(
237
+ tool_name,
238
+ arguments,
239
+ )
240
+ return self._convert_tool_result(tool_name, result)
241
+
242
+
243
+ async def make_mcp_tool(server: str, tool_name: str) -> Type[ToolMessage]:
244
+ async with FastMCPClient(server) as client:
245
+ return await client.make_tool(tool_name)
246
+
247
+
248
+ def make_mcp_tool_sync(server: str, tool_name: str) -> Type[ToolMessage]:
249
+ return asyncio.run(make_mcp_tool(server, tool_name))
@@ -99,9 +99,8 @@ class LLMConfig(BaseSettings):
99
99
 
100
100
  @property
101
101
  def model_max_output_tokens(self) -> int:
102
- return min(
103
- self.max_output_tokens or get_model_info(self.chat_model).max_output_tokens,
104
- get_model_info(self.chat_model).max_output_tokens,
102
+ return (
103
+ self.max_output_tokens or get_model_info(self.chat_model).max_output_tokens
105
104
  )
106
105
 
107
106
 
@@ -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.8
3
+ Version: 0.53.0
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=aMW4cRepRbE44iCGQu_fQF623bN8Q1HHBbRMGaO22kY,210
58
+ langroid/agent/tools/mcp/decorators.py,sha256=zyV8iYq3dg3taHEkaC5aNncyb4UdNBp-i1tRhMOFkd4,1147
59
+ langroid/agent/tools/mcp/fastmcp_client.py,sha256=goj7GSbez28cFaa-KmQRavw7MY_hHUCYhKP0fUZLDms,9548
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
@@ -68,10 +71,10 @@ langroid/embedding_models/protoc/embeddings_pb2.pyi,sha256=UkNy7BrNsmQm0vLb3NtGX
68
71
  langroid/embedding_models/protoc/embeddings_pb2_grpc.py,sha256=9dYQqkW3JPyBpSEjeGXTNpSqAkC-6FPtBHyteVob2Y8,2452
69
72
  langroid/language_models/__init__.py,sha256=3aD2qC1lz8v12HX4B-dilv27gNxYdGdeu1QvDlkqqHs,1095
70
73
  langroid/language_models/azure_openai.py,sha256=SW0Fp_y6HpERr9l6TtF6CYsKgKwjUf_hSL_2mhTV4wI,5034
71
- langroid/language_models/base.py,sha256=pfN3t-BktKmN_4K8pwmpjC9OdcHxsytM5s5TmsJ-nPg,28560
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.8.dist-info/METADATA,sha256=v0NjlHl9pXUfWPYH4-Gobzo4fYN9fcCpDaFG3cPFX-A,63519
133
- langroid-0.52.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
- langroid-0.52.8.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
135
- langroid-0.52.8.dist-info/RECORD,,
135
+ langroid-0.53.0.dist-info/METADATA,sha256=JohvBH6oCb3ki2nsiIcPT0IiNmmn0dM2eNjQ9lsLn5E,63549
136
+ langroid-0.53.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
137
+ langroid-0.53.0.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
138
+ langroid-0.53.0.dist-info/RECORD,,