langroid 0.24.1__tar.gz → 0.26.0__tar.gz

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 (148) hide show
  1. {langroid-0.24.1 → langroid-0.26.0}/PKG-INFO +2 -2
  2. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/base.py +339 -125
  3. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/callbacks/chainlit.py +57 -129
  4. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/chat_agent.py +19 -0
  5. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/orchestration.py +8 -3
  6. {langroid-0.24.1 → langroid-0.26.0}/pyproject.toml +3 -3
  7. {langroid-0.24.1 → langroid-0.26.0}/LICENSE +0 -0
  8. {langroid-0.24.1 → langroid-0.26.0}/README.md +0 -0
  9. {langroid-0.24.1 → langroid-0.26.0}/langroid/__init__.py +0 -0
  10. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/__init__.py +0 -0
  11. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/batch.py +0 -0
  12. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/callbacks/__init__.py +0 -0
  13. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/chat_document.py +0 -0
  14. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/helpers.py +0 -0
  15. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/junk +0 -0
  16. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/openai_assistant.py +0 -0
  17. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/__init__.py +0 -0
  18. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/arangodb/__init__.py +0 -0
  19. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/arangodb/arangodb_agent.py +0 -0
  20. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/arangodb/system_messages.py +0 -0
  21. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/arangodb/tools.py +0 -0
  22. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/arangodb/utils.py +0 -0
  23. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/doc_chat_agent.py +0 -0
  24. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/lance_doc_chat_agent.py +0 -0
  25. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/lance_rag/__init__.py +0 -0
  26. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/lance_rag/critic_agent.py +0 -0
  27. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/lance_rag/lance_rag_task.py +0 -0
  28. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/lance_rag/query_planner_agent.py +0 -0
  29. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/lance_tools.py +0 -0
  30. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/neo4j/__init__.py +0 -0
  31. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/neo4j/csv_kg_chat.py +0 -0
  32. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/neo4j/neo4j_chat_agent.py +0 -0
  33. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/neo4j/system_messages.py +0 -0
  34. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/neo4j/tools.py +0 -0
  35. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/relevance_extractor_agent.py +0 -0
  36. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/retriever_agent.py +0 -0
  37. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/sql/__init__.py +0 -0
  38. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/sql/sql_chat_agent.py +0 -0
  39. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/sql/utils/__init__.py +0 -0
  40. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/sql/utils/description_extractors.py +0 -0
  41. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/sql/utils/populate_metadata.py +0 -0
  42. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/sql/utils/system_message.py +0 -0
  43. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/sql/utils/tools.py +0 -0
  44. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/special/table_chat_agent.py +0 -0
  45. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/structured_message.py +0 -0
  46. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/task.py +0 -0
  47. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tool_message.py +0 -0
  48. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/__init__.py +0 -0
  49. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/duckduckgo_search_tool.py +0 -0
  50. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/file_tools.py +0 -0
  51. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/google_search_tool.py +0 -0
  52. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/metaphor_search_tool.py +0 -0
  53. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/recipient_tool.py +0 -0
  54. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/retrieval_tool.py +0 -0
  55. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/rewind_tool.py +0 -0
  56. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/tools/segment_extract_tool.py +0 -0
  57. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/typed_task.py +0 -0
  58. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent/xml_tool_message.py +0 -0
  59. {langroid-0.24.1 → langroid-0.26.0}/langroid/agent_config.py +0 -0
  60. {langroid-0.24.1 → langroid-0.26.0}/langroid/cachedb/__init__.py +0 -0
  61. {langroid-0.24.1 → langroid-0.26.0}/langroid/cachedb/base.py +0 -0
  62. {langroid-0.24.1 → langroid-0.26.0}/langroid/cachedb/momento_cachedb.py +0 -0
  63. {langroid-0.24.1 → langroid-0.26.0}/langroid/cachedb/redis_cachedb.py +0 -0
  64. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/__init__.py +0 -0
  65. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/base.py +0 -0
  66. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/clustering.py +0 -0
  67. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/models.py +0 -0
  68. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/protoc/__init__.py +0 -0
  69. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/protoc/embeddings.proto +0 -0
  70. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/protoc/embeddings_pb2.py +0 -0
  71. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/protoc/embeddings_pb2.pyi +0 -0
  72. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/protoc/embeddings_pb2_grpc.py +0 -0
  73. {langroid-0.24.1 → langroid-0.26.0}/langroid/embedding_models/remote_embeds.py +0 -0
  74. {langroid-0.24.1 → langroid-0.26.0}/langroid/exceptions.py +0 -0
  75. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/.chainlit/config.toml +0 -0
  76. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/.chainlit/translations/en-US.json +0 -0
  77. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/__init__.py +0 -0
  78. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/azure_openai.py +0 -0
  79. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/base.py +0 -0
  80. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/config.py +0 -0
  81. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/mock_lm.py +0 -0
  82. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/openai_gpt.py +0 -0
  83. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/prompt_formatter/__init__.py +0 -0
  84. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/prompt_formatter/base.py +0 -0
  85. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/prompt_formatter/hf_formatter.py +0 -0
  86. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/prompt_formatter/llama2_formatter.py +0 -0
  87. {langroid-0.24.1 → langroid-0.26.0}/langroid/language_models/utils.py +0 -0
  88. {langroid-0.24.1 → langroid-0.26.0}/langroid/mytypes.py +0 -0
  89. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/__init__.py +0 -0
  90. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/agent_chats.py +0 -0
  91. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/code-parsing.md +0 -0
  92. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/code_parser.py +0 -0
  93. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/config.py +0 -0
  94. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/document_parser.py +0 -0
  95. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/image_text.py +0 -0
  96. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/para_sentence_split.py +0 -0
  97. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/parse_json.py +0 -0
  98. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/parser.py +0 -0
  99. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/repo_loader.py +0 -0
  100. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/routing.py +0 -0
  101. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/search.py +0 -0
  102. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/spider.py +0 -0
  103. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/table_loader.py +0 -0
  104. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/url_loader.py +0 -0
  105. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/url_loader_cookies.py +0 -0
  106. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/urls.py +0 -0
  107. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/utils.py +0 -0
  108. {langroid-0.24.1 → langroid-0.26.0}/langroid/parsing/web_search.py +0 -0
  109. {langroid-0.24.1 → langroid-0.26.0}/langroid/prompts/__init__.py +0 -0
  110. {langroid-0.24.1 → langroid-0.26.0}/langroid/prompts/chat-gpt4-system-prompt.md +0 -0
  111. {langroid-0.24.1 → langroid-0.26.0}/langroid/prompts/dialog.py +0 -0
  112. {langroid-0.24.1 → langroid-0.26.0}/langroid/prompts/prompts_config.py +0 -0
  113. {langroid-0.24.1 → langroid-0.26.0}/langroid/prompts/templates.py +0 -0
  114. {langroid-0.24.1 → langroid-0.26.0}/langroid/py.typed +0 -0
  115. {langroid-0.24.1 → langroid-0.26.0}/langroid/pydantic_v1/__init__.py +0 -0
  116. {langroid-0.24.1 → langroid-0.26.0}/langroid/pydantic_v1/main.py +0 -0
  117. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/.chainlit/config.toml +0 -0
  118. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/.chainlit/translations/en-US.json +0 -0
  119. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/__init__.py +0 -0
  120. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/algorithms/__init__.py +0 -0
  121. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/algorithms/graph.py +0 -0
  122. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/configuration.py +0 -0
  123. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/constants.py +0 -0
  124. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/docker.py +0 -0
  125. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/git_utils.py +0 -0
  126. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/globals.py +0 -0
  127. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/llms/__init__.py +0 -0
  128. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/llms/strings.py +0 -0
  129. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/logging.py +0 -0
  130. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/object_registry.py +0 -0
  131. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/output/__init__.py +0 -0
  132. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/output/citations.py +0 -0
  133. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/output/printing.py +0 -0
  134. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/output/status.py +0 -0
  135. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/pandas_utils.py +0 -0
  136. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/pydantic_utils.py +0 -0
  137. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/system.py +0 -0
  138. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/types.py +0 -0
  139. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/web/__init__.py +0 -0
  140. {langroid-0.24.1 → langroid-0.26.0}/langroid/utils/web/login.py +0 -0
  141. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/__init__.py +0 -0
  142. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/base.py +0 -0
  143. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/chromadb.py +0 -0
  144. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/lancedb.py +0 -0
  145. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/meilisearch.py +0 -0
  146. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/momento.py +0 -0
  147. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/qdrant_cloud.py +0 -0
  148. {langroid-0.24.1 → langroid-0.26.0}/langroid/vector_store/qdrantdb.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langroid
