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
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,
@@ -483,7 +483,7 @@ class Task:
483
483
  self.message_counter.clear()
484
484
  # create a unique string that will not likely be in any message,
485
485
  # so we always have a message with count=1
486
- self.message_counter.update([hash("___NO_MESSAGE___")])
486
+ self.message_counter.update([str(hash("___NO_MESSAGE___"))])
487
487
 
488
488
  def _cache_session_store(self, key: str, value: str) -> None:
489
489
  """
@@ -2083,7 +2083,7 @@ class Task:
2083
2083
  """
2084
2084
  from langroid.agent.chat_document import ChatDocLoggerFields
2085
2085
 
2086
- default_values = ChatDocLoggerFields().dict().values()
2086
+ default_values = ChatDocLoggerFields().model_dump().values()
2087
2087
  msg_str_tsv = "\t".join(str(v) for v in default_values)
2088
2088
  if msg is not None:
2089
2089
  msg_str_tsv = msg.tsv_str()
@@ -2144,7 +2144,7 @@ class Task:
2144
2144
  else:
2145
2145
  # Get fields from the message
2146
2146
  fields = msg.log_fields()
2147
- fields_dict = fields.dict()
2147
+ fields_dict = fields.model_dump()
2148
2148
  fields_dict.update(
2149
2149
  {
2150
2150
  "responder": str(resp),
@@ -2155,13 +2155,11 @@ class Task:
2155
2155
 
2156
2156
  # Create a ChatDocLoggerFields-like object for the HTML logger
2157
2157
  # Create a simple BaseModel subclass dynamically
2158
- from langroid.pydantic_v1 import BaseModel
2158
+ from pydantic import BaseModel
2159
2159
 
2160
2160
  class LogFields(BaseModel):
2161
- class Config:
2162
- extra = "allow" # Allow extra fields
2161
+ model_config = ConfigDict(extra="allow") # Allow extra fields
2163
2162
 
2164
- # Create instance with the fields from fields_dict
2165
2163
  log_obj = LogFields(**fields_dict)
2166
2164
  self.html_logger.log(log_obj)
2167
2165
 
@@ -2173,8 +2171,6 @@ class Task:
2173
2171
  """
2174
2172
  if recipient == "":
2175
2173
  return True
2176
- # native responders names are USER, LLM, AGENT,
2177
- # and the names of subtasks are from Task.name attribute
2178
2174
  responder_names = [self.name.lower()] + [
2179
2175
  r.name.lower() for r in self.responders
2180
2176
  ]
@@ -2184,7 +2180,6 @@ class Task:
2184
2180
  """
2185
2181
  Is the recipient explicitly specified and does not match responder "e" ?
2186
2182
  """
2187
- # Note that recipient could be specified as an Entity or a Task name
2188
2183
  return (
2189
2184
  self.pending_message is not None
2190
2185
  and (recipient := self.pending_message.metadata.recipient) != ""
@@ -2195,8 +2190,6 @@ class Task:
2195
2190
 
2196
2191
  def _user_can_respond(self) -> bool:
2197
2192
  return self.interactive or (
2198
- # regardless of self.interactive, if a msg is explicitly addressed to
2199
- # user, then wait for user response
2200
2193
  self.pending_message is not None
2201
2194
  and self.pending_message.metadata.recipient == Entity.USER
2202
2195
  and not self.agent.has_tool_message_attempt(self.pending_message)
@@ -2204,19 +2197,13 @@ class Task:
2204
2197
 
2205
2198
  def _can_respond(self, e: Responder) -> bool:
2206
2199
  user_can_respond = self._user_can_respond()
2207
-
2208
2200
  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
2201
  return False
2212
-
2213
2202
  if self.pending_message is None:
2214
2203
  return True
2215
2204
  if isinstance(e, Task) and not e.agent.can_respond(self.pending_message):
2216
2205
  return False
2217
-
2218
2206
  if self._recipient_mismatch(e):
2219
- # Cannot respond if not addressed to this entity
2220
2207
  return False
2221
2208
  return self.pending_message.metadata.block != e
2222
2209
 
@@ -2230,7 +2217,6 @@ class Task:
2230
2217
  Args:
2231
2218
  enable (bool): value of `self.color_log` to set to,
2232
2219
  which will enable/diable rich logging
2233
-
2234
2220
  """
2235
2221
  self.color_log = enable
2236
2222
 
@@ -2247,16 +2233,13 @@ class Task:
2247
2233
  Args:
2248
2234
  msg (ChatDocument|str|None): message to parse
2249
2235
  addressing_prefix (str): prefix to address other agents or entities,
2250
- (e.g. "@". See documentation of `TaskConfig` for details).
2236
+ (e.g. "@". See documentation of `TaskConfig` for details).
2251
2237
  Returns:
2252
2238
  Tuple[bool|None, str|None, str|None]:
2253
2239
  bool: true=PASS, false=SEND, or None if neither
2254
2240
  str: recipient, or None
2255
2241
  str: content to send, or None
2256
2242
  """
2257
- # handle routing instruction-strings in result if any,
2258
- # such as PASS, PASS_TO, or SEND
2259
-
2260
2243
  msg_str = msg.content if isinstance(msg, ChatDocument) else msg
2261
2244
  if (
2262
2245
  self.agent.has_tool_message_attempt(msg)
@@ -2264,10 +2247,7 @@ class Task:
2264
2247
  and not msg_str.startswith(PASS_TO)
2265
2248
  and not msg_str.startswith(SEND_TO)
2266
2249
  ):
2267
- # if there's an attempted tool-call, we ignore any routing strings,
2268
- # unless they are at the start of the msg
2269
2250
  return None, None, None
2270
-
2271
2251
  content = msg.content if isinstance(msg, ChatDocument) else msg
2272
2252
  content = content.strip()
2273
2253
  if PASS in content and PASS_TO not in content:
@@ -2279,10 +2259,7 @@ class Task:
2279
2259
  and (addressee_content := parse_addressed_message(content, SEND_TO))[0]
2280
2260
  is not None
2281
2261
  ):
2282
- # Note this will discard any portion of content BEFORE SEND_TO.
2283
- # TODO maybe make this configurable.
2284
2262
  (addressee, content_to_send) = addressee_content
2285
- # if no content then treat same as PASS_TO
2286
2263
  if content_to_send == "":
2287
2264
  return True, addressee, None
2288
2265
  else:
@@ -2296,12 +2273,10 @@ class Task:
2296
2273
  is not None
2297
2274
  ):
2298
2275
  (addressee, content_to_send) = addressee_content
2299
- # if no content then treat same as PASS_TO
2300
2276
  if content_to_send == "":
2301
2277
  return True, addressee, None
2302
2278
  else:
2303
2279
  return False, addressee, content_to_send
2304
-
2305
2280
  return None, None, None
2306
2281
 
2307
2282
  def _classify_event(
@@ -2310,19 +2285,13 @@ class Task:
2310
2285
  """Classify a message into an AgentEvent for sequence matching."""
2311
2286
  if msg is None:
2312
2287
  return AgentEvent(event_type=EventType.NO_RESPONSE)
2313
-
2314
- # Determine the event type based on responder and message content
2315
2288
  event_type = EventType.NO_RESPONSE
2316
2289
  tool_name = None
2317
-
2318
- # Check if there are tool messages
2319
2290
  tool_messages = self.agent.try_get_tool_messages(msg, all_tools=True)
2320
2291
  if tool_messages:
2321
2292
  event_type = EventType.TOOL
2322
2293
  if len(tool_messages) == 1:
2323
2294
  tool_name = tool_messages[0].request
2324
-
2325
- # Check responder type
2326
2295
  if responder == Entity.LLM and not tool_messages:
2327
2296
  event_type = EventType.LLM_RESPONSE
2328
2297
  elif responder == Entity.AGENT:
@@ -2330,21 +2299,17 @@ class Task:
2330
2299
  elif responder == Entity.USER:
2331
2300
  event_type = EventType.USER_RESPONSE
2332
2301
  elif isinstance(responder, Task):
2333
- # For sub-task responses, check the sender in metadata
2334
2302
  if msg.metadata.sender == Entity.LLM:
2335
2303
  event_type = EventType.LLM_RESPONSE
2336
2304
  elif msg.metadata.sender == Entity.AGENT:
2337
2305
  event_type = EventType.AGENT_RESPONSE
2338
2306
  else:
2339
2307
  event_type = EventType.USER_RESPONSE
2340
-
2341
- # Get sender name
2342
2308
  sender_name = None
2343
2309
  if isinstance(responder, Entity):
2344
2310
  sender_name = responder.value
2345
2311
  elif isinstance(responder, Task):
2346
2312
  sender_name = responder.name
2347
-
2348
2313
  return AgentEvent(
2349
2314
  event_type=event_type,
2350
2315
  tool_name=tool_name,
@@ -2356,17 +2321,13 @@ class Task:
2356
2321
  ) -> List[ChatDocument]:
2357
2322
  """Get the chain of messages from response sequence."""
2358
2323
  if max_depth is None:
2359
- # Get max depth needed from all sequences
2360
2324
  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
2325
+ if self._parsed_done_sequences:
2326
+ max_depth = max(len(seq.events) for seq in self._parsed_done_sequences)
2365
2327
  return self.response_sequence[-max_depth:]
2366
2328
 
2367
2329
  def _matches_event(self, actual: AgentEvent, expected: AgentEvent) -> bool:
2368
2330
  """Check if an actual event matches an expected event pattern."""
2369
- # Check event type
2370
2331
  if expected.event_type == EventType.SPECIFIC_TOOL:
2371
2332
  if actual.event_type != EventType.TOOL:
2372
2333
  return False
@@ -2374,53 +2335,36 @@ class Task:
2374
2335
  return False
2375
2336
  elif actual.event_type != expected.event_type:
2376
2337
  return False
2377
-
2378
- # Check sender if specified
2379
2338
  if expected.sender and actual.sender != expected.sender:
2380
2339
  return False
2381
-
2382
- # TODO: Add content pattern matching for CONTENT_MATCH type
2383
-
2384
2340
  return True
2385
2341
 
2386
2342
  def _matches_sequence(
2387
2343
  self, msg_chain: List[ChatDocument], sequence: DoneSequence
2388
2344
  ) -> bool:
2389
2345
  """Check if a message chain matches a done sequence.
2390
-
2391
2346
  We traverse the message chain and try to match the sequence events.
2392
2347
  The events don't have to be consecutive in the chain.
2393
2348
  """
2394
2349
  if not sequence.events:
2395
2350
  return False
2396
-
2397
- # Convert messages to events
2398
2351
  events = []
2399
2352
  for i, msg in enumerate(msg_chain):
2400
- # Determine responder from metadata or by checking previous message
2401
2353
  responder = None
2402
2354
  if msg.metadata.sender:
2403
2355
  responder = msg.metadata.sender
2404
2356
  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
2357
  responder = None
2408
-
2409
2358
  event = self._classify_event(msg, responder)
2410
2359
  if event:
2411
2360
  events.append(event)
2412
-
2413
- # Try to match the sequence
2414
2361
  seq_idx = 0
2415
2362
  for event in events:
2416
2363
  if seq_idx >= len(sequence.events):
2417
2364
  break
2418
-
2419
2365
  expected = sequence.events[seq_idx]
2420
2366
  if self._matches_event(event, expected):
2421
2367
  seq_idx += 1
2422
-
2423
- # Check if we matched the entire sequence
2424
2368
  return seq_idx == len(sequence.events)
2425
2369
 
2426
2370
  def close_loggers(self) -> None:
@@ -2438,43 +2382,26 @@ class Task:
2438
2382
  current_responder: Optional[Responder],
2439
2383
  ) -> bool:
2440
2384
  """Check if the message chain plus current message matches a done sequence.
2441
-
2442
2385
  Process messages in reverse order (newest first) and match against
2443
2386
  the sequence events in reverse order.
2444
2387
  """
2445
- # Add current message to chain if not already there
2446
2388
  if not msg_chain or msg_chain[-1].id() != current_msg.id():
2447
2389
  msg_chain = msg_chain + [current_msg]
2448
-
2449
- # If we don't have enough messages for the sequence, can't match
2450
2390
  if len(msg_chain) < len(sequence.events):
2451
2391
  return False
2452
-
2453
- # Process in reverse order - start from the end of both lists
2454
2392
  seq_idx = len(sequence.events) - 1
2455
2393
  msg_idx = len(msg_chain) - 1
2456
-
2457
2394
  while seq_idx >= 0 and msg_idx >= 0:
2458
2395
  msg = msg_chain[msg_idx]
2459
2396
  expected = sequence.events[seq_idx]
2460
-
2461
- # Determine responder for this message
2462
2397
  if msg_idx == len(msg_chain) - 1 and current_responder is not None:
2463
- # For the last message, use the current responder
2464
2398
  responder = current_responder
2465
2399
  else:
2466
- # For other messages, determine from metadata
2467
2400
  responder = msg.metadata.sender
2468
-
2469
- # Classify the event
2470
2401
  event = self._classify_event(msg, responder)
2471
2402
  if not event:
2472
2403
  return False
2473
-
2474
- # Check if it matches
2475
2404
  matched = False
2476
-
2477
- # Special handling for CONTENT_MATCH
2478
2405
  if (
2479
2406
  expected.event_type == EventType.CONTENT_MATCH
2480
2407
  and expected.content_pattern
@@ -2483,14 +2410,9 @@ class Task:
2483
2410
  matched = True
2484
2411
  elif self._matches_event(event, expected):
2485
2412
  matched = True
2486
-
2487
2413
  if not matched:
2488
- # Strict matching - no skipping allowed
2489
2414
  return False
2490
2415
  else:
2491
- # Matched! Move to next expected event
2492
2416
  seq_idx -= 1
2493
2417
  msg_idx -= 1
2494
-
2495
- # We matched if we've matched all events in the sequence
2496
2418
  return seq_idx < 0