langroid 0.1.139__py3-none-any.whl → 0.1.219__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 (97) hide show
  1. langroid/__init__.py +70 -0
  2. langroid/agent/__init__.py +22 -0
  3. langroid/agent/base.py +120 -33
  4. langroid/agent/batch.py +134 -35
  5. langroid/agent/callbacks/__init__.py +0 -0
  6. langroid/agent/callbacks/chainlit.py +608 -0
  7. langroid/agent/chat_agent.py +164 -100
  8. langroid/agent/chat_document.py +19 -2
  9. langroid/agent/openai_assistant.py +20 -10
  10. langroid/agent/special/__init__.py +33 -10
  11. langroid/agent/special/doc_chat_agent.py +521 -108
  12. langroid/agent/special/lance_doc_chat_agent.py +258 -0
  13. langroid/agent/special/lance_rag/__init__.py +9 -0
  14. langroid/agent/special/lance_rag/critic_agent.py +136 -0
  15. langroid/agent/special/lance_rag/lance_rag_task.py +80 -0
  16. langroid/agent/special/lance_rag/query_planner_agent.py +180 -0
  17. langroid/agent/special/lance_tools.py +44 -0
  18. langroid/agent/special/neo4j/__init__.py +0 -0
  19. langroid/agent/special/neo4j/csv_kg_chat.py +174 -0
  20. langroid/agent/special/neo4j/neo4j_chat_agent.py +370 -0
  21. langroid/agent/special/neo4j/utils/__init__.py +0 -0
  22. langroid/agent/special/neo4j/utils/system_message.py +46 -0
  23. langroid/agent/special/relevance_extractor_agent.py +23 -7
  24. langroid/agent/special/retriever_agent.py +29 -174
  25. langroid/agent/special/sql/__init__.py +7 -0
  26. langroid/agent/special/sql/sql_chat_agent.py +47 -23
  27. langroid/agent/special/sql/utils/__init__.py +11 -0
  28. langroid/agent/special/sql/utils/description_extractors.py +95 -46
  29. langroid/agent/special/sql/utils/populate_metadata.py +28 -21
  30. langroid/agent/special/table_chat_agent.py +43 -9
  31. langroid/agent/task.py +423 -114
  32. langroid/agent/tool_message.py +67 -10
  33. langroid/agent/tools/__init__.py +8 -0
  34. langroid/agent/tools/duckduckgo_search_tool.py +66 -0
  35. langroid/agent/tools/google_search_tool.py +11 -0
  36. langroid/agent/tools/metaphor_search_tool.py +67 -0
  37. langroid/agent/tools/recipient_tool.py +6 -24
  38. langroid/agent/tools/sciphi_search_rag_tool.py +79 -0
  39. langroid/cachedb/__init__.py +6 -0
  40. langroid/embedding_models/__init__.py +24 -0
  41. langroid/embedding_models/base.py +9 -1
  42. langroid/embedding_models/models.py +117 -17
  43. langroid/embedding_models/protoc/embeddings.proto +19 -0
  44. langroid/embedding_models/protoc/embeddings_pb2.py +33 -0
  45. langroid/embedding_models/protoc/embeddings_pb2.pyi +50 -0
  46. langroid/embedding_models/protoc/embeddings_pb2_grpc.py +79 -0
  47. langroid/embedding_models/remote_embeds.py +153 -0
  48. langroid/language_models/__init__.py +22 -0
  49. langroid/language_models/azure_openai.py +47 -4
  50. langroid/language_models/base.py +26 -10
  51. langroid/language_models/config.py +5 -0
  52. langroid/language_models/openai_gpt.py +407 -121
  53. langroid/language_models/prompt_formatter/__init__.py +9 -0
  54. langroid/language_models/prompt_formatter/base.py +4 -6
  55. langroid/language_models/prompt_formatter/hf_formatter.py +135 -0
  56. langroid/language_models/utils.py +10 -9
  57. langroid/mytypes.py +10 -4
  58. langroid/parsing/__init__.py +33 -1
  59. langroid/parsing/document_parser.py +259 -63
  60. langroid/parsing/image_text.py +32 -0
  61. langroid/parsing/parse_json.py +143 -0
  62. langroid/parsing/parser.py +20 -7
  63. langroid/parsing/repo_loader.py +108 -46
  64. langroid/parsing/search.py +8 -0
  65. langroid/parsing/table_loader.py +44 -0
  66. langroid/parsing/url_loader.py +59 -13
  67. langroid/parsing/urls.py +18 -9
  68. langroid/parsing/utils.py +130 -9
  69. langroid/parsing/web_search.py +73 -0
  70. langroid/prompts/__init__.py +7 -0
  71. langroid/prompts/chat-gpt4-system-prompt.md +68 -0
  72. langroid/prompts/prompts_config.py +1 -1
  73. langroid/utils/__init__.py +10 -0
  74. langroid/utils/algorithms/__init__.py +3 -0
  75. langroid/utils/configuration.py +0 -1
  76. langroid/utils/constants.py +4 -0
  77. langroid/utils/logging.py +2 -5
  78. langroid/utils/output/__init__.py +15 -2
  79. langroid/utils/output/status.py +33 -0
  80. langroid/utils/pandas_utils.py +30 -0
  81. langroid/utils/pydantic_utils.py +446 -4
  82. langroid/utils/system.py +36 -1
  83. langroid/vector_store/__init__.py +34 -2
  84. langroid/vector_store/base.py +33 -2
  85. langroid/vector_store/chromadb.py +42 -13
  86. langroid/vector_store/lancedb.py +226 -60
  87. langroid/vector_store/meilisearch.py +7 -6
  88. langroid/vector_store/momento.py +3 -2
  89. langroid/vector_store/qdrantdb.py +82 -11
  90. {langroid-0.1.139.dist-info → langroid-0.1.219.dist-info}/METADATA +190 -129
  91. langroid-0.1.219.dist-info/RECORD +127 -0
  92. langroid/agent/special/recipient_validator_agent.py +0 -157
  93. langroid/parsing/json.py +0 -64
  94. langroid/utils/web/selenium_login.py +0 -36
  95. langroid-0.1.139.dist-info/RECORD +0 -103
  96. {langroid-0.1.139.dist-info → langroid-0.1.219.dist-info}/LICENSE +0 -0
  97. {langroid-0.1.139.dist-info → langroid-0.1.219.dist-info}/WHEEL +0 -0
