langroid 0.58.3__py3-none-any.whl → 0.59.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.
Files changed (63) hide show
  1. langroid/agent/base.py +39 -17
  2. langroid/agent/callbacks/chainlit.py +2 -1
  3. langroid/agent/chat_agent.py +73 -55
  4. langroid/agent/chat_document.py +7 -7
  5. langroid/agent/done_sequence_parser.py +46 -11
  6. langroid/agent/openai_assistant.py +9 -9
  7. langroid/agent/special/arangodb/arangodb_agent.py +10 -18
  8. langroid/agent/special/arangodb/tools.py +3 -3
  9. langroid/agent/special/doc_chat_agent.py +16 -14
  10. langroid/agent/special/lance_rag/critic_agent.py +2 -2
  11. langroid/agent/special/lance_rag/query_planner_agent.py +4 -4
  12. langroid/agent/special/lance_tools.py +6 -5
  13. langroid/agent/special/neo4j/neo4j_chat_agent.py +3 -7
  14. langroid/agent/special/relevance_extractor_agent.py +1 -1
  15. langroid/agent/special/sql/sql_chat_agent.py +11 -3
  16. langroid/agent/task.py +53 -94
  17. langroid/agent/tool_message.py +33 -17
  18. langroid/agent/tools/file_tools.py +4 -2
  19. langroid/agent/tools/mcp/fastmcp_client.py +19 -6
  20. langroid/agent/tools/orchestration.py +22 -17
  21. langroid/agent/tools/recipient_tool.py +3 -3
  22. langroid/agent/tools/task_tool.py +22 -16
  23. langroid/agent/xml_tool_message.py +90 -35
  24. langroid/cachedb/base.py +1 -1
  25. langroid/embedding_models/base.py +2 -2
  26. langroid/embedding_models/models.py +3 -7
  27. langroid/exceptions.py +4 -1
  28. langroid/language_models/azure_openai.py +2 -2
  29. langroid/language_models/base.py +6 -4
  30. langroid/language_models/config.py +2 -4
  31. langroid/language_models/model_info.py +9 -1
  32. langroid/language_models/openai_gpt.py +53 -18
  33. langroid/language_models/provider_params.py +3 -22
  34. langroid/mytypes.py +11 -4
  35. langroid/parsing/code_parser.py +1 -1
  36. langroid/parsing/file_attachment.py +1 -1
  37. langroid/parsing/md_parser.py +14 -4
  38. langroid/parsing/parser.py +22 -7
  39. langroid/parsing/repo_loader.py +3 -1
  40. langroid/parsing/search.py +1 -1
  41. langroid/parsing/url_loader.py +17 -51
  42. langroid/parsing/urls.py +5 -4
  43. langroid/prompts/prompts_config.py +1 -1
  44. langroid/pydantic_v1/__init__.py +61 -4
  45. langroid/pydantic_v1/main.py +10 -4
  46. langroid/utils/configuration.py +13 -11
  47. langroid/utils/constants.py +1 -1
  48. langroid/utils/globals.py +21 -5
  49. langroid/utils/html_logger.py +2 -1
  50. langroid/utils/object_registry.py +1 -1
  51. langroid/utils/pydantic_utils.py +55 -28
  52. langroid/utils/types.py +2 -2
  53. langroid/vector_store/base.py +3 -3
  54. langroid/vector_store/lancedb.py +5 -5
  55. langroid/vector_store/meilisearch.py +2 -2
  56. langroid/vector_store/pineconedb.py +4 -4
  57. langroid/vector_store/postgres.py +1 -1
  58. langroid/vector_store/qdrantdb.py +3 -3
  59. langroid/vector_store/weaviatedb.py +1 -1
  60. {langroid-0.58.3.dist-info → langroid-0.59.0.dist-info}/METADATA +3 -2
  61. {langroid-0.58.3.dist-info → langroid-0.59.0.dist-info}/RECORD +63 -63
  62. {langroid-0.58.3.dist-info → langroid-0.59.0.dist-info}/WHEEL +0 -0
  63. {langroid-0.58.3.dist-info → langroid-0.59.0.dist-info}/licenses/LICENSE +0 -0