3
- Version: 0.24.1
3
+ Version: 0.26.0
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  License: MIT
6
6
  Author: Prasad Chalasani
@@ -42,7 +42,7 @@ Requires-Dist: arango-datasets (>=1.2.2,<2.0.0) ; extra == "all" or extra == "ar
42
42
  Requires-Dist: async-generator (>=1.10,<2.0)
43
43
  Requires-Dist: bs4 (>=0.0.1,<0.0.2)
44
44
  Requires-Dist: cerebras-cloud-sdk (>=1.1.0,<2.0.0)
45
- Requires-Dist: chainlit (==1.1.202) ; extra == "all" or extra == "chainlit"
45
+ Requires-Dist: chainlit (>=1.3.2,<2.0.0) ; extra == "all" or extra == "chainlit"
46
46
  Requires-Dist: chromadb (>=0.4.21,<=0.4.23) ; extra == "vecdbs" or extra == "all" or extra == "chromadb"
47
47
  Requires-Dist: colorlog (>=6.7.0,<7.0.0)
48
48
  Requires-Dist: docstring-parser (>=0.15,<0.16)
@@ -175,6 +175,7 @@ class Agent(ABC):
175
175
  show_llm_response=noop_fn,
176
176
  show_agent_response=noop_fn,
177
177
  get_user_response=None,