langroid/__init__.py CHANGED
@@ -20,9 +20,79 @@ from .agent.base import (
20
20
  AgentConfig,
21
21
  )
22
22
 
23
+ from .agent.batch import (
24
+ run_batch_tasks,
25
+ llm_response_batch,
26
+ agent_response_batch,
27
+ )
28
+
29
+ from .agent.chat_document import (
30
+ ChatDocument,
31
+ ChatDocMetaData,
32
+ )
33
+
34
+ from .agent.tool_message import (
35
+ ToolMessage,
36
+ )
37
+
23
38
  from .agent.chat_agent import (
24
39
  ChatAgent,
25
40
  ChatAgentConfig,
26
41
  )
27
42
 
28
43
  from .agent.task import Task
44
+
45
+ try:
46
+ from .agent.callbacks.chainlit import (
47
+ ChainlitAgentCallbacks,
48
+ ChainlitTaskCallbacks,
49
+ ChainlitCallbackConfig,
50
+ )
51
+
52
+ chainlit_available = True
53
+ ChainlitAgentCallbacks
54
+ ChainlitTaskCallbacks
55
+ ChainlitCallbackConfig
56
+ except ImportError:
57
+ chainlit_available = False
58
+
59
+
60
+ from .mytypes import (
61
+ DocMetaData,
62
+ Document,
63
+ Entity,
64
+ )
65
+
66
+ __all__ = [
67
+ "mytypes",
68
+ "utils",
69
+ "parsing",
70
+ "prompts",
71
+ "cachedb",
72
+ "language_models",
73
+ "embedding_models",
74
+ "vector_store",
75
+ "agent",
76
+ "Agent",
77
+ "AgentConfig",
78
+ "ChatAgent",
79
+ "ChatAgentConfig",
80
+ "ChatDocument",
81
+ "ChatDocMetaData",
82
+ "Task",
83
+ "DocMetaData",
84
+ "Document",
85
+ "Entity",
86
+ "ToolMessage",
87
+ "run_batch_tasks",
88
+ "llm_response_batch",
89
+ "agent_response_batch",
90
+ ]
91
+ if chainlit_available:
92
+ __all__.extend(
93
+ [
94
+ "ChainlitAgentCallbacks",
95
+ "ChainlitTaskCallbacks",
96
+ "ChainlitCallbackConfig",
97
+ ]
98
+ )
@@ -13,6 +13,28 @@ from . import base
13
13
  from . import chat_document
