langroid 0.53.5__py3-none-any.whl → 0.53.7__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/chat_agent.py +98 -41
- langroid/agent/tools/mcp/__init__.py +8 -8
- langroid/agent/tools/mcp/decorators.py +2 -2
- langroid/agent/tools/mcp/fastmcp_client.py +176 -31
- langroid/language_models/base.py +10 -12
- langroid/language_models/mcp_client_lm.py +128 -0
- langroid/language_models/model_info.py +13 -2
- {langroid-0.53.5.dist-info → langroid-0.53.7.dist-info}/METADATA +1 -1
- {langroid-0.53.5.dist-info → langroid-0.53.7.dist-info}/RECORD +11 -10
- {langroid-0.53.5.dist-info → langroid-0.53.7.dist-info}/WHEEL +0 -0
- {langroid-0.53.5.dist-info → langroid-0.53.7.dist-info}/licenses/LICENSE +0 -0
langroid/agent/chat_agent.py
CHANGED
@@ -502,6 +502,17 @@ class ChatAgent(Agent):
|
|
502
502
|
idx = self.nth_message_idx_with_role(role, n_role_msgs)
|
503
503
|
return self.message_history[idx]
|
504
504
|
|
505
|
+
def last_message_idx_with_role(self, role: Role) -> int:
|
506
|
+
"""Index of last message in message_history, with specified role.
|
507
|
+
Return -1 if not found. Index = 0 is the first message in the history.
|
508
|
+
"""
|
509
|
+
indices_with_role = [
|
510
|
+
i for i, m in enumerate(self.message_history) if m.role == role
|
511
|
+
]
|
512
|
+
if len(indices_with_role) == 0:
|
513
|
+
return -1
|
514
|
+
return indices_with_role[-1]
|
515
|
+
|
505
516
|
def nth_message_idx_with_role(self, role: Role, n: int) -> int:
|
506
517
|
"""Index of `n`th message in message_history, with specified role.
|
507
518
|
(n is assumed to be 1-based, i.e. 1 is the first message with that role).
|
@@ -1229,9 +1240,18 @@ class ChatAgent(Agent):
|
|
1229
1240
|
idx: int,
|
1230
1241
|
tokens: int = 5,
|
1231
1242
|
warning: str = "...[Contents truncated!]",
|
1243
|
+
inplace: bool = True,
|
1232
1244
|
) -> LLMMessage:
|
1233
|
-
"""
|
1234
|
-
|
1245
|
+
"""
|
1246
|
+
Truncate message at idx in msg history to `tokens` tokens.
|
1247
|
+
|
1248
|
+
If inplace is True, the message is truncated in place, else
|
1249
|
+
it LEAVES the original message INTACT and returns a new message
|
1250
|
+
"""
|
1251
|
+
if inplace:
|
1252
|
+
llm_msg = self.message_history[idx]
|
1253
|
+
else:
|
1254
|
+
llm_msg = copy.deepcopy(self.message_history[idx])
|
1235
1255
|
orig_content = llm_msg.content
|
1236
1256
|
new_content = (
|
1237
1257
|
self.parser.truncate_tokens(orig_content, tokens)
|
@@ -1463,6 +1483,10 @@ class ChatAgent(Agent):
|
|
1463
1483
|
"""
|
1464
1484
|
Prepare messages to be sent to self.llm_response_messages,
|
1465
1485
|
which is the main method that calls the LLM API to get a response.
|
1486
|
+
If desired output tokens + message history exceeds the model context length,
|
1487
|
+
then first the max output tokens is reduced to fit, and if that is not
|
1488
|
+
possible, older messages may be truncated to accommodate at least
|
1489
|
+
self.config.llm.min_output_tokens of output.
|
1466
1490
|
|
1467
1491
|
Returns:
|
1468
1492
|
Tuple[List[LLMMessage], int]: (messages, output_len)
|
@@ -1530,17 +1554,42 @@ class ChatAgent(Agent):
|
|
1530
1554
|
truncate
|
1531
1555
|
and output_len > self.llm.chat_context_length() - self.chat_num_tokens(hist)
|
1532
1556
|
):
|
1557
|
+
CHAT_HISTORY_BUFFER = 300
|
1533
1558
|
# chat + output > max context length,
|
1534
1559
|
# so first try to shorten requested output len to fit;
|
1535
|
-
# use an extra margin of
|
1560
|
+
# use an extra margin of CHAT_HISTORY_BUFFER tokens
|
1561
|
+
# in case our calcs are off (and to allow for some extra tokens)
|
1536
1562
|
output_len = (
|
1537
|
-
self.llm.chat_context_length()
|
1563
|
+
self.llm.chat_context_length()
|
1564
|
+
- self.chat_num_tokens(hist)
|
1565
|
+
- CHAT_HISTORY_BUFFER
|
1538
1566
|
)
|
1539
|
-
if output_len
|
1540
|
-
|
1541
|
-
|
1567
|
+
if output_len > self.config.llm.min_output_tokens:
|
1568
|
+
logger.warning(
|
1569
|
+
f"""
|
1570
|
+
Chat Model context length is {self.llm.chat_context_length()},
|
1571
|
+
but the current message history is {self.chat_num_tokens(hist)}
|
1572
|
+
tokens long, which does not allow
|
1573
|
+
{self.config.llm.model_max_output_tokens} output tokens.
|
1574
|
+
Therefore we reduced `max_output_tokens` to {output_len} tokens,
|
1575
|
+
so they can fit within the model's context length
|
1576
|
+
"""
|
1577
|
+
)
|
1578
|
+
else:
|
1579
|
+
# unacceptably small output len, so compress early parts of conv
|
1580
|
+
# history if output_len is still too long.
|
1542
1581
|
# TODO we should really be doing summarization or other types of
|
1543
1582
|
# prompt-size reduction
|
1583
|
+
msg_idx_to_compress = 1 # don't touch system msg
|
1584
|
+
# we will try compressing msg indices up to but not including
|
1585
|
+
# last user msg
|
1586
|
+
last_msg_idx_to_compress = (
|
1587
|
+
self.last_message_idx_with_role(
|
1588
|
+
role=Role.USER,
|
1589
|
+
)
|
1590
|
+
- 1
|
1591
|
+
)
|
1592
|
+
n_truncated = 0
|
1544
1593
|
while (
|
1545
1594
|
self.chat_num_tokens(hist)
|
1546
1595
|
> self.llm.chat_context_length() - self.config.llm.min_output_tokens
|
@@ -1548,14 +1597,14 @@ class ChatAgent(Agent):
|
|
1548
1597
|
# try dropping early parts of conv history
|
1549
1598
|
# TODO we should really be doing summarization or other types of
|
1550
1599
|
# prompt-size reduction
|
1551
|
-
if
|
1600
|
+
if msg_idx_to_compress > last_msg_idx_to_compress:
|
1552
1601
|
# We want to preserve the first message (typically system msg)
|
1553
1602
|
# and last message (user msg).
|
1554
1603
|
raise ValueError(
|
1555
1604
|
"""
|
1556
1605
|
The (message history + max_output_tokens) is longer than the
|
1557
1606
|
max chat context length of this model, and we have tried
|
1558
|
-
reducing the requested max output tokens, as well as
|
1607
|
+
reducing the requested max output tokens, as well as truncating
|
1559
1608
|
early parts of the message history, to accommodate the model
|
1560
1609
|
context length, but we have run out of msgs to drop.
|
1561
1610
|
|
@@ -1566,51 +1615,59 @@ class ChatAgent(Agent):
|
|
1566
1615
|
- decreasing `max_output_tokens`
|
1567
1616
|
"""
|
1568
1617
|
)
|
1569
|
-
|
1570
|
-
#
|
1571
|
-
|
1572
|
-
|
1618
|
+
n_truncated += 1
|
1619
|
+
# compress the msg at idx `msg_idx_to_compress`
|
1620
|
+
hist[msg_idx_to_compress] = self.truncate_message(
|
1621
|
+
msg_idx_to_compress,
|
1622
|
+
tokens=30,
|
1623
|
+
warning="... [Contents truncated!]",
|
1624
|
+
)
|
1573
1625
|
|
1574
|
-
|
1626
|
+
msg_idx_to_compress += 1
|
1627
|
+
|
1628
|
+
output_len = min(
|
1629
|
+
self.config.llm.model_max_output_tokens,
|
1630
|
+
self.llm.chat_context_length()
|
1631
|
+
- self.chat_num_tokens(hist)
|
1632
|
+
- CHAT_HISTORY_BUFFER,
|
1633
|
+
)
|
1634
|
+
if output_len < self.config.llm.min_output_tokens:
|
1635
|
+
raise ValueError(
|
1636
|
+
f"""
|
1637
|
+
Tried to shorten prompt history for chat mode
|
1638
|
+
but even after truncating all messages except system msg and
|
1639
|
+
last (user) msg,
|
1640
|
+
the history token len {self.chat_num_tokens(hist)} is
|
1641
|
+
too long to accommodate the desired minimum output tokens
|
1642
|
+
{self.config.llm.min_output_tokens} within the
|
1643
|
+
model's context length {self.llm.chat_context_length()}.
|
1644
|
+
Please try shortening the system msg or user prompts,
|
1645
|
+
or adjust `config.llm.min_output_tokens` to be smaller.
|
1646
|
+
"""
|
1647
|
+
)
|
1648
|
+
else:
|
1649
|
+
# we MUST have truncated at least one msg
|
1575
1650
|
msg_tokens = self.chat_num_tokens()
|
1576
1651
|
logger.warning(
|
1577
1652
|
f"""
|
1578
1653
|
Chat Model context length is {self.llm.chat_context_length()}
|
1579
|
-
tokens, but the current message history is {msg_tokens} tokens long
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
{self.
|
1654
|
+
tokens, but the current message history is {msg_tokens} tokens long,
|
1655
|
+
which does not allow {self.config.llm.model_max_output_tokens}
|
1656
|
+
output tokens.
|
1657
|
+
Therefore we truncated the first {n_truncated} messages
|
1658
|
+
in the conversation history so that history token
|
1659
|
+
length is reduced to {self.chat_num_tokens(hist)}, and
|
1660
|
+
we use `max_output_tokens = {output_len}`,
|
1661
|
+
so they can fit within the model's context length
|
1662
|
+
of {self.llm.chat_context_length()} tokens.
|
1585
1663
|
"""
|
1586
1664
|
)
|
1587
1665
|
|
1588
|
-
if output_len < 0:
|
1589
|
-
raise ValueError(
|
1590
|
-
f"""
|
1591
|
-
Tried to shorten prompt history for chat mode
|
1592
|
-
but even after dropping all messages except system msg and last (
|
1593
|
-
user) msg, the history token len {self.chat_num_tokens(hist)} is longer
|
1594
|
-
than the model's max context length {self.llm.chat_context_length()}.
|
1595
|
-
Please try shortening the system msg or user prompts.
|
1596
|
-
"""
|
1597
|
-
)
|
1598
|
-
if output_len < self.config.llm.min_output_tokens:
|
1599
|
-
logger.warning(
|
1600
|
-
f"""
|
1601
|
-
Tried to shorten prompt history for chat mode
|
1602
|
-
but the feasible output length {output_len} is still
|
1603
|
-
less than the minimum output length {self.config.llm.min_output_tokens}.
|
1604
|
-
Your chat history is too long for this model,
|
1605
|
-
and the response may be truncated.
|
1606
|
-
"""
|
1607
|
-
)
|
1608
1666
|
if isinstance(message, ChatDocument):
|
1609
1667
|
# record the position of the corresponding LLMMessage in
|
1610
1668
|
# the message_history
|
1611
1669
|
message.metadata.msg_idx = len(hist) - 1
|
1612
1670
|
message.metadata.agent_id = self.id
|
1613
|
-
|
1614
1671
|
return hist, output_len
|
1615
1672
|
|
1616
1673
|
def _function_args(
|
@@ -1,10 +1,10 @@
|
|
1
1
|
from .decorators import mcp_tool
|
2
2
|
from .fastmcp_client import (
|
3
3
|
FastMCPClient,
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
get_tool,
|
5
|
+
get_tool_async,
|
6
|
+
get_tools,
|
7
|
+
get_tools_async,
|
8
8
|
get_mcp_tool_async,
|
9
9
|
get_mcp_tools_async,
|
10
10
|
)
|
@@ -13,10 +13,10 @@ from .fastmcp_client import (
|
|
13
13
|
__all__ = [
|
14
14
|
"mcp_tool",
|
15
15
|
"FastMCPClient",
|
16
|
-
"
|
17
|
-
"
|
18
|
-
"
|
19
|
-
"
|
16
|
+
"get_tool",
|
17
|
+
"get_tool_async",
|
18
|
+
"get_tools",
|
19
|
+
"get_tools_async",
|
20
20
|
"get_mcp_tool_async",
|
21
21
|
"get_mcp_tools_async",
|
22
22
|
]
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from typing import Callable, Type
|
2
2
|
|
3
3
|
from langroid.agent.tool_message import ToolMessage
|
4
|
-
from langroid.agent.tools.mcp.fastmcp_client import
|
4
|
+
from langroid.agent.tools.mcp.fastmcp_client import get_tool
|
5
5
|
|
6
6
|
|
7
7
|
def mcp_tool(
|
@@ -18,7 +18,7 @@ def mcp_tool(
|
|
18
18
|
|
19
19
|
def decorator(user_cls: Type[ToolMessage]) -> Type[ToolMessage]:
|
20
20
|
# build the “real” ToolMessage subclass for this server/tool
|
21
|
-
RealTool: Type[ToolMessage] =
|
21
|
+
RealTool: Type[ToolMessage] = get_tool(server, tool_name)
|
22
22
|
|
23
23
|
# copy user‐defined methods / attributes onto RealTool
|
24
24
|
for name, attr in user_cls.__dict__.items():
|
@@ -1,18 +1,30 @@
|
|
1
1
|
import asyncio
|
2
|
+
import datetime
|
2
3
|
import logging
|
3
|
-
from typing import Any, Dict, List, Optional, Tuple, Type, cast
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, TypeAlias, cast
|
4
5
|
|
5
6
|
from dotenv import load_dotenv
|
6
7
|
from fastmcp.client import Client
|
8
|
+
from fastmcp.client.roots import (
|
9
|
+
RootsHandler,
|
10
|
+
RootsList,
|
11
|
+
)
|
12
|
+
from fastmcp.client.sampling import SamplingHandler
|
7
13
|
from fastmcp.client.transports import ClientTransport
|
8
14
|
from fastmcp.server import FastMCP
|
15
|
+
from mcp.client.session import (
|
16
|
+
LoggingFnT,
|
17
|
+
MessageHandlerFnT,
|
18
|
+
)
|
9
19
|
from mcp.types import CallToolResult, TextContent, Tool
|
10
20
|
|
11
21
|
from langroid.agent.tool_message import ToolMessage
|
12
|
-
from langroid.pydantic_v1 import BaseModel, Field, create_model
|
22
|
+
from langroid.pydantic_v1 import AnyUrl, BaseModel, Field, create_model
|
13
23
|
|
14
24
|
load_dotenv() # load environment variables from .env
|
15
25
|
|
26
|
+
FastMCPServerSpec: TypeAlias = str | FastMCP[Any] | ClientTransport | AnyUrl
|
27
|
+
|
16
28
|
|
17
29
|
class FastMCPClient:
|
18
30
|
"""A client for interacting with a FastMCP server.
|
@@ -24,7 +36,15 @@ class FastMCPClient:
|
|
24
36
|
_cm: Optional[Client] = None
|
25
37
|
client: Optional[Client] = None
|
26
38
|
|
27
|
-
def __init__(
|
39
|
+
def __init__(
|
40
|
+
self,
|
41
|
+
server: FastMCPServerSpec,
|
42
|
+
sampling_handler: SamplingHandler | None = None, # type: ignore
|
43
|
+
roots: RootsList | RootsHandler | None = None, # type: ignore
|
44
|
+
log_handler: LoggingFnT | None = None,
|
45
|
+
message_handler: MessageHandlerFnT | None = None,
|
46
|
+
read_timeout_seconds: datetime.timedelta | None = None,
|
47
|
+
) -> None:
|
28
48
|
"""Initialize the FastMCPClient.
|
29
49
|
|
30
50
|
Args:
|
@@ -33,11 +53,23 @@ class FastMCPClient:
|
|
33
53
|
self.server = server
|
34
54
|
self.client = None
|
35
55
|
self._cm = None
|
56
|
+
self.sampling_handler = sampling_handler
|
57
|
+
self.roots = roots
|
58
|
+
self.log_handler = log_handler
|
59
|
+
self.message_handler = message_handler
|
60
|
+
self.read_timeout_seconds = read_timeout_seconds
|
36
61
|
|
37
62
|
async def __aenter__(self) -> "FastMCPClient":
|
38
63
|
"""Enter the async context manager and connect inner client."""
|
39
64
|
# create inner client context manager
|
40
|
-
self._cm = Client(
|
65
|
+
self._cm = Client(
|
66
|
+
self.server,
|
67
|
+
sampling_handler=self.sampling_handler,
|
68
|
+
roots=self.roots,
|
69
|
+
log_handler=self.log_handler,
|
70
|
+
message_handler=self.message_handler,
|
71
|
+
read_timeout_seconds=self.read_timeout_seconds,
|
72
|
+
)
|
41
73
|
# actually enter it (opens the session)
|
42
74
|
self.client = await self._cm.__aenter__() # type: ignore
|
43
75
|
return self
|
@@ -113,7 +145,7 @@ class FastMCPClient:
|
|
113
145
|
# Default fallback
|
114
146
|
return Any, Field(default=default, description=desc)
|
115
147
|
|
116
|
-
async def
|
148
|
+
async def get_tool_async(self, tool_name: str) -> Type[ToolMessage]:
|
117
149
|
"""
|
118
150
|
Create a Langroid ToolMessage subclass from the MCP Tool
|
119
151
|
with the given `tool_name`.
|
@@ -163,7 +195,17 @@ class FastMCPClient:
|
|
163
195
|
**fields,
|
164
196
|
),
|
165
197
|
)
|
166
|
-
|
198
|
+
# Store ALL client configuration needed to recreate a client
|
199
|
+
client_config = {
|
200
|
+
"server": self.server,
|
201
|
+
"sampling_handler": self.sampling_handler,
|
202
|
+
"roots": self.roots,
|
203
|
+
"log_handler": self.log_handler,
|
204
|
+
"message_handler": self.message_handler,
|
205
|
+
"read_timeout_seconds": self.read_timeout_seconds,
|
206
|
+
}
|
207
|
+
|
208
|
+
tool_model._client_config = client_config # type: ignore [attr-defined]
|
167
209
|
tool_model._renamed_fields = renamed # type: ignore[attr-defined]
|
168
210
|
|
169
211
|
# 2) define an arg-free call_tool_async()
|
@@ -171,15 +213,23 @@ class FastMCPClient:
|
|
171
213
|
from langroid.agent.tools.mcp.fastmcp_client import FastMCPClient
|
172
214
|
|
173
215
|
# pack up the payload
|
174
|
-
payload = self.dict(
|
216
|
+
payload = self.dict(
|
217
|
+
exclude=self.Config.schema_extra["exclude"].union(
|
218
|
+
["request", "purpose"]
|
219
|
+
),
|
220
|
+
)
|
175
221
|
|
176
222
|
# restore any renamed fields
|
177
223
|
for orig, new in self.__class__._renamed_fields.items(): # type: ignore
|
178
224
|
if new in payload:
|
179
225
|
payload[orig] = payload.pop(new)
|
180
226
|
|
227
|
+
client_cfg = getattr(self.__class__, "_client_config", None) # type: ignore
|
228
|
+
if not client_cfg:
|
229
|
+
# Fallback or error - ideally _client_config should always exist
|
230
|
+
raise RuntimeError(f"Client config missing on {self.__class__}")
|
181
231
|
# open a fresh client, call the tool, then close
|
182
|
-
async with FastMCPClient(
|
232
|
+
async with FastMCPClient(**client_cfg) as client: # type: ignore
|
183
233
|
return await client.call_mcp_tool(self.request, payload)
|
184
234
|
|
185
235
|
tool_model.call_tool_async = call_tool_async # type: ignore
|
@@ -195,7 +245,7 @@ class FastMCPClient:
|
|
195
245
|
|
196
246
|
return tool_model
|
197
247
|
|
198
|
-
async def
|
248
|
+
async def get_tools_async(self) -> List[Type[ToolMessage]]:
|
199
249
|
"""
|
200
250
|
Get all available tools as Langroid ToolMessage classes,
|
201
251
|
handling nested schemas, with `handle_async` methods
|
@@ -203,10 +253,7 @@ class FastMCPClient:
|
|
203
253
|
if not self.client:
|
204
254
|
raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
|
205
255
|
resp = await self.client.list_tools()
|
206
|
-
|
207
|
-
for t in resp:
|
208
|
-
tools.append(await self.get_langroid_tool(t.name))
|
209
|
-
return tools
|
256
|
+
return [await self.get_tool_async(t.name) for t in resp]
|
210
257
|
|
211
258
|
async def get_mcp_tool_async(self, name: str) -> Optional[Tool]:
|
212
259
|
"""Find the "original" MCP Tool (i.e. of type mcp.types.Tool) on the server
|
@@ -270,46 +317,144 @@ class FastMCPClient:
|
|
270
317
|
return self._convert_tool_result(tool_name, result)
|
271
318
|
|
272
319
|
|
273
|
-
|
274
|
-
|
320
|
+
# ==============================================================================
|
321
|
+
# Convenience functions (wrappers around FastMCPClient methods)
|
322
|
+
# These are useful for one-off calls without needing to manage the
|
323
|
+
# FastMCPClient context explicitly.
|
324
|
+
# ==============================================================================
|
325
|
+
|
326
|
+
|
327
|
+
async def get_tool_async(
|
328
|
+
server: FastMCPServerSpec,
|
275
329
|
tool_name: str,
|
330
|
+
**client_kwargs: Any,
|
276
331
|
) -> Type[ToolMessage]:
|
277
|
-
|
278
|
-
|
332
|
+
"""Get a single Langroid ToolMessage subclass for a specific MCP tool name (async).
|
333
|
+
|
334
|
+
This is a convenience wrapper that creates a temporary FastMCPClient.
|
279
335
|
|
336
|
+
Args:
|
337
|
+
server: Specification of the FastMCP server to connect to.
|
338
|
+
tool_name: The name of the tool to retrieve.
|
339
|
+
**client_kwargs: Additional keyword arguments to pass to the
|
340
|
+
FastMCPClient constructor (e.g., sampling_handler, roots).
|
280
341
|
|
281
|
-
|
282
|
-
|
342
|
+
Returns:
|
343
|
+
A dynamically created Langroid ToolMessage subclass representing the
|
344
|
+
requested tool.
|
345
|
+
"""
|
346
|
+
async with FastMCPClient(server, **client_kwargs) as client:
|
347
|
+
return await client.get_tool_async(tool_name)
|
348
|
+
|
349
|
+
|
350
|
+
def get_tool(
|
351
|
+
server: FastMCPServerSpec,
|
283
352
|
tool_name: str,
|
353
|
+
**client_kwargs: Any,
|
284
354
|
) -> Type[ToolMessage]:
|
285
|
-
|
355
|
+
"""Get a single Langroid ToolMessage subclass
|
356
|
+
for a specific MCP tool name (synchronous).
|
357
|
+
|
358
|
+
This is a convenience wrapper that creates a temporary FastMCPClient and runs the
|
359
|
+
async `get_tool_async` function using `asyncio.run()`.
|
286
360
|
|
361
|
+
Args:
|
362
|
+
server: Specification of the FastMCP server to connect to.
|
363
|
+
tool_name: The name of the tool to retrieve.
|
364
|
+
**client_kwargs: Additional keyword arguments to pass to the
|
365
|
+
FastMCPClient constructor (e.g., sampling_handler, roots).
|
287
366
|
|
288
|
-
|
289
|
-
|
367
|
+
Returns:
|
368
|
+
A dynamically created Langroid ToolMessage subclass representing the
|
369
|
+
requested tool.
|
370
|
+
"""
|
371
|
+
return asyncio.run(get_tool_async(server, tool_name, **client_kwargs))
|
372
|
+
|
373
|
+
|
374
|
+
async def get_tools_async(
|
375
|
+
server: FastMCPServerSpec,
|
376
|
+
**client_kwargs: Any,
|
290
377
|
) -> List[Type[ToolMessage]]:
|
291
|
-
|
292
|
-
|
378
|
+
"""Get all available tools as Langroid ToolMessage subclasses (async).
|
379
|
+
|
380
|
+
This is a convenience wrapper that creates a temporary FastMCPClient.
|
293
381
|
|
382
|
+
Args:
|
383
|
+
server: Specification of the FastMCP server to connect to.
|
384
|
+
**client_kwargs: Additional keyword arguments to pass to the
|
385
|
+
FastMCPClient constructor (e.g., sampling_handler, roots).
|
294
386
|
|
295
|
-
|
296
|
-
|
387
|
+
Returns:
|
388
|
+
A list of dynamically created Langroid ToolMessage subclasses
|
389
|
+
representing all available tools on the server.
|
390
|
+
"""
|
391
|
+
async with FastMCPClient(server, **client_kwargs) as client:
|
392
|
+
return await client.get_tools_async()
|
393
|
+
|
394
|
+
|
395
|
+
def get_tools(
|
396
|
+
server: FastMCPServerSpec,
|
397
|
+
**client_kwargs: Any,
|
297
398
|
) -> List[Type[ToolMessage]]:
|
298
|
-
|
399
|
+
"""Get all available tools as Langroid ToolMessage subclasses (synchronous).
|
400
|
+
|
401
|
+
This is a convenience wrapper that creates a temporary FastMCPClient and runs the
|
402
|
+
async `get_tools_async` function using `asyncio.run()`.
|
403
|
+
|
404
|
+
Args:
|
405
|
+
server: Specification of the FastMCP server to connect to.
|
406
|
+
**client_kwargs: Additional keyword arguments to pass to the
|
407
|
+
FastMCPClient constructor (e.g., sampling_handler, roots).
|
408
|
+
|
409
|
+
Returns:
|
410
|
+
A list of dynamically created Langroid ToolMessage subclasses
|
411
|
+
representing all available tools on the server.
|
412
|
+
"""
|
413
|
+
return asyncio.run(get_tools_async(server, **client_kwargs))
|
299
414
|
|
300
415
|
|
301
416
|
async def get_mcp_tool_async(
|
302
|
-
server:
|
417
|
+
server: FastMCPServerSpec,
|
303
418
|
name: str,
|
419
|
+
**client_kwargs: Any,
|
304
420
|
) -> Optional[Tool]:
|
305
|
-
|
421
|
+
"""Get the raw MCP Tool object for a specific tool name (async).
|
422
|
+
|
423
|
+
This is a convenience wrapper that creates a temporary FastMCPClient to
|
424
|
+
retrieve the tool definition from the server.
|
425
|
+
|
426
|
+
Args:
|
427
|
+
server: Specification of the FastMCP server to connect to.
|
428
|
+
name: The name of the tool to look up.
|
429
|
+
**client_kwargs: Additional keyword arguments to pass to the
|
430
|
+
FastMCPClient constructor.
|
431
|
+
|
432
|
+
Returns:
|
433
|
+
The raw `mcp.types.Tool` object from the server, or `None` if the tool
|
434
|
+
is not found.
|
435
|
+
"""
|
436
|
+
async with FastMCPClient(server, **client_kwargs) as client:
|
306
437
|
return await client.get_mcp_tool_async(name)
|
307
438
|
|
308
439
|
|
309
440
|
async def get_mcp_tools_async(
|
310
|
-
server:
|
441
|
+
server: FastMCPServerSpec,
|
442
|
+
**client_kwargs: Any,
|
311
443
|
) -> List[Tool]:
|
312
|
-
|
444
|
+
"""Get all available raw MCP Tool objects from the server (async).
|
445
|
+
|
446
|
+
This is a convenience wrapper that creates a temporary FastMCPClient to
|
447
|
+
retrieve the list of tool definitions from the server.
|
448
|
+
|
449
|
+
Args:
|
450
|
+
server: Specification of the FastMCP server to connect to.
|
451
|
+
**client_kwargs: Additional keyword arguments to pass to the
|
452
|
+
FastMCPClient constructor.
|
453
|
+
|
454
|
+
Returns:
|
455
|
+
A list of raw `mcp.types.Tool` objects available on the server.
|
456
|
+
"""
|
457
|
+
async with FastMCPClient(server, **client_kwargs) as client:
|
313
458
|
if not client.client:
|
314
459
|
raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
|
315
460
|
return await client.client.list_tools()
|
langroid/language_models/base.py
CHANGED
@@ -620,33 +620,31 @@ class LanguageModel(ABC):
|
|
620
620
|
def __call__(self, prompt: str, max_tokens: int) -> LLMResponse:
|
621
621
|
return self.generate(prompt, max_tokens)
|
622
622
|
|
623
|
+
@staticmethod
|
624
|
+
def _fallback_model_names(model: str) -> List[str]:
|
625
|
+
parts = model.split("/")
|
626
|
+
fallbacks = []
|
627
|
+
for i in range(1, len(parts)):
|
628
|
+
fallbacks.append("/".join(parts[i:]))
|
629
|
+
return fallbacks
|
630
|
+
|
623
631
|
def info(self) -> ModelInfo:
|
624
632
|
"""Info of relevant chat model"""
|
625
|
-
model = (
|
626
|
-
self.config.completion_model
|
627
|
-
if self.config.use_completion_for_chat
|
628
|
-
else self.config.chat_model
|
629
|
-
)
|
630
633
|
orig_model = (
|
631
634
|
self.config.completion_model
|
632
635
|
if self.config.use_completion_for_chat
|
633
636
|
else self.chat_model_orig
|
634
637
|
)
|
635
|
-
return get_model_info(orig_model,
|
638
|
+
return get_model_info(orig_model, self._fallback_model_names(orig_model))
|
636
639
|
|
637
640
|
def completion_info(self) -> ModelInfo:
|
638
641
|
"""Info of relevant completion model"""
|
639
|
-
model = (
|
640
|
-
self.config.chat_model
|
641
|
-
if self.config.use_chat_for_completion
|
642
|
-
else self.config.completion_model
|
643
|
-
)
|
644
642
|
orig_model = (
|
645
643
|
self.chat_model_orig
|
646
644
|
if self.config.use_chat_for_completion
|
647
645
|
else self.config.completion_model
|
648
646
|
)
|
649
|
-
return get_model_info(orig_model,
|
647
|
+
return get_model_info(orig_model, self._fallback_model_names(orig_model))
|
650
648
|
|
651
649
|
def supports_functions_or_tools(self) -> bool:
|
652
650
|
"""
|
@@ -0,0 +1,128 @@
|
|
1
|
+
"""
|
2
|
+
An API for an Agent in an MCP Server to use for chat-completions
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Awaitable, Callable, Dict, List, Optional, Union
|
6
|
+
|
7
|
+
from fastmcp.server import Context
|
8
|
+
|
9
|
+
import langroid.language_models as lm
|
10
|
+
from langroid.language_models import LLMResponse
|
11
|
+
from langroid.language_models.base import (
|
12
|
+
LanguageModel,
|
13
|
+
LLMConfig,
|
14
|
+
OpenAIJsonSchemaSpec,
|
15
|
+
OpenAIToolSpec,
|
16
|
+
ToolChoiceTypes,
|
17
|
+
)
|
18
|
+
from langroid.utils.types import to_string
|
19
|
+
|
20
|
+
|
21
|
+
def none_fn(x: str) -> None | str:
|
22
|
+
return None
|
23
|
+
|
24
|
+
|
25
|
+
class MCPClientLMConfig(LLMConfig):
|
26
|
+
"""
|
27
|
+
Mock Language Model Configuration.
|
28
|
+
|
29
|
+
Attributes:
|
30
|
+
response_dict (Dict[str, str]): A "response rule-book", in the form of a
|
31
|
+
dictionary; if last msg in dialog is x,then respond with response_dict[x]
|
32
|
+
"""
|
33
|
+
|
34
|
+
response_dict: Dict[str, str] = {}
|
35
|
+
response_fn: Callable[[str], None | str] = none_fn
|
36
|
+
response_fn_async: Optional[Callable[[str], Awaitable[Optional[str]]]] = None
|
37
|
+
default_response: str = "Mock response"
|
38
|
+
|
39
|
+
type: str = "mock"
|
40
|
+
|
41
|
+
|
42
|
+
class MockLM(LanguageModel):
|
43
|
+
|
44
|
+
def __init__(self, config: MockLMConfig = MockLMConfig()):
|
45
|
+
super().__init__(config)
|
46
|
+
self.config: MockLMConfig = config
|
47
|
+
|
48
|
+
def _response(self, msg: str) -> LLMResponse:
|
49
|
+
# response is based on this fallback order:
|
50
|
+
# - response_dict
|
51
|
+
# - response_fn
|
52
|
+
# - default_response
|
53
|
+
mapped_response = self.config.response_dict.get(
|
54
|
+
msg, self.config.response_fn(msg) or self.config.default_response
|
55
|
+
)
|
56
|
+
return lm.LLMResponse(
|
57
|
+
message=to_string(mapped_response),
|
58
|
+
cached=False,
|
59
|
+
)
|
60
|
+
|
61
|
+
async def _response_async(self, msg: str) -> LLMResponse:
|
62
|
+
# response is based on this fallback order:
|
63
|
+
# - response_dict
|
64
|
+
# - response_fn_async
|
65
|
+
# - response_fn
|
66
|
+
# - default_response
|
67
|
+
if self.config.response_fn_async is not None:
|
68
|
+
response = await self.config.response_fn_async(msg)
|
69
|
+
else:
|
70
|
+
response = self.config.response_fn(msg)
|
71
|
+
|
72
|
+
mapped_response = self.config.response_dict.get(
|
73
|
+
msg, response or self.config.default_response
|
74
|
+
)
|
75
|
+
return lm.LLMResponse(
|
76
|
+
message=to_string(mapped_response),
|
77
|
+
cached=False,
|
78
|
+
)
|
79
|
+
|
80
|
+
def chat(
|
81
|
+
self,
|
82
|
+
messages: Union[str, List[lm.LLMMessage]],
|
83
|
+
max_tokens: int = 200,
|
84
|
+
tools: Optional[List[OpenAIToolSpec]] = None,
|
85
|
+
tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
|
86
|
+
functions: Optional[List[lm.LLMFunctionSpec]] = None,
|
87
|
+
function_call: str | Dict[str, str] = "auto",
|
88
|
+
response_format: Optional[OpenAIJsonSchemaSpec] = None,
|
89
|
+
) -> lm.LLMResponse:
|
90
|
+
"""
|
91
|
+
Mock chat function for testing
|
92
|
+
"""
|
93
|
+
last_msg = messages[-1].content if isinstance(messages, list) else messages
|
94
|
+
return self._response(last_msg)
|
95
|
+
|
96
|
+
async def achat(
|
97
|
+
self,
|
98
|
+
messages: Union[str, List[lm.LLMMessage]],
|
99
|
+
max_tokens: int = 200,
|
100
|
+
tools: Optional[List[OpenAIToolSpec]] = None,
|
101
|
+
tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
|
102
|
+
functions: Optional[List[lm.LLMFunctionSpec]] = None,
|
103
|
+
function_call: str | Dict[str, str] = "auto",
|
104
|
+
response_format: Optional[OpenAIJsonSchemaSpec] = None,
|
105
|
+
) -> lm.LLMResponse:
|
106
|
+
"""
|
107
|
+
Mock chat function for testing
|
108
|
+
"""
|
109
|
+
last_msg = messages[-1].content if isinstance(messages, list) else messages
|
110
|
+
return await self._response_async(last_msg)
|
111
|
+
|
112
|
+
def generate(self, prompt: str, max_tokens: int = 200) -> lm.LLMResponse:
|
113
|
+
"""
|
114
|
+
Mock generate function for testing
|
115
|
+
"""
|
116
|
+
return self._response(prompt)
|
117
|
+
|
118
|
+
async def agenerate(self, prompt: str, max_tokens: int = 200) -> LLMResponse:
|
119
|
+
"""
|
120
|
+
Mock generate function for testing
|
121
|
+
"""
|
122
|
+
return await self._response_async(prompt)
|
123
|
+
|
124
|
+
def get_stream(self) -> bool:
|
125
|
+
return False
|
126
|
+
|
127
|
+
def set_stream(self, stream: bool) -> bool:
|
128
|
+
return False
|
@@ -406,10 +406,21 @@ MODEL_INFO: Dict[str, ModelInfo] = {
|
|
406
406
|
|
407
407
|
def get_model_info(
|
408
408
|
model: str | ModelName,
|
409
|
-
|
409
|
+
fallback_models: List[str] = [],
|
410
410
|
) -> ModelInfo:
|
411
411
|
"""Get model information by name or enum value"""
|
412
|
-
|
412
|
+
# Sequence of models to try, starting with the primary model
|
413
|
+
models_to_try = [model] + fallback_models
|
414
|
+
|
415
|
+
# Find the first model in the sequence that has info defined using next()
|
416
|
+
# on a generator expression that filters out None results from _get_model_info
|
417
|
+
found_info = next(
|
418
|
+
(info for m in models_to_try if (info := _get_model_info(m)) is not None),
|
419
|
+
None, # Default value if the iterator is exhausted (no valid info found)
|
420
|
+
)
|
421
|
+
|
422
|
+
# Return the found info, or a default ModelInfo if none was found
|
423
|
+
return found_info or ModelInfo()
|
413
424
|
|
414
425
|
|
415
426
|
def _get_model_info(model: str | ModelName) -> ModelInfo | None:
|
@@ -5,7 +5,7 @@ langroid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
langroid/agent/__init__.py,sha256=ll0Cubd2DZ-fsCMl7e10hf9ZjFGKzphfBco396IKITY,786
|
6
6
|
langroid/agent/base.py,sha256=zHwhNU403H-ZvogH4QhKTzaZn5_jt0ZdPHzSEmycDoc,80035
|
7
7
|
langroid/agent/batch.py,sha256=vi1r5i1-vN80WfqHDSwjEym_KfGsqPGUtwktmiK1nuk,20635
|
8
|
-
langroid/agent/chat_agent.py,sha256=
|
8
|
+
langroid/agent/chat_agent.py,sha256=2HIYzYxkrGkRIS97ioKfIqjaW3RbX89M39LjzBobBEY,88381
|
9
9
|
langroid/agent/chat_document.py,sha256=6O20Fp4QrquykaF2jFtwNHkvcoDte1LLwVZNk9mVH9c,18057
|
10
10
|
langroid/agent/openai_assistant.py,sha256=JkAcs02bIrgPNVvUWVR06VCthc5-ulla2QMBzux_q6o,34340
|
11
11
|
langroid/agent/task.py,sha256=HB6N-Jn80HFqCf0ZYOC1v3Bn3oO7NLjShHQJJFwW0q4,90557
|
@@ -54,9 +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=
|
58
|
-
langroid/agent/tools/mcp/decorators.py,sha256=
|
59
|
-
langroid/agent/tools/mcp/fastmcp_client.py,sha256=
|
57
|
+
langroid/agent/tools/mcp/__init__.py,sha256=DJNM0VeFnFS3pJKCyFGggT8JVjVu0rBzrGzasT1HaSM,387
|
58
|
+
langroid/agent/tools/mcp/decorators.py,sha256=h7dterhsmvWJ8q4mp_OopmuG2DF71ty8cZwOyzdDZuk,1127
|
59
|
+
langroid/agent/tools/mcp/fastmcp_client.py,sha256=g2mJe6cCpzF0XBmW6zAqCz5AvIEt0ZWwe8uAMM4jNS0,17445
|
60
60
|
langroid/cachedb/__init__.py,sha256=G2KyNnk3Qkhv7OKyxTOnpsxfDycx3NY0O_wXkJlalNY,96
|
61
61
|
langroid/cachedb/base.py,sha256=ztVjB1DtN6pLCujCWnR6xruHxwVj3XkYniRTYAKKqk0,1354
|
62
62
|
langroid/cachedb/redis_cachedb.py,sha256=7kgnbf4b5CKsCrlL97mHWKvdvlLt8zgn7lc528jEpiE,5141
|
@@ -71,10 +71,11 @@ langroid/embedding_models/protoc/embeddings_pb2.pyi,sha256=UkNy7BrNsmQm0vLb3NtGX
|
|
71
71
|
langroid/embedding_models/protoc/embeddings_pb2_grpc.py,sha256=9dYQqkW3JPyBpSEjeGXTNpSqAkC-6FPtBHyteVob2Y8,2452
|
72
72
|
langroid/language_models/__init__.py,sha256=3aD2qC1lz8v12HX4B-dilv27gNxYdGdeu1QvDlkqqHs,1095
|
73
73
|
langroid/language_models/azure_openai.py,sha256=SW0Fp_y6HpERr9l6TtF6CYsKgKwjUf_hSL_2mhTV4wI,5034
|
74
|
-
langroid/language_models/base.py,sha256=
|
74
|
+
langroid/language_models/base.py,sha256=253xcwXZ0yxSQ1W4SR50tAPZKCDc35yyU1o35EqB9b8,28484
|
75
75
|
langroid/language_models/config.py,sha256=9Q8wk5a7RQr8LGMT_0WkpjY8S4ywK06SalVRjXlfCiI,378
|
76
|
+
langroid/language_models/mcp_client_lm.py,sha256=wyDvlc26E_En5u_ZNZxajCHm8KBNi4jzG-dL76QCdt4,4098
|
76
77
|
langroid/language_models/mock_lm.py,sha256=5BgHKDVRWFbUwDT_PFgTZXz9-k8wJSA2e3PZmyDgQ1k,4022
|
77
|
-
langroid/language_models/model_info.py,sha256=
|
78
|
+
langroid/language_models/model_info.py,sha256=0e011vJZMi7XU9OkKT6doxlybrNJfMlP54klLDDNgFg,14939
|
78
79
|
langroid/language_models/openai_gpt.py,sha256=F28jqTEerN32m14q3K0oc3vnvBT8J7Q9xqXGZNKUjKU,85938
|
79
80
|
langroid/language_models/utils.py,sha256=n55Oe2_V_4VNGhytvPWLYC-0tFS07RTjN83KWl-p_MI,6032
|
80
81
|
langroid/language_models/prompt_formatter/__init__.py,sha256=2-5cdE24XoFDhifOLl8yiscohil1ogbP1ECkYdBlBsk,372
|
@@ -132,7 +133,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
|
|
132
133
|
langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
|
133
134
|
langroid/vector_store/qdrantdb.py,sha256=O6dSBoDZ0jzfeVBd7LLvsXu083xs2fxXtPa9gGX3JX4,18443
|
134
135
|
langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
|
135
|
-
langroid-0.53.
|
136
|
-
langroid-0.53.
|
137
|
-
langroid-0.53.
|
138
|
-
langroid-0.53.
|
136
|
+
langroid-0.53.7.dist-info/METADATA,sha256=crGfq16xZSGBqOyccaPQDgpy_hKGc1cgm4JmE2imWJQ,64945
|
137
|
+
langroid-0.53.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
138
|
+
langroid-0.53.7.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
|
139
|
+
langroid-0.53.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|