178
+ get_user_response_async=None,
178
179
  get_last_step=noop_fn,
179
180
  set_parent_agent=noop_fn,
180
181
  show_error_message=noop_fn,
@@ -322,6 +323,52 @@ class Agent(ABC):
322
323
  lambda msg: message_class.handle_message_fallback(self, msg),
323
324
  )
324
325
 
326
+ async_tool_name = f"{tool}_async"
327
+ if (
328
+ hasattr(message_class, "handle_async")
329
+ and inspect.isfunction(message_class.handle_async)
330
+ and not hasattr(self, async_tool_name)
331
+ ):
332
+ has_chat_doc_arg = (
333
+ len(inspect.signature(message_class.handle_async).parameters) > 1
334
+ )
335
+
336
+ if has_chat_doc_arg:
337
+
338
+ @no_type_check
339
+ async def handler(obj, chat_doc):
340
+ return await obj.handle_async(chat_doc)
341
+
342
+ else:
343
+
344
+ @no_type_check
345
+ async def handler(obj):
346
+ return await obj.handle_async()
347
+
348
+ setattr(self, async_tool_name, handler)
349
+ elif (
350
+ hasattr(message_class, "response_async")
351
+ and inspect.isfunction(message_class.response_async)
352
+ and not hasattr(self, async_tool_name)
353
+ ):
354
+ has_chat_doc_arg = (
355
+ len(inspect.signature(message_class.response_async).parameters) > 2
356
+ )
357
+
358
+ if has_chat_doc_arg:
359
+
360
+ @no_type_check
361
+ async def handler(obj, chat_doc):
362
+ return await obj.response_async(self, chat_doc)
363
+
364
+ else:
365
+
366
+ @no_type_check
367
+ async def handler(obj):
368
+ return await obj.response_async(self)
369
+
370
+ setattr(self, async_tool_name, handler)
371
+
325
372
  return [tool]
326
373
 
327
374
  def enable_message_handling(
@@ -393,32 +440,14 @@ class Agent(ABC):
393
440
  recipient=recipient,
394
441
  )
395
442
 
