langroid 0.58.2__py3-none-any.whl → 0.59.0b1__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.
Files changed (106) hide show
  1. langroid/agent/base.py +39 -17
  2. langroid/agent/base.py-e +2216 -0
  3. langroid/agent/callbacks/chainlit.py +2 -1
  4. langroid/agent/chat_agent.py +73 -55
  5. langroid/agent/chat_agent.py-e +2086 -0
  6. langroid/agent/chat_document.py +7 -7
  7. langroid/agent/chat_document.py-e +513 -0
  8. langroid/agent/openai_assistant.py +9 -9
  9. langroid/agent/openai_assistant.py-e +882 -0
  10. langroid/agent/special/arangodb/arangodb_agent.py +10 -18
  11. langroid/agent/special/arangodb/arangodb_agent.py-e +648 -0
  12. langroid/agent/special/arangodb/tools.py +3 -3
  13. langroid/agent/special/doc_chat_agent.py +16 -14
  14. langroid/agent/special/lance_rag/critic_agent.py +2 -2
  15. langroid/agent/special/lance_rag/query_planner_agent.py +4 -4
  16. langroid/agent/special/lance_tools.py +6 -5
  17. langroid/agent/special/lance_tools.py-e +61 -0
  18. langroid/agent/special/neo4j/neo4j_chat_agent.py +3 -7
  19. langroid/agent/special/neo4j/neo4j_chat_agent.py-e +430 -0
  20. langroid/agent/special/relevance_extractor_agent.py +1 -1
  21. langroid/agent/special/sql/sql_chat_agent.py +11 -3
  22. langroid/agent/task.py +9 -87
  23. langroid/agent/task.py-e +2418 -0
  24. langroid/agent/tool_message.py +33 -17
  25. langroid/agent/tool_message.py-e +400 -0
  26. langroid/agent/tools/file_tools.py +4 -2
  27. langroid/agent/tools/file_tools.py-e +234 -0
  28. langroid/agent/tools/mcp/fastmcp_client.py +19 -6
  29. langroid/agent/tools/mcp/fastmcp_client.py-e +584 -0
  30. langroid/agent/tools/orchestration.py +22 -17
  31. langroid/agent/tools/orchestration.py-e +301 -0
  32. langroid/agent/tools/recipient_tool.py +3 -3
  33. langroid/agent/tools/task_tool.py +22 -16
  34. langroid/agent/tools/task_tool.py-e +249 -0
  35. langroid/agent/xml_tool_message.py +90 -35
  36. langroid/agent/xml_tool_message.py-e +392 -0
  37. langroid/cachedb/base.py +1 -1
  38. langroid/embedding_models/base.py +2 -2
  39. langroid/embedding_models/models.py +3 -7
  40. langroid/embedding_models/models.py-e +563 -0
  41. langroid/exceptions.py +4 -1
  42. langroid/language_models/azure_openai.py +2 -2
  43. langroid/language_models/azure_openai.py-e +134 -0
  44. langroid/language_models/base.py +6 -4
  45. langroid/language_models/base.py-e +812 -0
  46. langroid/language_models/client_cache.py +64 -0
  47. langroid/language_models/config.py +2 -4
  48. langroid/language_models/config.py-e +18 -0
  49. langroid/language_models/model_info.py +9 -1
  50. langroid/language_models/model_info.py-e +483 -0
  51. langroid/language_models/openai_gpt.py +119 -20
  52. langroid/language_models/openai_gpt.py-e +2280 -0
  53. langroid/language_models/provider_params.py +3 -22
  54. langroid/language_models/provider_params.py-e +153 -0
  55. langroid/mytypes.py +11 -4
  56. langroid/mytypes.py-e +132 -0
  57. langroid/parsing/code_parser.py +1 -1
  58. langroid/parsing/file_attachment.py +1 -1
  59. langroid/parsing/file_attachment.py-e +246 -0
  60. langroid/parsing/md_parser.py +14 -4
  61. langroid/parsing/md_parser.py-e +574 -0
  62. langroid/parsing/parser.py +22 -7
  63. langroid/parsing/parser.py-e +410 -0
  64. langroid/parsing/repo_loader.py +3 -1
  65. langroid/parsing/repo_loader.py-e +812 -0
  66. langroid/parsing/search.py +1 -1
  67. langroid/parsing/url_loader.py +17 -51
  68. langroid/parsing/url_loader.py-e +683 -0
  69. langroid/parsing/urls.py +5 -4
  70. langroid/parsing/urls.py-e +279 -0
  71. langroid/prompts/prompts_config.py +1 -1
  72. langroid/pydantic_v1/__init__.py +45 -6
  73. langroid/pydantic_v1/__init__.py-e +36 -0
  74. langroid/pydantic_v1/main.py +11 -4
  75. langroid/pydantic_v1/main.py-e +11 -0
  76. langroid/utils/configuration.py +13 -11
  77. langroid/utils/configuration.py-e +141 -0
  78. langroid/utils/constants.py +1 -1
  79. langroid/utils/constants.py-e +32 -0
  80. langroid/utils/globals.py +21 -5
  81. langroid/utils/globals.py-e +49 -0
  82. langroid/utils/html_logger.py +2 -1
  83. langroid/utils/html_logger.py-e +825 -0
  84. langroid/utils/object_registry.py +1 -1
  85. langroid/utils/object_registry.py-e +66 -0
  86. langroid/utils/pydantic_utils.py +55 -28
  87. langroid/utils/pydantic_utils.py-e +602 -0
  88. langroid/utils/types.py +2 -2
  89. langroid/utils/types.py-e +113 -0
  90. langroid/vector_store/base.py +3 -3
  91. langroid/vector_store/lancedb.py +5 -5
  92. langroid/vector_store/lancedb.py-e +404 -0
  93. langroid/vector_store/meilisearch.py +2 -2
  94. langroid/vector_store/pineconedb.py +4 -4
  95. langroid/vector_store/pineconedb.py-e +427 -0
  96. langroid/vector_store/postgres.py +1 -1
  97. langroid/vector_store/qdrantdb.py +3 -3
  98. langroid/vector_store/weaviatedb.py +1 -1
  99. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/METADATA +3 -2
  100. langroid-0.59.0b1.dist-info/RECORD +181 -0
  101. langroid/agent/special/doc_chat_task.py +0 -0
  102. langroid/mcp/__init__.py +0 -1
  103. langroid/mcp/server/__init__.py +0 -1
  104. langroid-0.58.2.dist-info/RECORD +0 -145
  105. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/WHEEL +0 -0
  106. {langroid-0.58.2.dist-info → langroid-0.59.0b1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,584 @@
1
+ import asyncio
2
+ import datetime
3
+ import logging
4
+ from base64 import b64decode
5
+ from io import BytesIO
6
+ from typing import Any, Dict, List, Optional, Tuple, Type, TypeAlias, cast
7
+
8
+ from dotenv import load_dotenv
9
+ from fastmcp.client import Client
10
+ from fastmcp.client.roots import (
11
+ RootsHandler,
12
+ RootsList,
13
+ )
14
+ from fastmcp.client.sampling import SamplingHandler
15
+ from fastmcp.client.transports import ClientTransport
16
+ from fastmcp.server import FastMCP
17
+ from mcp.client.session import (
18
+ LoggingFnT,
19
+ MessageHandlerFnT,
20
+ )
21
+ from mcp.types import (
22
+ BlobResourceContents,
23
+ CallToolResult,
24
+ EmbeddedResource,
25
+ ImageContent,
26
+ TextContent,
27
+ TextResourceContents,
28
+ Tool,
29
+ )
30
+
31
+ from langroid.agent.base import Agent
32
+ from langroid.agent.chat_document import ChatDocument
33
+ from langroid.agent.tool_message import ToolMessage
34
+ from langroid.parsing.file_attachment import FileAttachment
35
+ from pydantic import AnyUrl, BaseModel, Field, create_model
36
+
37
+ load_dotenv() # load environment variables from .env
38
+
39
+ FastMCPServerSpec: TypeAlias = str | FastMCP[Any] | ClientTransport | AnyUrl
40
+
41
+
42
+ class FastMCPClient:
43
+ """A client for interacting with a FastMCP server.
44
+
45
+ Provides async context manager functionality to safely manage resources.
46
+ """
47
+
48
+ logger = logging.getLogger(__name__)
49
+ _cm: Optional[Client] = None
50
+ client: Optional[Client] = None
51
+
52
+ def __init__(
53
+ self,
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,
59
+ sampling_handler: SamplingHandler | None = None, # type: ignore
60
+ roots: RootsList | RootsHandler | None = None, # type: ignore
61
+ log_handler: LoggingFnT | None = None,
62
+ message_handler: MessageHandlerFnT | None = None,
63
+ read_timeout_seconds: datetime.timedelta | None = None,
64
+ ) -> None:
65
+ """Initialize the FastMCPClient.
66
+
67
+ Args:
68
+ server: FastMCP server or path to such a server
69
+ """
70
+ self.server = server
71
+ self.client = None
72
+ self._cm = None
73
+ self.sampling_handler = sampling_handler
74
+ self.roots = roots
75
+ self.log_handler = log_handler
76
+ self.message_handler = message_handler
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
82
+
83
+ async def __aenter__(self) -> "FastMCPClient":
84
+ """Enter the async context manager and connect inner client."""
85
+ # create inner client context manager
86
+ self._cm = Client(
87
+ self.server,
88
+ sampling_handler=self.sampling_handler,
89
+ roots=self.roots,
90
+ log_handler=self.log_handler,
91
+ message_handler=self.message_handler,
92
+ timeout=self.read_timeout_seconds,
93
+ )
94
+ # actually enter it (opens the session)
95
+ self.client = await self._cm.__aenter__() # type: ignore
96
+ return self
97
+
98
+ async def connect(self) -> None:
99
+ """Open the underlying session."""
100
+ await self.__aenter__()
101
+
102
+ async def close(self) -> None:
103
+ """Close the underlying session."""
104
+ await self.__aexit__(None, None, None)
105
+
106
+ async def __aexit__(
107
+ self,
108
+ exc_type: Optional[type[Exception]],
109
+ exc_val: Optional[Exception],
110
+ exc_tb: Optional[Any],
111
+ ) -> None:
112
+ """Exit the async context manager and close inner client."""
113
+ # exit and close the inner fastmcp.Client
114
+ if hasattr(self, "_cm"):
115
+ if self._cm is not None:
116
+ await self._cm.__aexit__(exc_type, exc_val, exc_tb) # type: ignore
117
+ self.client = None
118
+ self._cm = None
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
+
133
+ def _schema_to_field(
134
+ self, name: str, schema: Dict[str, Any], prefix: str
135
+ ) -> Tuple[Any, Any]:
136
+ """Convert a JSON Schema snippet into a (type, Field) tuple.
137
+
138
+ Args:
139
+ name: Name of the field.
140
+ schema: JSON Schema for this field.
141
+ prefix: Prefix to use for nested model names.
142
+
143
+ Returns:
144
+ A tuple of (python_type, Field(...)) for create_model.
145
+ """
146
+ t = schema.get("type")
147
+ default = schema.get("default", ...)
148
+ desc = schema.get("description")
149
+ # Object → nested BaseModel
150
+ if t == "object" and "properties" in schema:
151
+ sub_name = f"{prefix}_{name.capitalize()}"
152
+ sub_fields: Dict[str, Tuple[type, Any]] = {}
153
+ for k, sub_s in schema["properties"].items():
154
+ ftype, fld = self._schema_to_field(sub_name + k, sub_s, sub_name)
155
+ sub_fields[k] = (ftype, fld)
156
+ submodel = create_model( # type: ignore
157
+ sub_name,
158
+ __base__=BaseModel,
159
+ **sub_fields,
160
+ )
161
+ return submodel, Field(default=default, description=desc) # type: ignore
162
+ # Array → List of items
163
+ if t == "array" and "items" in schema:
164
+ item_type, _ = self._schema_to_field(name, schema["items"], prefix)
165
+ return List[item_type], Field(default=default, description=desc) # type: ignore
166
+ # Primitive types
167
+ if t == "string":
168
+ return str, Field(default=default, description=desc)
169
+ if t == "integer":
170
+ return int, Field(default=default, description=desc)
171
+ if t == "number":
172
+ return float, Field(default=default, description=desc)
173
+ if t == "boolean":
174
+ return bool, Field(default=default, description=desc)
175
+ # Fallback or unions
176
+ if any(key in schema for key in ("oneOf", "anyOf", "allOf")):
177
+ self.logger.warning("Unsupported union schema in field %s; using Any", name)
178
+ return Any, Field(default=default, description=desc)
179
+ # Default fallback
180
+ return Any, Field(default=default, description=desc)
181
+
182
+ async def get_tool_async(self, tool_name: str) -> Type[ToolMessage]:
183
+ """
184
+ Create a Langroid ToolMessage subclass from the MCP Tool
185
+ with the given `tool_name`.
186
+ """
187
+ if not self.client:
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
+ )
195
+ target = await self.get_mcp_tool_async(tool_name)
196
+ if target is None:
197
+ raise ValueError(f"No tool named {tool_name}")
198
+ props = target.inputSchema.get("properties", {})
199
+ fields: Dict[str, Tuple[type, Any]] = {}
200
+ for fname, schema in props.items():
201
+ ftype, fld = self._schema_to_field(fname, schema, target.name)
202
+ fields[fname] = (ftype, fld)
203
+
204
+ # Convert target.name to CamelCase and add Tool suffix
205
+ parts = target.name.replace("-", "_").split("_")
206
+ camel_case = "".join(part.capitalize() for part in parts)
207
+ model_name = f"{camel_case}Tool"
208
+
209
+ from langroid.agent.tool_message import ToolMessage as _BaseToolMessage
210
+
211
+ # IMPORTANT: Avoid clashes with reserved field names in Langroid ToolMessage!
212
+ # First figure out which field names are reserved
213
+ reserved = set(_BaseToolMessage.__annotations__.keys())
214
+ reserved.update(["recipient", "_handler", "name"])
215
+ renamed: Dict[str, str] = {}
216
+ new_fields: Dict[str, Tuple[type, Any]] = {}
217
+ for fname, (ftype, fld) in fields.items():
218
+ if fname in reserved:
219
+ new_name = fname + "__"
220
+ renamed[fname] = new_name
221
+ new_fields[new_name] = (ftype, fld)
222
+ else:
223
+ new_fields[fname] = (ftype, fld)
224
+ # now replace fields with our renamed‐aware mapping
225
+ fields = new_fields
226
+
227
+ # create Langroid ToolMessage subclass, with expected fields.
228
+ tool_model = cast(
229
+ Type[ToolMessage],
230
+ create_model( # type: ignore[call-overload]
231
+ model_name,
232
+ request=(str, target.name),
233
+ purpose=(str, target.description or f"Use the tool {target.name}"),
234
+ __base__=ToolMessage,
235
+ **fields,
236
+ ),
237
+ )
238
+ # Store ALL client configuration needed to recreate a client
239
+ client_config = {
240
+ "server": self.server,
241
+ "sampling_handler": self.sampling_handler,
242
+ "roots": self.roots,
243
+ "log_handler": self.log_handler,
244
+ "message_handler": self.message_handler,
245
+ "read_timeout_seconds": self.read_timeout_seconds,
246
+ }
247
+
248
+ tool_model._client_config = client_config # type: ignore [attr-defined]
249
+ tool_model._renamed_fields = renamed # type: ignore[attr-defined]
250
+
251
+ # 2) define an arg-free call_tool_async()
252
+ async def call_tool_async(itself: ToolMessage) -> Any:
253
+ from langroid.agent.tools.mcp.fastmcp_client import FastMCPClient
254
+
255
+ # pack up the payload
256
+ payload = itself.model_dump(
257
+ exclude=itself.model_config["json_schema_extra"]["exclude"].union(
258
+ ["request", "purpose"]
259
+ ),
260
+ )
261
+
262
+ # restore any renamed fields
263
+ for orig, new in itself.__class__._renamed_fields.items(): # type: ignore
264
+ if new in payload:
265
+ payload[orig] = payload.pop(new)
266
+
267
+ client_cfg = getattr(itself.__class__, "_client_config", None) # type: ignore
268
+ if not client_cfg:
269
+ # Fallback or error - ideally _client_config should always exist
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
+
279
+ # open a fresh client, call the tool, then close
280
+ async with FastMCPClient(**client_cfg) as client: # type: ignore
281
+ return await client.call_mcp_tool(itself.request, payload)
282
+
283
+ tool_model.call_tool_async = call_tool_async # type: ignore
284
+
285
+ if not hasattr(tool_model, "handle_async"):
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
314
+
315
+ # add the handle_async() method to the tool model
316
+ tool_model.handle_async = handle_async # type: ignore
317
+
318
+ return tool_model
319
+
320
+ async def get_tools_async(self) -> List[Type[ToolMessage]]:
321
+ """
322
+ Get all available tools as Langroid ToolMessage classes,
323
+ handling nested schemas, with `handle_async` methods
324
+ """
325
+ if not self.client:
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
+ )
333
+ resp = await self.client.list_tools()
334
+ return [await self.get_tool_async(t.name) for t in resp]
335
+
336
+ async def get_mcp_tool_async(self, name: str) -> Optional[Tool]:
337
+ """Find the "original" MCP Tool (i.e. of type mcp.types.Tool) on the server
338
+ matching `name`, or None if missing. This contains the metadata for the tool:
339
+ name, description, inputSchema, etc.
340
+
341
+ Args:
342
+ name: Name of the tool to look up.
343
+
344
+ Returns:
345
+ The raw Tool object from the server, or None.
346
+ """
347
+ if not self.client:
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
+ )
355
+ resp: List[Tool] = await self.client.list_tools()
356
+ return next((t for t in resp if t.name == name), None)
357
+
358
+ def _convert_tool_result(
359
+ self,
360
+ tool_name: str,
361
+ result: CallToolResult,
362
+ ) -> Optional[str | tuple[str, list[FileAttachment]]]:
363
+ if result.isError:
364
+ # Log more detailed error information
365
+ error_content = None
366
+ if result.content and len(result.content) > 0:
367
+ try:
368
+ error_content = [
369
+ item.text if hasattr(item, "text") else str(item)
370
+ for item in result.content
371
+ ]
372
+ except Exception as e:
373
+ error_content = [f"Could not extract error content: {str(e)}"]
374
+
375
+ self.logger.error(
376
+ f"Error calling MCP tool {tool_name}. Details: {error_content}"
377
+ )
378
+ return f"ERROR: Tool call failed - {error_content}"
379
+
380
+ results_text = [
381
+ item.text for item in result.content if isinstance(item, TextContent)
382
+ ]
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
411
+
412
+ async def call_mcp_tool(
413
+ self, tool_name: str, arguments: Dict[str, Any]
414
+ ) -> Optional[tuple[str, list[FileAttachment]]]:
415
+ """Call an MCP tool with the given arguments.
416
+
417
+ Args:
418
+ tool_name: Name of the tool to call.
419
+ arguments: Arguments to pass to the tool.
420
+
421
+ Returns:
422
+ The result of the tool call.
423
+ """
424
+ if not self.client:
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
+ )
432
+ result: CallToolResult = await self.client.session.call_tool(
433
+ tool_name,
434
+ arguments,
435
+ )
436
+ results = self._convert_tool_result(tool_name, result)
437
+
438
+ if isinstance(results, str):
439
+ return results, []
440
+
441
+ return results
442
+
443
+
444
+ # ==============================================================================
445
+ # Convenience functions (wrappers around FastMCPClient methods)
446
+ # These are useful for one-off calls without needing to manage the
447
+ # FastMCPClient context explicitly.
448
+ # ==============================================================================
449
+
450
+
451
+ async def get_tool_async(
452
+ server: FastMCPServerSpec,
453
+ tool_name: str,
454
+ **client_kwargs: Any,
455
+ ) -> Type[ToolMessage]:
456
+ """Get a single Langroid ToolMessage subclass for a specific MCP tool name (async).
457
+
458
+ This is a convenience wrapper that creates a temporary FastMCPClient.
459
+
460
+ Args:
461
+ server: Specification of the FastMCP server to connect to.
462
+ tool_name: The name of the tool to retrieve.
463
+ **client_kwargs: Additional keyword arguments to pass to the
464
+ FastMCPClient constructor (e.g., sampling_handler, roots).
465
+
466
+ Returns:
467
+ A dynamically created Langroid ToolMessage subclass representing the
468
+ requested tool.
469
+ """
470
+ async with FastMCPClient(server, **client_kwargs) as client:
471
+ return await client.get_tool_async(tool_name)
472
+
473
+
474
+ def get_tool(
475
+ server: FastMCPServerSpec,
476
+ tool_name: str,
477
+ **client_kwargs: Any,
478
+ ) -> Type[ToolMessage]:
479
+ """Get a single Langroid ToolMessage subclass
480
+ for a specific MCP tool name (synchronous).
481
+
482
+ This is a convenience wrapper that creates a temporary FastMCPClient and runs the
483
+ async `get_tool_async` function using `asyncio.run()`.
484
+
485
+ Args:
486
+ server: Specification of the FastMCP server to connect to.
487
+ tool_name: The name of the tool to retrieve.
488
+ **client_kwargs: Additional keyword arguments to pass to the
489
+ FastMCPClient constructor (e.g., sampling_handler, roots).
490
+
491
+ Returns:
492
+ A dynamically created Langroid ToolMessage subclass representing the
493
+ requested tool.
494
+ """
495
+ return asyncio.run(get_tool_async(server, tool_name, **client_kwargs))
496
+
497
+
498
+ async def get_tools_async(
499
+ server: FastMCPServerSpec,
500
+ **client_kwargs: Any,
501
+ ) -> List[Type[ToolMessage]]:
502
+ """Get all available tools as Langroid ToolMessage subclasses (async).
503
+
504
+ This is a convenience wrapper that creates a temporary FastMCPClient.
505
+
506
+ Args:
507
+ server: Specification of the FastMCP server to connect to.
508
+ **client_kwargs: Additional keyword arguments to pass to the
509
+ FastMCPClient constructor (e.g., sampling_handler, roots).
510
+
511
+ Returns:
512
+ A list of dynamically created Langroid ToolMessage subclasses
513
+ representing all available tools on the server.
514
+ """
515
+ async with FastMCPClient(server, **client_kwargs) as client:
516
+ return await client.get_tools_async()
517
+
518
+
519
+ def get_tools(
520
+ server: FastMCPServerSpec,
521
+ **client_kwargs: Any,
522
+ ) -> List[Type[ToolMessage]]:
523
+ """Get all available tools as Langroid ToolMessage subclasses (synchronous).
524
+
525
+ This is a convenience wrapper that creates a temporary FastMCPClient and runs the
526
+ async `get_tools_async` function using `asyncio.run()`.
527
+
528
+ Args:
529
+ server: Specification of the FastMCP server to connect to.
530
+ **client_kwargs: Additional keyword arguments to pass to the
531
+ FastMCPClient constructor (e.g., sampling_handler, roots).
532
+
533
+ Returns:
534
+ A list of dynamically created Langroid ToolMessage subclasses
535
+ representing all available tools on the server.
536
+ """
537
+ return asyncio.run(get_tools_async(server, **client_kwargs))
538
+
539
+
540
+ async def get_mcp_tool_async(
541
+ server: FastMCPServerSpec,
542
+ name: str,
543
+ **client_kwargs: Any,
544
+ ) -> Optional[Tool]:
545
+ """Get the raw MCP Tool object for a specific tool name (async).
546
+
547
+ This is a convenience wrapper that creates a temporary FastMCPClient to
548
+ retrieve the tool definition from the server.
549
+
550
+ Args:
551
+ server: Specification of the FastMCP server to connect to.
552
+ name: The name of the tool to look up.
553
+ **client_kwargs: Additional keyword arguments to pass to the
554
+ FastMCPClient constructor.
555
+
556
+ Returns:
557
+ The raw `mcp.types.Tool` object from the server, or `None` if the tool
558
+ is not found.
559
+ """
560
+ async with FastMCPClient(server, **client_kwargs) as client:
561
+ return await client.get_mcp_tool_async(name)
562
+
563
+
564
+ async def get_mcp_tools_async(
565
+ server: FastMCPServerSpec,
566
+ **client_kwargs: Any,
567
+ ) -> List[Tool]:
568
+ """Get all available raw MCP Tool objects from the server (async).
569
+
570
+ This is a convenience wrapper that creates a temporary FastMCPClient to
571
+ retrieve the list of tool definitions from the server.
572
+
573
+ Args:
574
+ server: Specification of the FastMCP server to connect to.
575
+ **client_kwargs: Additional keyword arguments to pass to the
576
+ FastMCPClient constructor.
577
+
578
+ Returns:
579
+ A list of raw `mcp.types.Tool` objects available on the server.
580
+ """
581
+ async with FastMCPClient(server, **client_kwargs) as client:
582
+ if not client.client:
583
+ raise RuntimeError("Client not initialized. Use async with FastMCPClient.")
584
+ return await client.client.list_tools()
@@ -5,11 +5,12 @@ termination, routing to another agent, etc.
5
5
 