langroid/agent/task.py CHANGED
@@ -27,6 +27,7 @@ from typing import (
27
27
  )
28
28
 
29
29
  import numpy as np
30
+ from pydantic import BaseModel, ConfigDict
30
31
  from rich import print
31
32
  from rich.markup import escape
32
33
 
@@ -45,7 +46,6 @@ from langroid.exceptions import InfiniteLoopException
45
46
  from langroid.mytypes import Entity
46
47
  from langroid.parsing.parse_json import extract_top_level_json
47
48
  from langroid.parsing.routing import parse_addressed_message
48
- from langroid.pydantic_v1 import BaseModel
49
49
  from langroid.utils.configuration import settings
50
50
  from langroid.utils.constants import (
51
51
  DONE,
@@ -90,6 +90,9 @@ class AgentEvent(BaseModel):
90
90
 
91
91
  event_type: EventType
92
92
  tool_name: Optional[str] = None # For SPECIFIC_TOOL
93
+ tool_class: Optional[Type[Any]] = (
94
+ None # For storing tool class references when using SPECIFIC_TOOL events
95
+ )
93
96
  content_pattern: Optional[str] = None # For CONTENT_MATCH (regex)
94
97
  responder: Optional[str] = None # Specific responder name
95
98
  # Optionally match only if the responder was specific entity/task
@@ -146,6 +149,7 @@ class TaskConfig(BaseModel):
146
149
  done_sequences (List[DoneSequence]): List of event sequences that trigger task
147
150
  completion. Task is done if ANY sequence matches the recent event history.
148
151
  Each sequence is checked against the message parent chain.
152
+ Tool classes can be referenced in sequences like "T[MyToolClass]".
149
153
 
150
154
  """
151
155
 
@@ -298,14 +302,8 @@ class Task:
298
302
  set_parent_agent=noop_fn,
299
303
  )
300
304
  self.config = config
301
- # Store parsed done sequences
305
+ # Store parsed done sequences (will be initialized after agent assignment)
302
306
  self._parsed_done_sequences: Optional[List[DoneSequence]] = None
303
- if self.config.done_sequences:
304
- from .done_sequence_parser import parse_done_sequences
305
-
306
- self._parsed_done_sequences = parse_done_sequences(
307
- self.config.done_sequences
308
- )
309
307
  # how to behave as a sub-task; can be overridden by `add_sub_task()`
310
308
  self.config_sub_task = copy.deepcopy(config)
311
309
  # counts of distinct pending messages in history,
@@ -340,6 +338,21 @@ class Task:
340
338
  self.agent.set_system_message(system_message)
341
339
  if user_message:
342
340
  self.agent.set_user_message(user_message)
341
+
342
+ # Initialize parsed done sequences now that self.agent is available
343
+ if self.config.done_sequences:
344
+ from .done_sequence_parser import parse_done_sequences
345
+
346
+ # Pass agent's llm_tools_map directly
347
+ tools_map = (
348
+ self.agent.llm_tools_map
349
+ if hasattr(self.agent, "llm_tools_map")
350
+ else None
351
+ )
352
+ self._parsed_done_sequences = parse_done_sequences(
353
+ self.config.done_sequences, tools_map
354
+ )
355
+
343
356
  self.max_cost: float = 0
344
357
  self.max_tokens: int = 0
345
358
  self.session_id: str = ""
@@ -483,7 +496,7 @@ class Task:
483
496
  self.message_counter.clear()
484
497
  # create a unique string that will not likely be in any message,
485
498
  # so we always have a message with count=1
486
- self.message_counter.update([hash("___NO_MESSAGE___")])
499
+ self.message_counter.update([str(hash("___NO_MESSAGE___"))])
487
500
 
488
501
  def _cache_session_store(self, key: str, value: str) -> None:
489
502
  """
@@ -2083,7 +2096,7 @@ class Task:
2083
2096
  """
2084
2097
  from langroid.agent.chat_document import ChatDocLoggerFields
2085
2098
 
2086
- default_values = ChatDocLoggerFields().dict().values()
2099
+ default_values = ChatDocLoggerFields().model_dump().values()
2087
2100
  msg_str_tsv = "\t".join(str(v) for v in default_values)
2088
2101
  if msg is not None:
2089
2102
  msg_str_tsv = msg.tsv_str()
@@ -2144,7 +2157,7 @@ class Task:
2144
2157
  else:
2145
2158
  # Get fields from the message
2146
2159
  fields = msg.log_fields()
2147
- fields_dict = fields.dict()
2160
+ fields_dict = fields.model_dump()
2148
2161
  fields_dict.update(
2149
2162
  {
2150
2163
  "responder": str(resp),
@@ -2155,13 +2168,11 @@ class Task:
2155
2168
 
2156
2169
  # Create a ChatDocLoggerFields-like object for the HTML logger
2157
2170
  # Create a simple BaseModel subclass dynamically
2158
- from langroid.pydantic_v1 import BaseModel
2171
+ from pydantic import BaseModel
2159
2172
 
2160
2173
  class LogFields(BaseModel):
2161
- class Config:
2162
- extra = "allow" # Allow extra fields
2174
+ model_config = ConfigDict(extra="allow") # Allow extra fields
2163
2175
 
2164
- # Create instance with the fields from fields_dict
2165
2176
  log_obj = LogFields(**fields_dict)
2166
2177
  self.html_logger.log(log_obj)
2167
2178
 
@@ -2173,8 +2184,6 @@ class Task:
2173
2184
  """
2174
2185
  if recipient == "":
2175
2186
  return True
2176
- # native responders names are USER, LLM, AGENT,
2177
- # and the names of subtasks are from Task.name attribute
2178
2187
  responder_names = [self.name.lower()] + [
2179
2188
  r.name.lower() for r in self.responders
2180
2189
  ]
@@ -2184,7 +2193,6 @@ class Task:
2184
2193
  """
2185
2194
  Is the recipient explicitly specified and does not match responder "e" ?
2186
2195
  """
2187
- # Note that recipient could be specified as an Entity or a Task name
2188
2196
  return (
2189
2197
  self.pending_message is not None
2190
2198
  and (recipient := self.pending_message.metadata.recipient) != ""
@@ -2195,8 +2203,6 @@ class Task:
2195
2203
 
2196
2204
  def _user_can_respond(self) -> bool:
2197
2205
  return self.interactive or (
2198
- # regardless of self.interactive, if a msg is explicitly addressed to
2199
- # user, then wait for user response
2200
2206
  self.pending_message is not None
2201
2207
  and self.pending_message.metadata.recipient == Entity.USER
2202
2208
  and not self.agent.has_tool_message_attempt(self.pending_message)
@@ -2204,19 +2210,13 @@ class Task:
2204
2210
 
2205
2211
  def _can_respond(self, e: Responder) -> bool:
2206
2212
  user_can_respond = self._user_can_respond()
2207
-
2208
2213
  if self.pending_sender == e or (e == Entity.USER and not user_can_respond):
2209
- # sender is same as e (an entity cannot respond to its own msg),
2210
- # or user cannot respond
2211
2214
  return False
2212
-
2213
2215
  if self.pending_message is None:
2214
2216
  return True
2215
2217
  if isinstance(e, Task) and not e.agent.can_respond(self.pending_message):
2216
2218
  return False
2217
-
2218
2219
  if self._recipient_mismatch(e):
2219
- # Cannot respond if not addressed to this entity
2220
2220
  return False
2221
2221
  return self.pending_message.metadata.block != e
2222
2222
 
@@ -2230,7 +2230,6 @@ class Task:
2230
2230
  Args:
2231
2231
  enable (bool): value of `self.color_log` to set to,
2232
2232
  which will enable/diable rich logging
2233
-
2234
2233
  """
2235
2234
  self.color_log = enable
2236
2235
 
@@ -2247,16 +2246,13 @@ class Task:
2247
2246
  Args:
2248
2247
  msg (ChatDocument|str|None): message to parse
2249
2248
  addressing_prefix (str): prefix to address other agents or entities,
2250
- (e.g. "@". See documentation of `TaskConfig` for details).
2249
+ (e.g. "@". See documentation of `TaskConfig` for details).
2251
2250
  Returns:
2252
2251
  Tuple[bool|None, str|None, str|None]:
2253
2252
  bool: true=PASS, false=SEND, or None if neither
2254
2253
  str: recipient, or None
2255
2254
  str: content to send, or None
2256
2255
  """
2257
- # handle routing instruction-strings in result if any,
2258
- # such as PASS, PASS_TO, or SEND
2259
-
2260
2256
  msg_str = msg.content if isinstance(msg, ChatDocument) else msg
2261
2257
  if (
2262
2258
  self.agent.has_tool_message_attempt(msg)
@@ -2264,10 +2260,7 @@ class Task:
2264
2260
  and not msg_str.startswith(PASS_TO)
2265
2261
  and not msg_str.startswith(SEND_TO)
2266
2262
  ):
2267
- # if there's an attempted tool-call, we ignore any routing strings,
2268
- # unless they are at the start of the msg
2269
2263
  return None, None, None
2270
-
2271
2264
  content = msg.content if isinstance(msg, ChatDocument) else msg
2272
2265
  content = content.strip()
2273
2266
  if PASS in content and PASS_TO not in content:
@@ -2279,10 +2272,7 @@ class Task:
2279
2272
  and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
2280
2273
  is not None
2281
2274
  ):
2282
- # Note this will discard any portion of content BEFORE SEND_TO.
2283
- # TODO maybe make this configurable.
2284
2275
  (addressee, content_to_send) = addressee_content
2285
- # if no content then treat same as PASS_TO
2286
2276
  if content_to_send == "":
2287
2277
  return True, addressee, None
2288
2278
  else:
@@ -2296,12 +2286,10 @@ class Task:
2296
2286
  is not None
2297
2287
  ):
2298
2288
  (addressee, content_to_send) = addressee_content
2299
- # if no content then treat same as PASS_TO
2300
2289
  if content_to_send == "":
2301
2290
  return True, addressee, None
2302
2291
  else:
2303
2292
  return False, addressee, content_to_send
2304
-
2305
2293
  return None, None, None
2306
2294
 
2307
2295
  def _classify_event(
@@ -2310,19 +2298,13 @@ class Task:
2310
2298
  """Classify a message into an AgentEvent for sequence matching."""
2311
2299
  if msg is None:
2312
2300
  return AgentEvent(event_type=EventType.NO_RESPONSE)
2313
-
2314
- # Determine the event type based on responder and message content
2315
2301
  event_type = EventType.NO_RESPONSE
2316
2302
  tool_name = None
2317
-
2318
- # Check if there are tool messages
2319
2303
  tool_messages = self.agent.try_get_tool_messages(msg, all_tools=True)
2320
2304
  if tool_messages:
2321
2305
  event_type = EventType.TOOL
2322
2306
  if len(tool_messages) == 1:
2323
2307
  tool_name = tool_messages[0].request
2324
-
2325
- # Check responder type
2326
2308
  if responder == Entity.LLM and not tool_messages:
2327
2309
  event_type = EventType.LLM_RESPONSE
2328
2310
  elif responder == Entity.AGENT:
@@ -2330,21 +2312,17 @@ class Task:
2330
2312
  elif responder == Entity.USER:
2331
2313
  event_type = EventType.USER_RESPONSE
2332
2314
  elif isinstance(responder, Task):
2333
- # For sub-task responses, check the sender in metadata
2334
2315
  if msg.metadata.sender == Entity.LLM:
2335
2316
  event_type = EventType.LLM_RESPONSE
2336
2317
  elif msg.metadata.sender == Entity.AGENT:
2337
2318
  event_type = EventType.AGENT_RESPONSE
2338
2319
  else:
2339
2320
  event_type = EventType.USER_RESPONSE
2340
-
2341
- # Get sender name
2342
2321
  sender_name = None
2343
2322
  if isinstance(responder, Entity):
2344
2323
  sender_name = responder.value
2345
2324
  elif isinstance(responder, Task):
2346
2325
  sender_name = responder.name
2347
-
2348
2326
  return AgentEvent(
2349
2327
  event_type=event_type,
2350
2328
  tool_name=tool_name,
@@ -2356,71 +2334,74 @@ class Task:
2356
2334
  ) -> List[ChatDocument]:
2357
2335
  """Get the chain of messages from response sequence."""
2358
2336
  if max_depth is None:
2359
- # Get max depth needed from all sequences
2360
2337
  max_depth = 50 # default fallback
2361
- if self._parsed_done_sequences:
2362
- max_depth = max(len(seq.events) for seq in self._parsed_done_sequences)
2363
-
2364
- # Simply return the last max_depth elements from response_sequence
2338
+ if self._parsed_done_sequences:
2339
+ max_depth = max(len(seq.events) for seq in self._parsed_done_sequences)
2365
2340
  return self.response_sequence[-max_depth:]
2366
2341
 
2367
2342
  def _matches_event(self, actual: AgentEvent, expected: AgentEvent) -> bool:
2368
2343
  """Check if an actual event matches an expected event pattern."""
2369
- # Check event type
2370
2344
  if expected.event_type == EventType.SPECIFIC_TOOL:
2371
2345
  if actual.event_type != EventType.TOOL:
2372
2346
  return False
2347
+
2348
+ # First try tool_class matching if available
2349
+ if expected.tool_class is not None:
2350
+ # Handle case where actual.tool_class might be a class instance
2351
+ if hasattr(actual, "tool_class") and actual.tool_class is not None:
2352
+ # If actual.tool_class is an instance, get its class
2353
+ if isinstance(actual.tool_class, type):
2354
+ actual_class = actual.tool_class
2355
+ else:
2356
+ actual_class = type(actual.tool_class)
2357
+
2358
+ # Compare the tool classes
2359
+ if actual_class == expected.tool_class:
2360
+ return True
2361
+ # Also check if actual tool is an instance of expected class
2362
+ if not isinstance(actual.tool_class, type) and isinstance(
2363
+ actual.tool_class, expected.tool_class
2364
+ ):
2365
+ return True
2366
+
2367
+ # If tool_class comparison didn't match, continue to tool_name fallback
2368
+
2369
+ # Fall back to tool_name comparison for backwards compatibility
2373
2370
  if expected.tool_name and actual.tool_name != expected.tool_name:
2374
2371
  return False
2372
+
2375
2373
  elif actual.event_type != expected.event_type:
2376
2374
  return False
2377
-
2378
- # Check sender if specified
2379
2375
  if expected.sender and actual.sender != expected.sender:
2380
2376
  return False
2381
-
2382
- # TODO: Add content pattern matching for CONTENT_MATCH type
2383
-
2384
2377
  return True
2385
2378
 
2386
2379
  def _matches_sequence(
2387
2380
  self, msg_chain: List[ChatDocument], sequence: DoneSequence
2388
2381
  ) -> bool:
2389
2382
  """Check if a message chain matches a done sequence.
2390
-
2391
2383
  We traverse the message chain and try to match the sequence events.
2392
2384
  The events don't have to be consecutive in the chain.
2393
2385
  """
2394
2386
  if not sequence.events:
2395
2387
  return False
2396
-
2397
- # Convert messages to events
2398
2388
  events = []
2399
2389
  for i, msg in enumerate(msg_chain):
2400
- # Determine responder from metadata or by checking previous message
2401
2390
  responder = None
2402
2391
  if msg.metadata.sender:
2403
2392
  responder = msg.metadata.sender
2404
2393
  elif msg.metadata.sender_name:
2405
- # Could be a task name - keep as None for now since we can't resolve
2406
- # the actual Task object from just the name
2407
2394
  responder = None
2408
-
2409
2395
  event = self._classify_event(msg, responder)
2410
2396
  if event:
2411
2397
  events.append(event)
2412
-
2413
- # Try to match the sequence
2414
2398
  seq_idx = 0
2415
2399
  for event in events:
2416
2400
  if seq_idx >= len(sequence.events):
2417
2401
  break
2418
-
2419
2402
  expected = sequence.events[seq_idx]
2420
2403
  if self._matches_event(event, expected):
2421
2404
  seq_idx += 1
2422
-
2423
- # Check if we matched the entire sequence
2424
2405
  return seq_idx == len(sequence.events)
2425
2406
 
2426
2407
  def close_loggers(self) -> None:
@@ -2438,43 +2419,26 @@ class Task:
2438
2419
  current_responder: Optional[Responder],
2439
2420
  ) -> bool:
2440
2421
  """Check if the message chain plus current message matches a done sequence.
2441
-
2442
2422
  Process messages in reverse order (newest first) and match against
2443
2423
  the sequence events in reverse order.
2444
2424
  """
2445
- # Add current message to chain if not already there
2446
2425
  if not msg_chain or msg_chain[-1].id() != current_msg.id():
2447
2426
  msg_chain = msg_chain + [current_msg]
2448
-
2449
- # If we don't have enough messages for the sequence, can't match
2450
2427
  if len(msg_chain) < len(sequence.events):
2451
2428
  return False
2452
-
2453
- # Process in reverse order - start from the end of both lists
2454
2429
  seq_idx = len(sequence.events) - 1
2455
2430
  msg_idx = len(msg_chain) - 1
2456
-
2457
2431
  while seq_idx >= 0 and msg_idx >= 0:
2458
2432
  msg = msg_chain[msg_idx]
2459
2433
  expected = sequence.events[seq_idx]
2460
-
2461
- # Determine responder for this message
2462
2434
  if msg_idx == len(msg_chain) - 1 and current_responder is not None:
2463
- # For the last message, use the current responder
2464
2435
  responder = current_responder
2465
2436
  else:
2466
- # For other messages, determine from metadata
2467
2437
  responder = msg.metadata.sender
2468
-
2469
- # Classify the event
2470
2438
  event = self._classify_event(msg, responder)
2471
2439
  if not event:
2472
2440
  return False
2473
-
2474
- # Check if it matches
2475
2441
  matched = False
2476
-
2477
- # Special handling for CONTENT_MATCH
2478
2442
  if (
2479
2443
  expected.event_type == EventType.CONTENT_MATCH
2480
2444
  and expected.content_pattern
@@ -2483,14 +2447,9 @@ class Task:
2483
2447
  matched = True
2484
2448
  elif self._matches_event(event, expected):
2485
2449
  matched = True
2486
-
2487
2450
  if not matched:
2488
- # Strict matching - no skipping allowed
2489
2451
  return False
2490
2452
  else:
2491
- # Matched! Move to next expected event
2492
2453
  seq_idx -= 1
2493
2454
  msg_idx -= 1
2494
-
2495
- # We matched if we've matched all events in the sequence
2496
2455
  return seq_idx < 0
@@ -14,9 +14,9 @@ from random import choice
14
14
  from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
15
15
 
16
16
  from docstring_parser import parse
17
+ from pydantic import BaseModel, ConfigDict
17
18
 
18
19
  from langroid.language_models.base import LLMFunctionSpec
19
- from langroid.pydantic_v1 import BaseModel, Extra
20
20
  from langroid.utils.pydantic_utils import (
21
21
  _recursive_purge_dict_key,
22
22
  generate_simple_schema,
@@ -40,6 +40,14 @@ def format_schema_for_strict(schema: Any) -> None:
40
40
  This may not be equivalent to the original schema.
41
41
  """
42
42
  if isinstance(schema, dict):
43
+ # Handle $ref nodes - they can't have any other properties
44
+ if "$ref" in schema:
45
+ # Keep only the $ref, remove all other properties like description
46
+ ref_value = schema["$ref"]
47
+ schema.clear()
48
+ schema["$ref"] = ref_value
49
+ return
50
+
43
51
  if "type" in schema and schema["type"] == "object":
44
52
  schema["additionalProperties"] = False
45
53
 
@@ -111,14 +119,21 @@ class ToolMessage(ABC, BaseModel):
111
119
  # Optional param to limit number of tokens in the result of the tool.
112
120
  _max_result_tokens: int | None = None
113
121
 
114
- class Config:
115
- extra = Extra.allow
116
- arbitrary_types_allowed = False
117
- validate_all = True
118
- validate_assignment = True
122
+ model_config = ConfigDict(
123
+ extra="allow",
124
+ arbitrary_types_allowed=False,
125
+ validate_default=True,
126
+ validate_assignment=True,
119
127
  # do not include these fields in the generated schema
120
128
  # since we don't require the LLM to specify them
121
- schema_extra = {"exclude": {"purpose", "id"}}
129
+ json_schema_extra={"exclude": ["purpose", "id"]},
130
+ )
131
+
132
+ # Define excluded fields as a class method to avoid Pydantic treating it as
133
+ # a model field
134
+ @classmethod
135
+ def _get_excluded_fields(cls) -> set[str]:
136
+ return {"purpose", "id"}
122
137
 
123
138
  @classmethod
124
139
  def name(cls) -> str:
@@ -196,18 +211,18 @@ class ToolMessage(ABC, BaseModel):
196
211
  return "\n\n".join(formatted_examples)
197
212
 
198
213
  def to_json(self) -> str:
199
- return self.json(indent=4, exclude=self.Config.schema_extra["exclude"])
214
+ return self.model_dump_json(indent=4, exclude=self._get_excluded_fields())
200
215
 
201
216
  def format_example(self) -> str:
202
- return self.json(indent=4, exclude=self.Config.schema_extra["exclude"])
217
+ return self.model_dump_json(indent=4, exclude=self._get_excluded_fields())
203
218
 
204
219
  def dict_example(self) -> Dict[str, Any]:
205
- return self.dict(exclude=self.Config.schema_extra["exclude"])
220
+ return self.model_dump(exclude=self._get_excluded_fields())
206
221
 
207
222
  def get_value_of_type(self, target_type: Type[Any]) -> Any:
208
223
  """Try to find a value of a desired type in the fields of the ToolMessage."""
209
- ignore_fields = self.Config.schema_extra["exclude"].union(["request"])
210
- for field_name in set(self.dict().keys()) - ignore_fields:
224
+ ignore_fields = self._get_excluded_fields().union({"request"})
225
+ for field_name in set(self.model_dump().keys()) - ignore_fields:
211
226
  value = getattr(self, field_name)
212
227
  if is_instance_of(value, target_type):
213
228
  return value
@@ -224,7 +239,7 @@ class ToolMessage(ABC, BaseModel):
224
239
  Any: default value of the field, or None if not set or if the
225
240
  field does not exist.
226
241
  """
227
- schema = cls.schema()
242
+ schema = cls.model_json_schema()
228
243
  properties = schema["properties"]
229
244
  return properties.get(f, {}).get("default", None)
230
245
 
@@ -304,7 +319,7 @@ class ToolMessage(ABC, BaseModel):
304
319
  LLMFunctionSpec: the schema as an LLMFunctionSpec
305
320
 
306
321
  """
307
- schema = copy.deepcopy(cls.schema())
322
+ schema = copy.deepcopy(cls.model_json_schema())
308
323
  docstring = parse(cls.__doc__ or "")
309
324
  parameters = {
310
325
  k: v for k, v in schema.items() if k not in ("title", "description")
@@ -316,7 +331,7 @@ class ToolMessage(ABC, BaseModel):
316
331
  if "description" not in parameters["properties"][name]:
317
332
  parameters["properties"][name]["description"] = description
318
333
 
319
- excludes = cls.Config.schema_extra["exclude"]
334
+ excludes = cls._get_excluded_fields().copy()
320
335
  if not request:
321
336
  excludes = excludes.union({"request"})
322
337
  # exclude 'excludes' from parameters["properties"]:
@@ -374,7 +389,8 @@ class ToolMessage(ABC, BaseModel):
374
389
  _recursive_purge_dict_key(parameters, "additionalProperties")
375
390
  return LLMFunctionSpec(
376
391
  name=cls.default_value("request"),
377
- description=cls.default_value("purpose"),
392
+ description=cls.default_value("purpose")
393
+ or f"Tool for {cls.default_value('request')}",
378
394
  parameters=parameters,
379
395
  )
380
396
 
@@ -388,6 +404,6 @@ class ToolMessage(ABC, BaseModel):
388
404
  """
389
405
  schema = generate_simple_schema(
390
406
  cls,
391
- exclude=list(cls.Config.schema_extra["exclude"]),
407
+ exclude=list(cls._get_excluded_fields()),
392
408
  )
393
409
  return schema
@@ -4,10 +4,10 @@ from textwrap import dedent
4
4
  from typing import Callable, List, Tuple, Type
5
5
 
6
6
  import git
7
+ from pydantic import Field
7
8
 
8
9
  from langroid.agent.tool_message import ToolMessage
9
10
  from langroid.agent.xml_tool_message import XMLToolMessage
10
- from langroid.pydantic_v1 import Field
11
11
  from langroid.utils.git_utils import git_commit_file
12
12
  from langroid.utils.system import create_file, list_dir, read_file
13
13
 
@@ -91,7 +91,9 @@ class WriteFileTool(XMLToolMessage):
91
91
  content: str = Field(
92
92
  ...,
93
93
  description="The content to write to the file",
94
- verbatim=True, # preserve the content as is; uses CDATA section in XML
94
+ json_schema_extra={
95
+ "verbatim": True
96
+ }, # preserve the content as is; uses CDATA section in XML
95
97
  )
96
98
  _curr_dir: Callable[[], str] | None = None
97
99
  _git_repo: Callable[[], git.Repo] | None = None
@@ -27,12 +27,12 @@ from mcp.types import (
27
27
  TextResourceContents,
28
28
  Tool,
29
29
  )
30
+ from pydantic import AnyUrl, BaseModel, Field, create_model
30
31
 
31
32
  from langroid.agent.base import Agent
32
33
  from langroid.agent.chat_document import ChatDocument
33
34
  from langroid.agent.tool_message import ToolMessage
34
35
  from langroid.parsing.file_attachment import FileAttachment
35
- from langroid.pydantic_v1 import AnyUrl, BaseModel, Field, create_model
36
36
 
37
37
  load_dotenv() # load environment variables from .env
38
38
 
@@ -253,11 +253,24 @@ class FastMCPClient:
253
253
  from langroid.agent.tools.mcp.fastmcp_client import FastMCPClient
254
254
 
255
255
  # pack up the payload
256
- payload = itself.dict(
257
- exclude=itself.Config.schema_extra["exclude"].union(
258
- ["request", "purpose"]
259
- ),
260
- )
256
+ # Get exclude fields from model config with proper type checking
257
+ exclude_fields = set()
258
+ model_config = getattr(itself, "model_config", {})
259
+ if (
260
+ isinstance(model_config, dict)
261
+ and "json_schema_extra" in model_config
262
+ and model_config["json_schema_extra"] is not None
263
+ and isinstance(model_config["json_schema_extra"], dict)
264
+ and "exclude" in model_config["json_schema_extra"]
265
+ ):
266
+ exclude_list = model_config["json_schema_extra"]["exclude"]
267
+ if isinstance(exclude_list, (list, set, tuple)):
268
+ exclude_fields = set(exclude_list)
269
+
270
+ # Add standard excluded fields
271
+ exclude_fields.update(["request", "purpose"])
272
+
273
+ payload = itself.model_dump(exclude=exclude_fields)
261
274
 
262
275
  # restore any renamed fields
263
276
  for orig, new in itself.__class__._renamed_fields.items(): # type: ignore
@@ -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):