396
- async def agent_response_async(
443
+ def _agent_response_final(
397
444
  self,
398
- msg: Optional[str | ChatDocument] = None,
399
- ) -> Optional[ChatDocument]:
400
- return self.agent_response(msg)
401
-
402
- def agent_response(
403
- self,
404
- msg: Optional[str | ChatDocument] = None,
445
+ msg: Optional[str | ChatDocument],
446
+ results: Optional[str | OrderedDict[str, str] | ChatDocument],
405
447
  ) -> Optional[ChatDocument]:
406
448
  """
407
- Response from the "agent itself", typically (but not only)
408
- used to handle LLM's "tool message" or `function_call`
409
- (e.g. OpenAI `function_call`).
410
- Args:
411
- msg (str|ChatDocument): the input to respond to: if msg is a string,
412
- and it contains a valid JSON-structured "tool message", or
413
- if msg is a ChatDocument, and it contains a `function_call`.
414
- Returns:
415
- Optional[ChatDocument]: the response, packaged as a ChatDocument
416
-
449
+ Convert results to final response.
417
450
  """
418
- if msg is None:
419
- return None
420
-
421
- results = self.handle_message(msg)
422
451
  if results is None:
423
452
  return None
424
453
  if not settings.quiet:
@@ -438,7 +467,7 @@ class Agent(ABC):
438
467
  if isinstance(results, ChatDocument):
439
468
  # Preserve trail of tool_ids for OpenAI Assistant fn-calls
440
469
  results.metadata.tool_ids = (
441
- [] if isinstance(msg, str) else msg.metadata.tool_ids
470
+ [] if msg is None or isinstance(msg, str) else msg.metadata.tool_ids
442
471
  )
443
472
  return results
444
473
  sender_name = self.config.name
@@ -461,10 +490,49 @@ class Agent(ABC):
461
490
  sender_name=sender_name,
462
491
  oai_tool_id=oai_tool_id,
463
492
  # preserve trail of tool_ids for OpenAI Assistant fn-calls
464
- tool_ids=[] if isinstance(msg, str) else msg.metadata.tool_ids,
493
+ tool_ids=(
494
+ [] if msg is None or isinstance(msg, str) else msg.metadata.tool_ids
495
+ ),
465
496
  ),
466
497
  )
467
498
 
499
+ async def agent_response_async(
500
+ self,
501
+ msg: Optional[str | ChatDocument] = None,
502
+ ) -> Optional[ChatDocument]:
503
+ """
504
+ Asynch version of `agent_response`. See there for details.
505
+ """
506
+ if msg is None:
507
+ return None
508
+
509
+ results = await self.handle_message_async(msg)
510
+
511
+ return self._agent_response_final(msg, results)
512
+
513
+ def agent_response(
514
+ self,
515
+ msg: Optional[str | ChatDocument] = None,
516
+ ) -> Optional[ChatDocument]:
517
+ """
518
+ Response from the "agent itself", typically (but not only)
519
+ used to handle LLM's "tool message" or `function_call`
520
+ (e.g. OpenAI `function_call`).
521
+ Args:
522
+ msg (str|ChatDocument): the input to respond to: if msg is a string,
523
+ and it contains a valid JSON-structured "tool message", or
524
+ if msg is a ChatDocument, and it contains a `function_call`.
525
+ Returns:
526
+ Optional[ChatDocument]: the response, packaged as a ChatDocument
527
+
528
+ """
529
+ if msg is None:
530
+ return None
531
+
532
+ results = self.handle_message(msg)
533
+
534
+ return self._agent_response_final(msg, results)
535
+
468
536
  def process_tool_results(
469
537
  self,
470
538
  results: str,
@@ -625,60 +693,46 @@ class Agent(ABC):
625
693
  recipient=recipient,
626
694
  )
627
695
 
628
- async def user_response_async(
629
- self,
630
- msg: Optional[str | ChatDocument] = None,
631
- ) -> Optional[ChatDocument]:
632
- return self.user_response(msg)
633
-
634
- def user_response(
635
- self,
636
- msg: Optional[str | ChatDocument] = None,
637
- ) -> Optional[ChatDocument]:
696
+ def user_can_respond(self, msg: Optional[str | ChatDocument] = None) -> bool:
638
697
  """
639
- Get user response to current message. Could allow (human) user to intervene
640
- with an actual answer, or quit using "q" or "x"
698
+ Whether the user can respond to a message.
641
699
 
642
700
  Args:
643
701
  msg (str|ChatDocument): the string to respond to.
644
702
 
645
703
  Returns:
646
- (str) User response, packaged as a ChatDocument
647
704
 
648
705
  """
649
-
650
706
  # When msg explicitly addressed to user, this means an actual human response
651
707
  # is being sought.
652
708
  need_human_response = (
653
709
  isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
654
710
  )
655
- default_user_msg = (
656
- (self.default_human_response or "null") if need_human_response else ""
657
- )
658
711
 
659
712
  if not self.interactive and not need_human_response:
660
- return None
661
- elif self.default_human_response is not None:
662
- user_msg = self.default_human_response
663
- else:
664
- if self.callbacks.get_user_response is not None:
665
- # ask user with empty prompt: no need for prompt
666
- # since user has seen the conversation so far.
667
- # But non-empty prompt can be useful when Agent
668
- # uses a tool that requires user input, or in other scenarios.
669
- user_msg = self.callbacks.get_user_response(prompt="")
670
- else:
671
- user_msg = Prompt.ask(
672
- f"[blue]{self.indent}"
673
- + self.config.human_prompt
674
- + f"\n{self.indent}"
675
- ).strip()
713
+ return False
714
+
715
+ return True
716
+
717
+ def _user_response_final(
718
+ self, msg: Optional[str | ChatDocument], user_msg: str
719
+ ) -> Optional[ChatDocument]:
720
+ """
721
+ Convert user_msg to final response.
722
+ """
723
+ if not user_msg:
724
+ need_human_response = (
725
+ isinstance(msg, ChatDocument) and msg.metadata.recipient == Entity.USER
726
+ )
727
+ user_msg = (
728
+ (self.default_human_response or "null") if need_human_response else ""
729
+ )
730
+ user_msg = user_msg.strip()
676
731
 
677
732
  tool_ids = []
678
733
  if msg is not None and isinstance(msg, ChatDocument):
679
734
  tool_ids = msg.metadata.tool_ids
680
735
 
681
- user_msg = user_msg.strip() or default_user_msg.strip()
682
736
  # only return non-None result if user_msg not empty
683
737
  if not user_msg:
684
738
  return None
@@ -700,6 +754,72 @@ class Agent(ABC):
700
754
  ),
701
755
  )
702
756
 
757
+ async def user_response_async(
758
+ self,
759
+ msg: Optional[str | ChatDocument] = None,
760
+ ) -> Optional[ChatDocument]:
761
+ """
762
+ Asynch version of `user_response`. See there for details.
763
+ """
764
+ if not self.user_can_respond(msg):
765
+ return None
766
+
767
+ if self.default_human_response is not None:
768
+ user_msg = self.default_human_response
769
+ else:
770
+ if (
771
+ self.callbacks.get_user_response_async is not None
772
+ and self.callbacks.get_user_response_async is not async_noop_fn
773
+ ):
774
+ user_msg = await self.callbacks.get_user_response_async(prompt="")
775
+ elif self.callbacks.get_user_response is not None:
776
+ user_msg = self.callbacks.get_user_response(prompt="")
777
+ else:
778
+ user_msg = Prompt.ask(
779
+ f"[blue]{self.indent}"
780
+ + self.config.human_prompt
781
+ + f"\n{self.indent}"
782
+ )
783
+
784
+ return self._user_response_final(msg, user_msg)
785
+
786
+ def user_response(
787
+ self,
788
+ msg: Optional[str | ChatDocument] = None,
789
+ ) -> Optional[ChatDocument]:
790
+ """
791
+ Get user response to current message. Could allow (human) user to intervene
792
+ with an actual answer, or quit using "q" or "x"
793
+
794
+ Args:
795
+ msg (str|ChatDocument): the string to respond to.
796
+
797
+ Returns:
798
+ (str) User response, packaged as a ChatDocument
799
+
800
+ """
801
+
802
+ if not self.user_can_respond(msg):
803
+ return None
804
+
805
+ if self.default_human_response is not None:
806
+ user_msg = self.default_human_response
807
+ else:
808
+ if self.callbacks.get_user_response is not None:
809
+ # ask user with empty prompt: no need for prompt
810
+ # since user has seen the conversation so far.
811
+ # But non-empty prompt can be useful when Agent
812
+ # uses a tool that requires user input, or in other scenarios.
813
+ user_msg = self.callbacks.get_user_response(prompt="")
814
+ else:
815
+ user_msg = Prompt.ask(
816
+ f"[blue]{self.indent}"
817
+ + self.config.human_prompt
818
+ + f"\n{self.indent}"
819
+ )
820
+
821
+ return self._user_response_final(msg, user_msg)
822
+
703
823
  @no_type_check
704
824
  def llm_can_respond(self, message: Optional[str | ChatDocument] = None) -> bool:
705
825
  """
@@ -791,7 +911,7 @@ class Agent(ABC):
791
911
  f"""
792
912
  Requested output length has been shortened to {output_len}
793
913
  so that the total length of Prompt + Output is less than
794
- the completion context length of the LLM.
914
+ the completion context length of the LLM.
795
915
  """
796
916
  )
797
917
 
@@ -865,7 +985,7 @@ class Agent(ABC):
865
985
  f"""
866
986
  Requested output length has been shortened to {output_len}
867
987
  so that the total length of Prompt + Output is less than