6
6
  from typing import Any, List, Tuple
7
7
 
8
+ from pydantic import ConfigDict, field_validator
9
+
8
10
  from langroid.agent.chat_agent import ChatAgent
9
11
  from langroid.agent.chat_document import ChatDocument
10
12
  from langroid.agent.tool_message import ToolMessage
11
13
  from langroid.mytypes import Entity
12
- from langroid.pydantic_v1 import Extra
13
14
  from langroid.utils.types import to_string
14
15
 
15
16
 
@@ -48,6 +49,12 @@ class DoneTool(ToolMessage):
48
49
  request: str = "done_tool"
49
50
  content: str = ""
50
51
 
52
+ @field_validator("content", mode="before")
53
+ @classmethod
54
+ def convert_content_to_string(cls, v: Any) -> str:
55
+ """Convert content to string if it's not already."""
56
+ return str(v) if v is not None else ""
57
+
51
58
  def response(self, agent: ChatAgent) -> ChatDocument:
52
59
  return agent.create_agent_response(
53
60
  content=self.content,
@@ -90,14 +97,13 @@ class ResultTool(ToolMessage):
90
97
  purpose: str = "Ignored; Wrapper for a structured message"
91
98
  id: str = "" # placeholder for OpenAI-API tool_call_id
92
99
 
93
- class Config:
94
- extra = Extra.allow
95
- arbitrary_types_allowed = False
96
- validate_all = True
97
- validate_assignment = True
98
- # do not include these fields in the generated schema
99
- # since we don't require the LLM to specify them
100
- schema_extra = {"exclude": {"purpose", "id", "strict"}}
100
+ model_config = ConfigDict(
101
+ extra="allow",
102
+ arbitrary_types_allowed=False,
103
+ validate_default=True,
104
+ validate_assignment=True,
105
+ json_schema_extra={"exclude": ["purpose", "id", "strict"]},
106
+ )
101
107
 
102
108
  def handle(self) -> AgentDoneTool:
103
109
  return AgentDoneTool(tools=[self])
@@ -132,14 +138,13 @@ class FinalResultTool(ToolMessage):
132
138
  id: str = "" # placeholder for OpenAI-API tool_call_id
133
139
  _allow_llm_use: bool = False
134
140
 
135
- class Config:
136
- extra = Extra.allow
137
- arbitrary_types_allowed = False
138
- validate_all = True
139
- validate_assignment = True
140
- # do not include these fields in the generated schema
141
- # since we don't require the LLM to specify them
142
- schema_extra = {"exclude": {"purpose", "id", "strict"}}
141
+ model_config = ConfigDict(
142
+ extra="allow",
143
+ arbitrary_types_allowed=False,
144
+ validate_default=True,
145
+ validate_assignment=True,
146
+ json_schema_extra={"exclude": ["purpose", "id", "strict"]},
147
+ )
143
148
 
144
149
 
145
150
  class PassTool(ToolMessage):