langroid 0.54.0__py3-none-any.whl → 0.54.2__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/base.py +190 -27
- langroid/agent/task.py +13 -0
- langroid/agent/tools/mcp/fastmcp_client.py +143 -33
- {langroid-0.54.0.dist-info → langroid-0.54.2.dist-info}/METADATA +4 -1
- {langroid-0.54.0.dist-info → langroid-0.54.2.dist-info}/RECORD +7 -7
- {langroid-0.54.0.dist-info → langroid-0.54.2.dist-info}/WHEEL +0 -0
- {langroid-0.54.0.dist-info → langroid-0.54.2.dist-info}/licenses/LICENSE +0 -0
langroid/agent/base.py
CHANGED
@@ -251,6 +251,172 @@ class Agent(ABC):
|
|
251
251
|
def clear_dialog(self) -> None:
|
252
252
|
self.dialog = []
|
253
253
|
|
254
|
+
def _analyze_handler_params(
|
255
|
+
self, handler_method: Any
|
256
|
+
) -> Tuple[bool, Optional[str], Optional[str]]:
|
257
|
+
"""
|
258
|
+
Analyze parameters of a handler method to determine their types.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
Tuple of (has_annotations, agent_param_name, chat_doc_param_name)
|
262
|
+
- has_annotations: True if useful type annotations were found
|
263
|
+
- agent_param_name: Name of the agent parameter if found
|
264
|
+
- chat_doc_param_name: Name of the chat_doc parameter if found
|
265
|
+
"""
|
266
|
+
sig = inspect.signature(handler_method)
|
267
|
+
params = list(sig.parameters.values())
|
268
|
+
# Remove 'self' parameter
|
269
|
+
params = [p for p in params if p.name != "self"]
|
270
|
+
|
271
|
+
agent_param = None
|
272
|
+
chat_doc_param = None
|
273
|
+
has_annotations = False
|
274
|
+
|
275
|
+
for param in params:
|
276
|
+
# First try type annotations
|
277
|
+
if param.annotation != inspect.Parameter.empty:
|
278
|
+
ann_str = str(param.annotation)
|
279
|
+
# Check for Agent-like types
|
280
|
+
if (
|
281
|
+
param.annotation == self.__class__
|
282
|
+
or "Agent" in ann_str
|
283
|
+
or (
|
284
|
+
hasattr(param.annotation, "__name__")
|
285
|
+
and "Agent" in param.annotation.__name__
|
286
|
+
)
|
287
|
+
):
|
288
|
+
agent_param = param.name
|
289
|
+
has_annotations = True
|
290
|
+
# Check for ChatDocument-like types
|
291
|
+
elif "ChatDocument" in ann_str or "ChatDoc" in ann_str:
|
292
|
+
chat_doc_param = param.name
|
293
|
+
has_annotations = True
|
294
|
+
|
295
|
+
# Fallback to parameter names
|
296
|
+
elif param.name == "agent":
|
297
|
+
agent_param = param.name
|
298
|
+
elif param.name == "chat_doc":
|
299
|
+
chat_doc_param = param.name
|
300
|
+
|
301
|
+
return has_annotations, agent_param, chat_doc_param
|
302
|
+
|
303
|
+
@no_type_check
|
304
|
+
def _create_handler_wrapper(
|
305
|
+
self,
|
306
|
+
message_class: Type[ToolMessage],
|
307
|
+
handler_method: Any,
|
308
|
+
is_async: bool = False,
|
309
|
+
) -> Any:
|
310
|
+
"""
|
311
|
+
Create a wrapper function for a handler method based on its signature.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
message_class: The ToolMessage class
|
315
|
+
handler_method: The handle/handle_async method
|
316
|
+
is_async: Whether this is for an async handler
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
Appropriate wrapper function
|
320
|
+
"""
|
321
|
+
sig = inspect.signature(handler_method)
|
322
|
+
params = list(sig.parameters.values())
|
323
|
+
params = [p for p in params if p.name != "self"]
|
324
|
+
|
325
|
+
has_annotations, agent_param, chat_doc_param = self._analyze_handler_params(
|
326
|
+
handler_method,
|
327
|
+
)
|
328
|
+
|
329
|
+
# Build wrapper based on found parameters
|
330
|
+
if len(params) == 0:
|
331
|
+
if is_async:
|
332
|
+
|
333
|
+
async def wrapper(obj: Any) -> Any:
|
334
|
+
return await obj.handle_async()
|
335
|
+
|
336
|
+
else:
|
337
|
+
|
338
|
+
def wrapper(obj: Any) -> Any:
|
339
|
+
return obj.handle()
|
340
|
+
|
341
|
+
elif agent_param and chat_doc_param:
|
342
|
+
# Both parameters present - build wrapper respecting their order
|
343
|
+
param_names = [p.name for p in params]
|
344
|
+
if param_names.index(agent_param) < param_names.index(chat_doc_param):
|
345
|
+
# agent is first parameter
|
346
|
+
if is_async:
|
347
|
+
|
348
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
349
|
+
return await obj.handle_async(self, chat_doc)
|
350
|
+
|
351
|
+
else:
|
352
|
+
|
353
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
354
|
+
return obj.handle(self, chat_doc)
|
355
|
+
|
356
|
+
else:
|
357
|
+
# chat_doc is first parameter
|
358
|
+
if is_async:
|
359
|
+
|
360
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
361
|
+
return await obj.handle_async(chat_doc, self)
|
362
|
+
|
363
|
+
else:
|
364
|
+
|
365
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
366
|
+
return obj.handle(chat_doc, self)
|
367
|
+
|
368
|
+
elif agent_param and not chat_doc_param:
|
369
|
+
# Only agent parameter
|
370
|
+
if is_async:
|
371
|
+
|
372
|
+
async def wrapper(obj: Any) -> Any:
|
373
|
+
return await obj.handle_async(self)
|
374
|
+
|
375
|
+
else:
|
376
|
+
|
377
|
+
def wrapper(obj: Any) -> Any:
|
378
|
+
return obj.handle(self)
|
379
|
+
|
380
|
+
elif chat_doc_param and not agent_param:
|
381
|
+
# Only chat_doc parameter
|
382
|
+
if is_async:
|
383
|
+
|
384
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
385
|
+
return await obj.handle_async(chat_doc)
|
386
|
+
|
387
|
+
else:
|
388
|
+
|
389
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
390
|
+
return obj.handle(chat_doc)
|
391
|
+
|
392
|
+
else:
|
393
|
+
# No recognized parameters - backward compatibility
|
394
|
+
# Assume single parameter is chat_doc (legacy behavior)
|
395
|
+
if len(params) == 1:
|
396
|
+
if is_async:
|
397
|
+
|
398
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
399
|
+
return await obj.handle_async(chat_doc)
|
400
|
+
|
401
|
+
else:
|
402
|
+
|
403
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
404
|
+
return obj.handle(chat_doc)
|
405
|
+
|
406
|
+
else:
|
407
|
+
# Multiple unrecognized parameters - best guess
|
408
|
+
if is_async:
|
409
|
+
|
410
|
+
async def wrapper(obj: Any, chat_doc: Any) -> Any:
|
411
|
+
return await obj.handle_async(chat_doc)
|
412
|
+
|
413
|
+
else:
|
414
|
+
|
415
|
+
def wrapper(obj: Any, chat_doc: Any) -> Any:
|
416
|
+
return obj.handle(chat_doc)
|
417
|
+
|
418
|
+
return wrapper
|
419
|
+
|
254
420
|
def _get_tool_list(
|
255
421
|
self, message_class: Optional[Type[ToolMessage]] = None
|
256
422
|
) -> List[str]:
|
@@ -304,13 +470,12 @@ class Agent(ABC):
|
|
304
470
|
in one place, i.e. in the message class.
|
305
471
|
See `tests/main/test_stateless_tool_messages.py` for an example.
|
306
472
|
"""
|
307
|
-
|
308
|
-
|
473
|
+
wrapper = self._create_handler_wrapper(
|
474
|
+
message_class,
|
475
|
+
message_class.handle,
|
476
|
+
is_async=False,
|
309
477
|
)
|
310
|
-
|
311
|
-
setattr(self, handler, lambda obj, chat_doc: obj.handle(chat_doc))
|
312
|
-
else:
|
313
|
-
setattr(self, handler, lambda obj: obj.handle())
|
478
|
+
setattr(self, handler, wrapper)
|
314
479
|
elif (
|
315
480
|
hasattr(message_class, "response")
|
316
481
|
and inspect.isfunction(message_class.response)
|
@@ -320,11 +485,17 @@ class Agent(ABC):
|
|
320
485
|
len(inspect.signature(message_class.response).parameters) > 2
|
321
486
|
)
|
322
487
|
if has_chat_doc_arg:
|
323
|
-
|
324
|
-
|
325
|
-
|
488
|
+
|
489
|
+
def response_wrapper_with_chat_doc(obj: Any, chat_doc: Any) -> Any:
|
490
|
+
return obj.response(self, chat_doc)
|
491
|
+
|
492
|
+
setattr(self, handler, response_wrapper_with_chat_doc)
|
326
493
|
else:
|
327
|
-
|
494
|
+
|
495
|
+
def response_wrapper_no_chat_doc(obj: Any) -> Any:
|
496
|
+
return obj.response(self)
|
497
|
+
|
498
|
+
setattr(self, handler, response_wrapper_no_chat_doc)
|
328
499
|
|
329
500
|
if hasattr(message_class, "handle_message_fallback") and (
|
330
501
|
inspect.isfunction(message_class.handle_message_fallback)
|
@@ -334,10 +505,13 @@ class Agent(ABC):
|
|
334
505
|
# `handle_message_fallback` method (which does nothing).
|
335
506
|
# It's possible multiple tool messages have a `handle_message_fallback`,
|
336
507
|
# in which case, the last one inserted will be used.
|
508
|
+
def fallback_wrapper(msg: Any) -> Any:
|
509
|
+
return message_class.handle_message_fallback(self, msg)
|
510
|
+
|
337
511
|
setattr(
|
338
512
|
self,
|
339
513
|
"handle_message_fallback",
|
340
|
-
|
514
|
+
fallback_wrapper,
|
341
515
|
)
|
342
516
|
|
343
517
|
async_handler_name = f"{handler}_async"
|
@@ -346,23 +520,12 @@ class Agent(ABC):
|
|
346
520
|
and inspect.isfunction(message_class.handle_async)
|
347
521
|
and not hasattr(self, async_handler_name)
|
348
522
|
):
|
349
|
-
|
350
|
-
|
523
|
+
wrapper = self._create_handler_wrapper(
|
524
|
+
message_class,
|
525
|
+
message_class.handle_async,
|
526
|
+
is_async=True,
|
351
527
|
)
|
352
|
-
|
353
|
-
if has_chat_doc_arg:
|
354
|
-
|
355
|
-
@no_type_check
|
356
|
-
async def handler(obj, chat_doc):
|
357
|
-
return await obj.handle_async(chat_doc)
|
358
|
-
|
359
|
-
else:
|
360
|
-
|
361
|
-
@no_type_check
|
362
|
-
async def handler(obj):
|
363
|
-
return await obj.handle_async()
|
364
|
-
|
365
|
-
setattr(self, async_handler_name, handler)
|
528
|
+
setattr(self, async_handler_name, wrapper)
|
366
529
|
elif (
|
367
530
|
hasattr(message_class, "response_async")
|
368
531
|
and inspect.isfunction(message_class.response_async)
|
langroid/agent/task.py
CHANGED
@@ -104,6 +104,10 @@ class TaskConfig(BaseModel):
|
|
104
104
|
to use string-based signaling, and it is recommended to use the
|
105
105
|
new Orchestration tools instead (see agent/tools/orchestration.py),
|
106
106
|
e.g. DoneTool, SendTool, etc.
|
107
|
+
done_if_tool (bool): whether to consider the task done if the pending message
|
108
|
+
contains a Tool attempt by the LLM
|
109
|
+
(including tools not handled by the agent).
|
110
|
+
Default is False.
|
107
111
|
|
108
112
|
"""
|
109
113
|
|
@@ -115,6 +119,7 @@ class TaskConfig(BaseModel):
|
|
115
119
|
addressing_prefix: str = ""
|
116
120
|
allow_subtask_multi_oai_tools: bool = True
|
117
121
|
recognize_string_signals: bool = True
|
122
|
+
done_if_tool: bool = False
|
118
123
|
|
119
124
|
|
120
125
|
class Task:
|
@@ -1828,6 +1833,14 @@ class Task:
|
|
1828
1833
|
if self._is_kill():
|
1829
1834
|
return (True, StatusCode.KILL)
|
1830
1835
|
result = result or self.pending_message
|
1836
|
+
|
1837
|
+
# Check if task should be done if message contains a tool
|
1838
|
+
if self.config.done_if_tool and result is not None:
|
1839
|
+
if isinstance(result, ChatDocument) and self.agent.try_get_tool_messages(
|
1840
|
+
result, all_tools=True
|
1841
|
+
):
|
1842
|
+
return (True, StatusCode.DONE)
|
1843
|
+
|
1831
1844
|
allow_done_string = self.config.recognize_string_signals
|
1832
1845
|
# An entity decided task is done, either via DoneTool,
|
1833
1846
|
# or by explicitly saying DONE
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import asyncio
|
2
2
|
import datetime
|
3
3
|
import logging
|
4
|
+
from base64 import b64decode
|
5
|
+
from io import BytesIO
|
4
6
|
from typing import Any, Dict, List, Optional, Tuple, Type, TypeAlias, cast
|
5
7
|
|
6
8
|
from dotenv import load_dotenv
|
@@ -16,9 +18,20 @@ from mcp.client.session import (
|
|
16
18
|
LoggingFnT,
|
17
19
|
MessageHandlerFnT,
|
18
20
|
)
|
19
|
-
from mcp.types import
|
21
|
+
from mcp.types import (
|
22
|
+
BlobResourceContents,
|
23
|
+
CallToolResult,
|
24
|
+
EmbeddedResource,
|
25
|
+
ImageContent,
|
26
|
+
TextContent,
|
27
|
+
TextResourceContents,
|
28
|
+
Tool,
|
29
|
+
)
|
20
30
|
|
31
|
+
from langroid.agent.base import Agent
|
32
|
+
from langroid.agent.chat_document import ChatDocument
|
21
33
|
from langroid.agent.tool_message import ToolMessage
|
34
|
+
from langroid.parsing.file_attachment import FileAttachment
|
22
35
|
from langroid.pydantic_v1 import AnyUrl, BaseModel, Field, create_model
|
23
36
|
|
24
37
|
load_dotenv() # load environment variables from .env
|
@@ -39,6 +52,10 @@ class FastMCPClient:
|
|
39
52
|
def __init__(
|
40
53
|
self,
|
41
54
|
server: FastMCPServerSpec,
|
55
|
+
persist_connection: bool = False,
|
56
|
+
forward_images: bool = True,
|
57
|
+
forward_text_resources: bool = False,
|
58
|
+
forward_blob_resources: bool = False,
|
42
59
|
sampling_handler: SamplingHandler | None = None, # type: ignore
|
43
60
|
roots: RootsList | RootsHandler | None = None, # type: ignore
|
44
61
|
log_handler: LoggingFnT | None = None,
|
@@ -58,6 +75,10 @@ class FastMCPClient:
|
|
58
75
|
self.log_handler = log_handler
|
59
76
|
self.message_handler = message_handler
|
60
77
|
self.read_timeout_seconds = read_timeout_seconds
|
78
|
+
self.persist_connection = persist_connection
|
79
|
+
self.forward_text_resources = forward_text_resources
|
80
|
+
self.forward_blob_resources = forward_blob_resources
|
81
|
+
self.forward_images = forward_images
|
61
82
|
|
62
83
|
async def __aenter__(self) -> "FastMCPClient":
|
63
84
|
"""Enter the async context manager and connect inner client."""
|
@@ -96,6 +117,19 @@ class FastMCPClient:
|
|
96
117
|
self.client = None
|
97
118
|
self._cm = None
|
98
119
|
|
120
|
+
def __del__(self) -> None:
|
121
|
+
"""Warn about unclosed persistent connections."""
|
122
|
+
if self.client is not None and self.persist_connection:
|
123
|
+
import warnings
|
124
|
+
|
125
|
+
warnings.warn(
|
126
|
+
f"FastMCPClient with persist_connection=True was not properly closed. "
|
127
|
+
f"Connection to {self.server} may leak resources. "
|
128
|
+
f"Use 'async with' or call await client.close()",
|
129
|
+
ResourceWarning,
|
130
|
+
stacklevel=2,
|
131
|
+
)
|
132
|
+
|
99
133
|
def _schema_to_field(
|
100
134
|
self, name: str, schema: Dict[str, Any], prefix: str
|
101
135
|
) -> Tuple[Any, Any]:
|
@@ -151,7 +185,13 @@ class FastMCPClient:
|
|
151
185
|
with the given `tool_name`.
|
152
186
|
"""
|
153
187
|
if not self.client:
|
154
|
-
|
188
|
+
if self.persist_connection:
|
189
|
+
await self.connect()
|
190
|
+
assert self.client
|
191
|
+
else:
|
192
|
+
raise RuntimeError(
|
193
|
+
"Client not initialized. Use async with FastMCPClient."
|
194
|
+
)
|
155
195
|
target = await self.get_mcp_tool_async(tool_name)
|
156
196
|
if target is None:
|
157
197
|
raise ValueError(f"No tool named {tool_name}")
|
@@ -209,36 +249,68 @@ class FastMCPClient:
|
|
209
249
|
tool_model._renamed_fields = renamed # type: ignore[attr-defined]
|
210
250
|
|
211
251
|
# 2) define an arg-free call_tool_async()
|
212
|
-
async def call_tool_async(
|
252
|
+
async def call_tool_async(itself: ToolMessage) -> Any:
|
213
253
|
from langroid.agent.tools.mcp.fastmcp_client import FastMCPClient
|
214
254
|
|
215
255
|
# pack up the payload
|
216
|
-
payload =
|
217
|
-
exclude=
|
256
|
+
payload = itself.dict(
|
257
|
+
exclude=itself.Config.schema_extra["exclude"].union(
|
218
258
|
["request", "purpose"]
|
219
259
|
),
|
220
260
|
)
|
221
261
|
|
222
262
|
# restore any renamed fields
|
223
|
-
for orig, new in
|
263
|
+
for orig, new in itself.__class__._renamed_fields.items(): # type: ignore
|
224
264
|
if new in payload:
|
225
265
|
payload[orig] = payload.pop(new)
|
226
266
|
|
227
|
-
client_cfg = getattr(
|
267
|
+
client_cfg = getattr(itself.__class__, "_client_config", None) # type: ignore
|
228
268
|
if not client_cfg:
|
229
269
|
# Fallback or error - ideally _client_config should always exist
|
230
|
-
raise RuntimeError(f"Client config missing on {
|
270
|
+
raise RuntimeError(f"Client config missing on {itself.__class__}")
|
271
|
+
|
272
|
+
# Connect the client if not yet connected and keep the connection open
|
273
|
+
if self.persist_connection:
|
274
|
+
if not self.client:
|
275
|
+
await self.connect()
|
276
|
+
|
277
|
+
return await self.call_mcp_tool(itself.request, payload)
|
278
|
+
|
231
279
|
# open a fresh client, call the tool, then close
|
232
280
|
async with FastMCPClient(**client_cfg) as client: # type: ignore
|
233
|
-
return await client.call_mcp_tool(
|
281
|
+
return await client.call_mcp_tool(itself.request, payload)
|
234
282
|
|
235
283
|
tool_model.call_tool_async = call_tool_async # type: ignore
|
236
284
|
|
237
285
|
if not hasattr(tool_model, "handle_async"):
|
238
|
-
# 3) define
|
239
|
-
|
240
|
-
|
241
|
-
|
286
|
+
# 3) define handle_async() method with optional agent parameter
|
287
|
+
from typing import Union
|
288
|
+
|
289
|
+
async def handle_async(
|
290
|
+
self: ToolMessage, agent: Optional[Agent] = None
|
291
|
+
) -> Union[str, Optional[ChatDocument]]:
|
292
|
+
"""
|
293
|
+
Auto-generated handler for MCP tool. Returns ChatDocument with files
|
294
|
+
if files are present and agent is provided, otherwise returns text.
|
295
|
+
|
296
|
+
To override: define your own handle_async method with matching signature
|
297
|
+
if you need file handling, or simpler signature if you only need text.
|
298
|
+
"""
|
299
|
+
response = await self.call_tool_async() # type: ignore[attr-defined]
|
300
|
+
if response is None:
|
301
|
+
return None
|
302
|
+
|
303
|
+
content, files = response
|
304
|
+
|
305
|
+
# If we have files and an agent is provided, return a ChatDocument
|
306
|
+
if files and agent is not None:
|
307
|
+
return agent.create_agent_response(
|
308
|
+
content=content,
|
309
|
+
files=files,
|
310
|
+
)
|
311
|
+
else:
|
312
|
+
# Otherwise, just return the text content
|
313
|
+
return str(content) if content is not None else None
|
242
314
|
|
243
315
|
# add the handle_async() method to the tool model
|
244
316
|
tool_model.handle_async = handle_async # type: ignore
|
@@ -251,7 +323,13 @@ class FastMCPClient:
|
|
251
323
|
handling nested schemas, with `handle_async` methods
|
252
324
|
"""
|
253
325
|
if not self.client:
|
254
|
-
|
326
|
+
if self.persist_connection:
|
327
|
+
await self.connect()
|
328
|
+
assert self.client
|
329
|
+
else:
|
330
|
+
raise RuntimeError(
|
331
|
+
"Client not initialized. Use async with FastMCPClient."
|
332
|
+
)
|
255
333
|
resp = await self.client.list_tools()
|
256
334
|
return [await self.get_tool_async(t.name) for t in resp]
|
257
335
|
|
@@ -267,7 +345,13 @@ class FastMCPClient:
|
|
267
345
|
The raw Tool object from the server, or None.
|
268
346
|
"""
|
269
347
|
if not self.client:
|
270
|
-
|
348
|
+
if self.persist_connection:
|
349
|
+
await self.connect()
|
350
|
+
assert self.client
|
351
|
+
else:
|
352
|
+
raise RuntimeError(
|
353
|
+
"Client not initialized. Use async with FastMCPClient."
|
354
|
+
)
|
271
355
|
resp: List[Tool] = await self.client.list_tools()
|
272
356
|
return next((t for t in resp if t.name == name), None)
|
273
357
|
|
@@ -275,7 +359,7 @@ class FastMCPClient:
|
|
275
359
|
self,
|
276
360
|
tool_name: str,
|
277
361
|
result: CallToolResult,
|
278
|
-
) ->
|
362
|
+
) -> Optional[str | tuple[str, list[FileAttachment]]]:
|
279
363
|
if result.isError:
|
280
364
|
# Log more detailed error information
|
281
365
|
error_content = None
|
@@ -293,26 +377,41 @@ class FastMCPClient:
|
|
293
377
|
)
|
294
378
|
return f"ERROR: Tool call failed - {error_content}"
|
295
379
|
|
296
|
-
|
297
|
-
not isinstance(item, TextContent) for item in result.content
|
298
|
-
)
|
299
|
-
if has_nontext_results:
|
300
|
-
self.logger.warning(
|
301
|
-
f"""
|
302
|
-
MCP Tool {tool_name} returned non-text results,
|
303
|
-
which will be skipped.
|
304
|
-
""",
|
305
|
-
)
|
306
|
-
results = [
|
380
|
+
results_text = [
|
307
381
|
item.text for item in result.content if isinstance(item, TextContent)
|
308
382
|
]
|
309
|
-
|
310
|
-
|
311
|
-
|
383
|
+
results_file = []
|
384
|
+
|
385
|
+
for item in result.content:
|
386
|
+
if isinstance(item, ImageContent) and self.forward_images:
|
387
|
+
results_file.append(
|
388
|
+
FileAttachment.from_bytes(
|
389
|
+
b64decode(item.data),
|
390
|
+
mime_type=item.mimeType,
|
391
|
+
)
|
392
|
+
)
|
393
|
+
elif isinstance(item, EmbeddedResource):
|
394
|
+
if (
|
395
|
+
isinstance(item.resource, TextResourceContents)
|
396
|
+
and self.forward_text_resources
|
397
|
+
):
|
398
|
+
results_text.append(item.resource.text)
|
399
|
+
elif (
|
400
|
+
isinstance(item.resource, BlobResourceContents)
|
401
|
+
and self.forward_blob_resources
|
402
|
+
):
|
403
|
+
results_file.append(
|
404
|
+
FileAttachment.from_io(
|
405
|
+
BytesIO(b64decode(item.resource.blob)),
|
406
|
+
mime_type=item.resource.mimeType,
|
407
|
+
)
|
408
|
+
)
|
409
|
+
|
410
|
+
return "\n".join(results_text), results_file
|
312
411
|
|
313
412
|
async def call_mcp_tool(
|
314
413
|
self, tool_name: str, arguments: Dict[str, Any]
|
315
|
-
) -> str
|
414
|
+
) -> Optional[tuple[str, list[FileAttachment]]]:
|
316
415
|
"""Call an MCP tool with the given arguments.
|
317
416
|
|
318
417
|
Args:
|
@@ -323,12 +422,23 @@ class FastMCPClient:
|
|
323
422
|
The result of the tool call.
|
324
423
|
"""
|
325
424
|
if not self.client:
|
326
|
-
|
425
|
+
if self.persist_connection:
|
426
|
+
await self.connect()
|
427
|
+
assert self.client
|
428
|
+
else:
|
429
|
+
raise RuntimeError(
|
430
|
+
"Client not initialized. Use async with FastMCPClient."
|
431
|
+
)
|
327
432
|
result: CallToolResult = await self.client.session.call_tool(
|
328
433
|
tool_name,
|
329
434
|
arguments,
|
330
435
|
)
|
331
|
-
|
436
|
+
results = self._convert_tool_result(tool_name, result)
|
437
|
+
|
438
|
+
if isinstance(results, str):
|
439
|
+
return results, []
|
440
|
+
|
441
|
+
return results
|
332
442
|
|
333
443
|
|
334
444
|
# ==============================================================================
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: langroid
|
3
|
-
Version: 0.54.
|
3
|
+
Version: 0.54.2
|
4
4
|
Summary: Harness LLMs with Multi-Agent Programming
|
5
5
|
Author-email: Prasad Chalasani <pchalasani@gmail.com>
|
6
6
|
License: MIT
|
@@ -344,6 +344,9 @@ teacher_task.run()
|
|
344
344
|
<details>
|
345
345
|
<summary> <b>Click to expand</b></summary>
|
346
346
|
|
347
|
+
- **Jun 2025:**
|
348
|
+
- [0.54.0](https://github.com/langroid/langroid/releases/tag/0.54.0) Portkey AI Gateway support - access 200+ models
|
349
|
+
across providers through unified API with caching, retries, observability.
|
347
350
|
- **Mar-Apr 2025:**
|
348
351
|
- [0.53.0](https://github.com/langroid/langroid/releases/tag/0.53.0) MCP Tools Support.
|
349
352
|
- [0.52.0](https://github.com/langroid/langroid/releases/tag/0.52.0) Multimodal support, i.e. allow PDF, image
|
@@ -3,12 +3,12 @@ langroid/exceptions.py,sha256=OPjece_8cwg94DLPcOGA1ddzy5bGh65pxzcHMnssTz8,2995
|
|
3
3
|
langroid/mytypes.py,sha256=HIcYAqGeA9OK0Hlscym2FI5Oax9QFljDZoVgRlomhRk,4014
|
4
4
|
langroid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
langroid/agent/__init__.py,sha256=ll0Cubd2DZ-fsCMl7e10hf9ZjFGKzphfBco396IKITY,786
|
6
|
-
langroid/agent/base.py,sha256=
|
6
|
+
langroid/agent/base.py,sha256=a45iqoWet2W60h4wUIOyHDYlotgS0asqIjfbONT4fZQ,85706
|
7
7
|
langroid/agent/batch.py,sha256=wpE9RqCNDVDhAXkCB7wEqfCIEAi6qKcrhaZ-Zr9T4C0,21375
|
8
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
|
-
langroid/agent/task.py,sha256=
|
11
|
+
langroid/agent/task.py,sha256=Ns9SoeMoAHDW7OEcPHJ22cgQKsHzWSt9UAWi9BFl4NM,91366
|
12
12
|
langroid/agent/tool_message.py,sha256=BhjP-_TfQ2tgxuY4Yo_JHLOwwt0mJ4BwjPnREvEY4vk,14744
|
13
13
|
langroid/agent/xml_tool_message.py,sha256=oeBKnJNoGaKdtz39XoWGMTNlVyXew2MWH5lgtYeh8wQ,15496
|
14
14
|
langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -56,7 +56,7 @@ langroid/agent/tools/segment_extract_tool.py,sha256=__srZ_VGYLVOdPrITUM8S0HpmX4q
|
|
56
56
|
langroid/agent/tools/tavily_search_tool.py,sha256=soI-j0HdgVQLf09wRQScaEK4b5RpAX9C4cwOivRFWWI,1903
|
57
57
|
langroid/agent/tools/mcp/__init__.py,sha256=DJNM0VeFnFS3pJKCyFGggT8JVjVu0rBzrGzasT1HaSM,387
|
58
58
|
langroid/agent/tools/mcp/decorators.py,sha256=h7dterhsmvWJ8q4mp_OopmuG2DF71ty8cZwOyzdDZuk,1127
|
59
|
-
langroid/agent/tools/mcp/fastmcp_client.py,sha256=
|
59
|
+
langroid/agent/tools/mcp/fastmcp_client.py,sha256=rxdNRinJoxFLbuTEAy7gVocC0jFRwcwDcoz9KAJN7sg,22068
|
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
|
@@ -133,7 +133,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
|
|
133
133
|
langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
|
134
134
|
langroid/vector_store/qdrantdb.py,sha256=O6dSBoDZ0jzfeVBd7LLvsXu083xs2fxXtPa9gGX3JX4,18443
|
135
135
|
langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
|
136
|
-
langroid-0.54.
|
137
|
-
langroid-0.54.
|
138
|
-
langroid-0.54.
|
139
|
-
langroid-0.54.
|
136
|
+
langroid-0.54.2.dist-info/METADATA,sha256=MyoCOXBAWkcArSbJp4sYz9ORHKlYwpcyo2zOAUxe5TQ,65395
|
137
|
+
langroid-0.54.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
138
|
+
langroid-0.54.2.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
|
139
|
+
langroid-0.54.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|