868
- the completion context length of the LLM.
988
+ the completion context length of the LLM.
869
989
  """
870
990
  )
871
991
  if self.llm.get_stream() and not settings.quiet:
@@ -1075,7 +1195,7 @@ class Agent(ABC):
1075
1195
  if tool_name not in self.llm_tools_handled:
1076
1196
  logger.warning(
1077
1197
  f"""
1078
- The function_call '{tool_name}' is not handled
1198
+ The function_call '{tool_name}' is not handled
1079
1199
  by the agent named '{self.config.name}'!
1080
1200
  If you intended this agent to handle this function_call,
1081
1201
  either the fn-call name is incorrectly generated by the LLM,
@@ -1110,7 +1230,7 @@ class Agent(ABC):
1110
1230
  if tool_name not in self.llm_tools_handled:
1111
1231
  logger.warning(
1112
1232
  f"""
1113
- The tool_call '{tool_name}' is not handled
1233
+ The tool_call '{tool_name}' is not handled
1114
1234
  by the agent named '{self.config.name}'!
1115
1235
  If you intended this agent to handle this function_call,
1116
1236
  either the fn-call name is incorrectly generated by the LLM,
@@ -1145,65 +1265,18 @@ class Agent(ABC):
1145
1265
  [f"{e['loc']}: {e['msg']}" for e in ve.errors() if "loc" in e]
1146
1266
  )
1147
1267
  return f"""
1148
- There were one or more errors in your attempt to use the
1149
- TOOL or function_call named '{tool_name}':
1268
+ There were one or more errors in your attempt to use the
1269
+ TOOL or function_call named '{tool_name}':
1150
1270
  {bad_field_errors}
1151
1271
  Please write your message again, correcting the errors.
1152
1272
  """
1153
1273
 
1154
- def handle_message(
1155
- self, msg: str | ChatDocument
1156
- ) -> None | str | OrderedDict[str, str] | ChatDocument:
1274
+ def _get_multiple_orch_tool_errs(
1275
+ self, tools: List[ToolMessage]
1276
+ ) -> List[str | ChatDocument | None]:
1157
1277
  """
1158
- Handle a "tool" message either a string containing one or more
1159
- valid "tool" JSON substrings, or a
1160
- ChatDocument containing a `function_call` attribute.
1161
- Handle with the corresponding handler method, and return
1162
- the results as a combined string.
1163
-
1164
- Args:
1165
- msg (str | ChatDocument): The string or ChatDocument to handle
1166
-
1167
- Returns:
1168
- The result of the handler method can be:
1169
- - None if no tools successfully handled, or no tools present
1170
- - str if langroid-native JSON tools were handled, and results concatenated,
1171
- OR there's a SINGLE OpenAI tool-call.
1172
- (We do this so the common scenario of a single tool/fn-call
1173
- has a simple behavior).
1174
- - Dict[str, str] if multiple OpenAI tool-calls were handled
1175
- (dict is an id->result map)
1176
- - ChatDocument if a handler returned a ChatDocument, intended to be the
1177
- final response of the `agent_response` method.
1278
+ Return error document if the message contains multiple orchestration tools
1178
1279
  """
1179
- try:
1180
- tools = self.get_tool_messages(msg)
1181
- tools = [t for t in tools if self._tool_recipient_match(t)]
1182
- except ValidationError as ve:
1183
- # correct tool name but bad fields
1184
- return self.tool_validation_error(ve)
1185
- except XMLException as xe: # from XMLToolMessage parsing
1186
- return str(xe)
1187
- except ValueError:
1188
- # invalid tool name
1189
- # We return None since returning "invalid tool name" would
1190
- # be considered a valid result in task loop, and would be treated
1191
- # as a response to the tool message even though the tool was not intended
1192
- # for this agent.
1193
- return None
1194
- if len(tools) > 1 and not self.config.allow_multiple_tools:
1195
- return self.to_ChatDocument("ERROR: Use ONE tool at a time!")
1196
- if len(tools) == 0:
1197
- fallback_result = self.handle_message_fallback(msg)
1198
- if fallback_result is None:
1199
- return None
1200
- return self.to_ChatDocument(
1201
- fallback_result,
1202
- chat_doc=msg if isinstance(msg, ChatDocument) else None,
1203
- )
1204
- has_ids = all([t.id != "" for t in tools])
1205
- chat_doc = msg if isinstance(msg, ChatDocument) else None
1206
-
1207
1280
  # check whether there are multiple orchestration-tools (e.g. DoneTool etc),
1208
1281
  # in which case set result to error-string since we don't yet support
1209
1282
  # multi-tools with one or more orch tools.
@@ -1230,20 +1303,24 @@ class Agent(ABC):
1230
1303
  )
1231
1304
 
1232
1305
  has_orch = any(isinstance(t, ORCHESTRATION_TOOLS) for t in tools)
1233
- results: List[str | ChatDocument | None]
1234
1306
  if has_orch and len(tools) > 1:
1235
1307
  err_str = "ERROR: Use ONE tool at a time!"
1236
- results = [err_str for _ in tools]
1237
- else:
1238
- results = [self.handle_tool_message(t, chat_doc=chat_doc) for t in tools]
1239
- # if there's a solitary ChatDocument|str result, return it as is
1240
- if len(results) == 1 and isinstance(results[0], (str, ChatDocument)):
1241
- return results[0]
1242
- # extract content from ChatDocument results so we have all str|None
1243
- results = [r.content if isinstance(r, ChatDocument) else r for r in results]
1308
+ return [err_str for _ in tools]
1309
+
1310
+ return []
1311
+
1312
+ def _handle_message_final(
1313
+ self, tools: List[ToolMessage], results: List[str | ChatDocument | None]
1314
+ ) -> None | str | OrderedDict[str, str] | ChatDocument:
1315
+ """
1316
+ Convert results to final response
1317
+ """
1318
+ # extract content from ChatDocument results so we have all str|None
1319
+ results = [r.content if isinstance(r, ChatDocument) else r for r in results]
1244
1320
 
1245
- # now all results are str|None
1246
1321
  tool_names = [t.default_value("request") for t in tools]
1322
+
1323
+ has_ids = all([t.id != "" for t in tools])
1247
1324
  if has_ids:
1248
1325
  id2result = OrderedDict(
1249
1326
  (t.id, r)
@@ -1278,6 +1355,112 @@ class Agent(ABC):
1278
1355
  final = "\n\n".join(str_results)
1279
1356
  return final
1280
1357
 
1358
+ async def handle_message_async(
1359
+ self, msg: str | ChatDocument
1360
+ ) -> None | str | OrderedDict[str, str] | ChatDocument:
1361
+ """
1362
+ Asynch version of `handle_message`. See there for details.
1363
+ """
1364
+ try:
1365
+ tools = self.get_tool_messages(msg)
1366
+ tools = [t for t in tools if self._tool_recipient_match(t)]
1367
+ except ValidationError as ve:
1368
+ # correct tool name but bad fields
1369
+ return self.tool_validation_error(ve)
1370
+ except XMLException as xe: # from XMLToolMessage parsing
1371
+ return str(xe)
1372
+ except ValueError:
1373
+ # invalid tool name
1374
+ # We return None since returning "invalid tool name" would
1375
+ # be considered a valid result in task loop, and would be treated
1376
+ # as a response to the tool message even though the tool was not intended
1377
+ # for this agent.
1378
+ return None
1379
+ if len(tools) > 1 and not self.config.allow_multiple_tools:
1380
+ return self.to_ChatDocument("ERROR: Use ONE tool at a time!")
1381
+ if len(tools) == 0:
1382
+ fallback_result = self.handle_message_fallback(msg)
1383
+ if fallback_result is None:
1384
+ return None
1385
+ return self.to_ChatDocument(
1386
+ fallback_result,
1387
+ chat_doc=msg if isinstance(msg, ChatDocument) else None,
1388
+ )
1389
+ chat_doc = msg if isinstance(msg, ChatDocument) else None
1390
+
1391
+ results = self._get_multiple_orch_tool_errs(tools)
1392
+ if not results:
1393
+ results = [
1394
+ await self.handle_tool_message_async(t, chat_doc=chat_doc)
1395
+ for t in tools
1396
+ ]
1397
+ # if there's a solitary ChatDocument|str result, return it as is
1398
+ if len(results) == 1 and isinstance(results[0], (str, ChatDocument)):
1399
+ return results[0]
1400
+
1401
+ return self._handle_message_final(tools, results)
1402
+
1403
+ def handle_message(
1404
+ self, msg: str | ChatDocument
1405
+ ) -> None | str | OrderedDict[str, str] | ChatDocument:
1406
+ """
1407
+ Handle a "tool" message either a string containing one or more
1408
+ valid "tool" JSON substrings, or a
1409
+ ChatDocument containing a `function_call` attribute.
1410
+ Handle with the corresponding handler method, and return
1411
+ the results as a combined string.
1412
+
1413
+ Args:
1414
+ msg (str | ChatDocument): The string or ChatDocument to handle
1415
+
1416
+ Returns:
1417
+ The result of the handler method can be:
1418
+ - None if no tools successfully handled, or no tools present
1419
+ - str if langroid-native JSON tools were handled, and results concatenated,
1420
+ OR there's a SINGLE OpenAI tool-call.
1421
+ (We do this so the common scenario of a single tool/fn-call
1422
+ has a simple behavior).
1423
+ - Dict[str, str] if multiple OpenAI tool-calls were handled
1424
+ (dict is an id->result map)
1425
+ - ChatDocument if a handler returned a ChatDocument, intended to be the
1426
+ final response of the `agent_response` method.
1427
+ """
1428
+ try:
1429
+ tools = self.get_tool_messages(msg)
1430
+ tools = [t for t in tools if self._tool_recipient_match(t)]
1431
+ except ValidationError as ve:
1432
+ # correct tool name but bad fields
1433
+ return self.tool_validation_error(ve)
1434
+ except XMLException as xe: # from XMLToolMessage parsing
1435
+ return str(xe)
1436
+ except ValueError:
1437
+ # invalid tool name
1438
+ # We return None since returning "invalid tool name" would
1439
+ # be considered a valid result in task loop, and would be treated
1440
+ # as a response to the tool message even though the tool was not intended
1441
+ # for this agent.
1442
+ return None
1443
+ if len(tools) > 1 and not self.config.allow_multiple_tools:
1444
+ return self.to_ChatDocument("ERROR: Use ONE tool at a time!")
1445
+ if len(tools) == 0:
1446
+ fallback_result = self.handle_message_fallback(msg)
1447
+ if fallback_result is None:
1448
+ return None
1449
+ return self.to_ChatDocument(
1450
+ fallback_result,
1451
+ chat_doc=msg if isinstance(msg, ChatDocument) else None,
1452
+ )
1453
+ chat_doc = msg if isinstance(msg, ChatDocument) else None
1454
+
1455
+ results = self._get_multiple_orch_tool_errs(tools)
1456
+ if not results:
1457
+ results = [self.handle_tool_message(t, chat_doc=chat_doc) for t in tools]
1458
+ # if there's a solitary ChatDocument|str result, return it as is
1459
+ if len(results) == 1 and isinstance(results[0], (str, ChatDocument)):
1460
+ return results[0]
1461
+
1462
+ return self._handle_message_final(tools, results)
1463
+
1281
1464
  @property
1282
1465
  def all_llm_tools_known(self) -> set[str]:
1283
1466
  """All known tools; this may extend self.llm_tools_known."""
@@ -1546,6 +1729,37 @@ class Agent(ABC):
1546
1729
  ) + truncate_warning
1547
1730
  return result
1548
1731
 
1732
+ async def handle_tool_message_async(
1733
+ self,
1734
+ tool: ToolMessage,
1735
+ chat_doc: Optional[ChatDocument] = None,
1736
+ ) -> None | str | ChatDocument:
1737
+ """
1738
+ Asynch version of `handle_tool_message`. See there for details.
1739
+ """
1740
+ tool_name = tool.default_value("request")
1741
+ handler_method = getattr(self, tool_name + "_async", None)
1742
+ if handler_method is None:
1743
+ return self.handle_tool_message(tool, chat_doc=chat_doc)
1744
+ has_chat_doc_arg = (
1745
+ chat_doc is not None
1746
+ and "chat_doc" in inspect.signature(handler_method).parameters
1747
+ )
1748
+ try:
1749
+ if has_chat_doc_arg:
1750
+ maybe_result = await handler_method(tool, chat_doc=chat_doc)
1751
+ else:
1752
+ maybe_result = await handler_method(tool)
1753
+ result = self.to_ChatDocument(maybe_result, tool_name, chat_doc)
1754
+ except Exception as e:
1755
+ # raise the error here since we are sure it's
1756
+ # not a pydantic validation error,
1757
+ # which we check in `handle_message`
1758
+ raise e
1759
+ return self._maybe_truncate_result(
1760
+ result, tool._max_result_tokens
1761
+ ) # type: ignore
1762
+
1549
1763
  def handle_tool_message(
1550
1764
  self,
1551
1765
  tool: ToolMessage,