14
14
  from . import chat_agent
15
15
  from . import task
16
+ from . import batch
16
17
  from . import tool_message
17
18
  from . import tools
18
19
  from . import special
20
+
21
+ __all__ = [
22
+ "Agent",
23
+ "AgentConfig",
24
+ "ChatDocAttachment",
25
+ "ChatDocMetaData",
26
+ "ChatDocLoggerFields",
27
+ "ChatDocument",
28
+ "ChatAgent",
29
+ "ChatAgentConfig",
30
+ "ToolMessage",
31
+ "Task",
32
+ "base",
33
+ "chat_document",
34
+ "chat_agent",
35
+ "task",
36
+ "batch",
37
+ "tool_message",
38
+ "tools",
39
+ "special",
40
+ ]
langroid/agent/base.py CHANGED
@@ -4,6 +4,7 @@ import json
4
4
  import logging
5
5
  from abc import ABC
6
6
  from contextlib import ExitStack
7
+ from types import SimpleNamespace
7
8
  from typing import (
8
9
  Any,
9
10
  Callable,
@@ -21,6 +22,7 @@ from typing import (
21
22
  from pydantic import BaseSettings, ValidationError
22
23
  from rich import print
23
24
  from rich.console import Console
25
+ from rich.markup import escape
24
26
  from rich.prompt import Prompt
25
27
 
26
28
  from langroid.agent.chat_document import ChatDocMetaData, ChatDocument
@@ -35,11 +37,12 @@ from langroid.language_models.base import (
35
37
  )
36
38
  from langroid.language_models.openai_gpt import OpenAIGPTConfig
37
39
  from langroid.mytypes import Entity
38
- from langroid.parsing.json import extract_top_level_json
40
+ from langroid.parsing.parse_json import extract_top_level_json
39
41
  from langroid.parsing.parser import Parser, ParsingConfig
40
42
  from langroid.prompts.prompts_config import PromptsConfig
41
43
  from langroid.utils.configuration import settings
42
44
  from langroid.utils.constants import NO_ANSWER
45
+ from langroid.utils.output import status
43
46
  from langroid.vector_store.base import VectorStore, VectorStoreConfig
44
47
 
45
48
  console = Console(quiet=settings.quiet)
@@ -62,6 +65,10 @@ class AgentConfig(BaseSettings):
62
65
  show_stats: bool = True # show token usage/cost stats?
63
66
 
64
67
 
68
+ def noop_fn(*args: List[Any], **kwargs: Dict[str, Any]) -> None:
69
+ pass
70
+
71
+
65
72
  class Agent(ABC):
66
73
  """
67
74
  An Agent is an abstraction that encapsulates mainly two components:
@@ -73,7 +80,7 @@ class Agent(ABC):
73
80
  information about any tool/function-calling messages that have been defined.
74
81
  """
75
82
 
76
- def __init__(self, config: AgentConfig):
83
+ def __init__(self, config: AgentConfig = AgentConfig()):
77
84
  self.config = config
78
85
  self.lock = asyncio.Lock() # for async access to update self.llm.usage_cost
79
86
  self.dialog: List[Tuple[str, str]] = [] # seq of LLM (prompt, response) tuples
@@ -90,6 +97,18 @@ class Agent(ABC):
90
97
  self.parser: Optional[Parser] = (
91
98
  Parser(config.parsing) if config.parsing else None
92
99
  )
100
+ self.callbacks = SimpleNamespace(
101
+ start_llm_stream=lambda: noop_fn,
102
+ cancel_llm_stream=noop_fn,
103
+ finish_llm_stream=noop_fn,
104
+ show_llm_response=noop_fn,
105
+ show_agent_response=noop_fn,
106
+ get_user_response=None,
107
+ get_last_step=noop_fn,
108
+ set_parent_agent=noop_fn,
109
+ show_error_message=noop_fn,
110
+ show_start_response=noop_fn,
111
+ )
93
112
 
94
113
  def entity_responders(
95
114
  self,
@@ -253,6 +272,10 @@ class Agent(ABC):
253
272
  ]
254
273
  return "\n\n".join(sample_convo)
255
274
 
275
+ def agent_response_template(self) -> ChatDocument:
276
+ """Template for agent_response."""
277
+ return self._response_template(Entity.AGENT)
278
+
256
279
  async def agent_response_async(
257
280
  self,
258
281
  msg: Optional[str | ChatDocument] = None,
@@ -290,6 +313,11 @@ class Agent(ABC):
290
313
  if not settings.quiet:
291
314
  console.print(f"[red]{self.indent}", end="")
292
315
  print(f"[red]Agent: {results}")
316
+ maybe_json = len(extract_top_level_json(results)) > 0
317
+ self.callbacks.show_agent_response(
318
+ content=results,
319
+ language="json" if maybe_json else "text",
320
+ )
293
321
  sender_name = self.config.name
294
322
  if isinstance(msg, ChatDocument) and msg.function_call is not None:
295
323
  # if result was from handling an LLM `function_call`,
@@ -307,6 +335,20 @@ class Agent(ABC):
307
335
  ),
308
336
  )
309
337
 
338
+ def _response_template(self, e: Entity) -> ChatDocument:
339
+ """Template for response from entity `e`."""
340
+ return ChatDocument(
341
+ content="",
342
+ tool_messages=[],
343
+ metadata=ChatDocMetaData(
344
+ source=e, sender=e, sender_name=self.config.name, tool_ids=[]
345
+ ),
346
+ )
347
+
348
+ def user_response_template(self) -> ChatDocument:
349
+ """Template for user_response."""
350
+ return self._response_template(Entity.USER)
351
+
310
352
  async def user_response_async(
311
353
  self,
312
354
  msg: Optional[str | ChatDocument] = None,
@@ -334,11 +376,18 @@ class Agent(ABC):
334
376
  elif not settings.interactive:
335
377
  user_msg = ""
336
378
  else:
337
- user_msg = Prompt.ask(
338
- f"[blue]{self.indent}Human "
339
- "(respond or q, x to exit current level, "
340
- f"or hit enter to continue)\n{self.indent}",
341
- ).strip()
379
+ if self.callbacks.get_user_response is not None:
380
+ # ask user with empty prompt: no need for prompt
381
+ # since user has seen the conversation so far.
382
+ # But non-empty prompt can be useful when Agent
383
+ # uses a tool that requires user input, or in other scenarios.
384
+ user_msg = self.callbacks.get_user_response(prompt="")
385
+ else:
386
+ user_msg = Prompt.ask(
387
+ f"[blue]{self.indent}Human "
388
+ "(respond or q, x to exit current level, "
389
+ f"or hit enter to continue)\n{self.indent}",
390
+ ).strip()
342
391
 
343
392
  tool_ids = []
344
393
  if msg is not None and isinstance(msg, ChatDocument):
@@ -377,13 +426,6 @@ class Agent(ABC):
377
426
  if self.llm is None:
378
427
  return False
379
428
 
380
- if isinstance(message, ChatDocument) and message.function_call is not None:
381
- # LLM should not handle `function_call` messages,
382
- # EVEN if message.function_call is not a legit function_call
383
- # The OpenAI API raises error if there is a message in history
384
- # from a non-Assistant role, with a `function_call` in it
385
- return False
386
-
387
429
  if message is not None and len(self.get_tool_messages(message)) > 0:
388
430
  # if there is a valid "tool" message (either JSON or via `function_call`)
389
431
  # then LLM cannot respond to it
@@ -391,6 +433,10 @@ class Agent(ABC):
391
433
 
392
434
  return True
393
435
 
436
+ def llm_response_template(self) -> ChatDocument:
437
+ """Template for llm_response."""
438
+ return self._response_template(Entity.LLM)
439
+
394
440
  @no_type_check
395
441
  async def llm_response_async(
396
442
  self,
@@ -434,7 +480,7 @@ class Agent(ABC):
434
480
  # streaming was enabled, AND we did not find a cached response.
435
481
  # If we are here, it means the response has not yet been displayed.
436
482
  cached = f"[red]{self.indent}(cached)[/red]" if response.cached else ""
437
- print(cached + "[green]" + response.message)
483
+ print(cached + "[green]" + escape(response.message))
438
484
  async with self.lock:
439
485
  self.update_token_usage(
440
486
  response,
@@ -472,7 +518,7 @@ class Agent(ABC):
472
518
  with ExitStack() as stack: # for conditionally using rich spinner
473
519
  if not self.llm.get_stream():
474
520
  # show rich spinner only if not streaming!
475
- cm = console.status("LLM responding to message...")
521
+ cm = status("LLM responding to message...")
476
522
  stack.enter_context(cm)
477
523
  output_len = self.config.llm.max_output_tokens
478
524
  if (
@@ -507,7 +553,7 @@ class Agent(ABC):
507
553
  # If we are here, it means the response has not yet been displayed.
508
554
  cached = f"[red]{self.indent}(cached)[/red]" if response.cached else ""
509
555
  console.print(f"[green]{self.indent}", end="")
510
- print(cached + "[green]" + response.message)
556
+ print(cached + "[green]" + escape(response.message))
511
557
  self.update_token_usage(
512
558
  response,
513
559
  prompt,
@@ -520,17 +566,38 @@ class Agent(ABC):
520
566
  cdoc.metadata.tool_ids = [] if isinstance(msg, str) else msg.metadata.tool_ids
521
567
  return cdoc
522
568
 
569
+ def has_tool_message_attempt(self, msg: str | ChatDocument | None) -> bool:
570
+ """Check whether msg contains a Tool/fn-call attempt (by the LLM)"""
571
+ if msg is None:
572
+ return False
573
+ try:
574
+ tools = self.get_tool_messages(msg)
575
+ return len(tools) > 0
576
+ except ValidationError:
577
+ # there is a tool/fn-call attempt but had a validation error,
578
+ # so we still consider this a tool message "attempt"
579
+ return True
580
+ return False
581
+
523
582
  def get_tool_messages(self, msg: str | ChatDocument) -> List[ToolMessage]:
524
583
  if isinstance(msg, str):
525
584
  return self.get_json_tool_messages(msg)
585
+ if len(msg.tool_messages) > 0:
586
+ # We've already found tool_messages
587
+ # (either via OpenAI Fn-call or Langroid-native ToolMessage)
588
+ return msg.tool_messages
526
589
  assert isinstance(msg, ChatDocument)
527
590
  # when `content` is non-empty, we assume there will be no `function_call`
528
591
  if msg.content != "":
529
- return self.get_json_tool_messages(msg.content)
592
+ tools = self.get_json_tool_messages(msg.content)
593
+ msg.tool_messages = tools
594
+ return tools
530
595
 
531
596
  # otherwise, we look for a `function_call`
532
597
  fun_call_cls = self.get_function_call_class(msg)
533
- return [fun_call_cls] if fun_call_cls is not None else []
598
+ tools = [fun_call_cls] if fun_call_cls is not None else []
599
+ msg.tool_messages = tools
600
+ return tools
534
601
 
535
602
  def get_json_tool_messages(self, input_str: str) -> List[ToolMessage]:
536
603
  """
@@ -554,7 +621,17 @@ class Agent(ABC):
554
621
  tool_name = msg.function_call.name
555
622
  tool_msg = msg.function_call.arguments or {}
556
623
  if tool_name not in self.llm_tools_handled:
557
- raise ValueError(f"{tool_name} is not a valid function_call!")
624
+ logger.warning(
625
+ f"""
626
+ The function_call '{tool_name}' is not handled
627
+ by the agent named '{self.config.name}'!
628
+ If you intended this agent to handle this function_call,
629
+ either the fn-call name is incorrectly generated by the LLM,
630
+ (in which case you may need to adjust your LLM instructions),
631
+ or you need to enable this agent to handle this fn-call.
632
+ """
633
+ )
634
+ return None
558
635
  tool_class = self.llm_tools_map[tool_name]
559
636
  tool_msg.update(dict(request=tool_name))
560
637
  tool = tool_class.parse_obj(tool_msg)
@@ -573,7 +650,7 @@ class Agent(ABC):
573
650
  """
574
651
  tool_name = cast(ToolMessage, ve.model).default_value("request")
575
652
  bad_field_errors = "\n".join(
576
- [f"{e['loc'][0]}: {e['msg']}" for e in ve.errors() if "loc" in e]
653
+ [f"{e['loc']}: {e['msg']}" for e in ve.errors() if "loc" in e]
577
654
  )
578
655
  return f"""
579
656
  There were one or more errors in your attempt to use the
@@ -617,7 +694,7 @@ class Agent(ABC):
617
694
 
618
695
  results_list = [r for r in results if r is not None]
619
696
  if len(results_list) == 0:
620
- return self.handle_message_fallback(msg)
697
+ return None # self.handle_message_fallback(msg)
621
698
  # there was a non-None result
622
699
  chat_doc_results = [r for r in results_list if isinstance(r, ChatDocument)]
623
700
  if len(chat_doc_results) > 1:
@@ -632,19 +709,13 @@ class Agent(ABC):
632
709
 
633
710
  str_doc_results = [r for r in results_list if isinstance(r, str)]
634
711
  final = "\n".join(str_doc_results)
635
- if final == "":
636
- logger.warning(
637
- """final result from a tool handler should not be empty str, since
638
- it would be considered an invalid result and other responders
639
- will be tried, and we may not necessarily want that"""
640
- )
641
712
  return final
642
713
 
643
714
  def handle_message_fallback(
644
715
  self, msg: str | ChatDocument
645
716
  ) -> str | ChatDocument | None:
646
717
  """
647
- Fallback method to handle possible "tool" msg if not other method applies
718
+ Fallback method to handle possible "tool" msg if no other method applies
648
719
  or if an error is thrown.
649
720
  This method can be overridden by subclasses.
650
721
 
@@ -659,7 +730,11 @@ class Agent(ABC):
659
730
  def _get_one_tool_message(self, json_str: str) -> Optional[ToolMessage]:
660
731
  json_data = json.loads(json_str)
661
732
  request = json_data.get("request")
662
- if request is None or request not in self.llm_tools_handled:
733
+ if (
734
+ request is None
735
+ or not (isinstance(request, str))
736
+ or request not in self.llm_tools_handled
737
+ ):
663
738
  return None
664
739
 
665
740
  message_class = self.llm_tools_map.get(request)
@@ -702,7 +777,13 @@ class Agent(ABC):
702
777
  if isinstance(prompt, str):
703
778
  return self.parser.num_tokens(prompt)
704
779
  else:
705
- return sum([self.parser.num_tokens(m.content) for m in prompt])
780
+ return sum(
781
+ [
782
+ self.parser.num_tokens(m.content)
783
+ + self.parser.num_tokens(str(m.function_call or ""))
784
+ for m in prompt
785
+ ]
786
+ )
706
787
 
707
788
  def _get_response_stats(
708
789
  self, chat_length: int, tot_cost: float, response: LLMResponse
@@ -727,11 +808,17 @@ class Agent(ABC):
727
808
  assert isinstance(self.llm, LanguageModel)
728
809
  context_length = self.llm.chat_context_length()
729
810
  max_out = self.config.llm.max_output_tokens
811
+
812
+ llm_model = (
813
+ "no-LLM" if self.config.llm is None else self.llm.config.chat_model
814
+ )
815
+
730
816
  return (
731
- f"[bold]Stats:[/bold] [magenta] N_MSG={chat_length}, "
817
+ f"[bold]Stats:[/bold] [magenta]N_MSG={chat_length}, "
732
818
  f"TOKENS: in={in_tokens}, out={out_tokens}, "
733
819
  f"max={max_out}, ctx={context_length}, "
734
- f"COST: now=${llm_response_cost}, cumul=${cumul_cost}[/magenta]"
820
+ f"COST: now=${llm_response_cost}, cumul=${cumul_cost} "
821
+ f"[bold]({llm_model})[/bold][/magenta]"
735
822
  )
736
823
  return ""
737
824