langroid 0.6.7__py3-none-any.whl → 0.9.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 (37) hide show
  1. langroid/agent/base.py +499 -55
  2. langroid/agent/callbacks/chainlit.py +1 -1
  3. langroid/agent/chat_agent.py +191 -37
  4. langroid/agent/chat_document.py +142 -29
  5. langroid/agent/openai_assistant.py +20 -4
  6. langroid/agent/special/lance_doc_chat_agent.py +25 -18
  7. langroid/agent/special/lance_rag/critic_agent.py +37 -5
  8. langroid/agent/special/lance_rag/query_planner_agent.py +102 -63
  9. langroid/agent/special/lance_tools.py +10 -2
  10. langroid/agent/special/sql/sql_chat_agent.py +69 -13
  11. langroid/agent/task.py +179 -43
  12. langroid/agent/tool_message.py +19 -7
  13. langroid/agent/tools/__init__.py +5 -0
  14. langroid/agent/tools/orchestration.py +216 -0
  15. langroid/agent/tools/recipient_tool.py +6 -11
  16. langroid/agent/tools/rewind_tool.py +1 -1
  17. langroid/agent/typed_task.py +19 -0
  18. langroid/language_models/.chainlit/config.toml +121 -0
  19. langroid/language_models/.chainlit/translations/en-US.json +231 -0
  20. langroid/language_models/base.py +114 -12
  21. langroid/language_models/mock_lm.py +10 -1
  22. langroid/language_models/openai_gpt.py +260 -36
  23. langroid/mytypes.py +0 -1
  24. langroid/parsing/parse_json.py +19 -2
  25. langroid/utils/pydantic_utils.py +19 -0
  26. langroid/vector_store/base.py +3 -1
  27. langroid/vector_store/lancedb.py +2 -0
  28. {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/METADATA +4 -1
  29. {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/RECORD +32 -33
  30. pyproject.toml +2 -1
  31. langroid/agent/special/lance_rag_new/__init__.py +0 -9
  32. langroid/agent/special/lance_rag_new/critic_agent.py +0 -171
  33. langroid/agent/special/lance_rag_new/lance_rag_task.py +0 -144
  34. langroid/agent/special/lance_rag_new/query_planner_agent.py +0 -222
  35. langroid/agent/team.py +0 -1758
  36. {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/LICENSE +0 -0
  37. {langroid-0.6.7.dist-info → langroid-0.9.0.dist-info}/WHEEL +0 -0
@@ -58,7 +58,7 @@ async def setup_llm() -> None:
58
58
  timeout = llm_settings.get("timeout", 90)
59
59
  logger.info(f"Using model: {model}")
60
60
  llm_config = lm.OpenAIGPTConfig(
61
- chat_model=model or lm.OpenAIChatModel.GPT4_TURBO,
61
+ chat_model=model or lm.OpenAIChatModel.GPT4o,
62
62
  # or, other possibilities for example:
63
63
  # "litellm/ollama_chat/mistral"
64
64
  # "litellm/ollama_chat/mistral:7b-instruct-v0.2-q8_0"
@@ -16,8 +16,10 @@ from langroid.language_models.base import (
16
16
  LLMFunctionSpec,
17
17
  LLMMessage,
18
18
  LLMResponse,
19
+ OpenAIToolSpec,
19
20
  Role,
20
21
  StreamingIfAllowed,
22
+ ToolChoiceTypes,
21
23
  )
22
24
  from langroid.language_models.openai_gpt import OpenAIGPT
23
25
  from langroid.utils.configuration import settings
@@ -39,14 +41,22 @@ class ChatAgentConfig(AgentConfig):
39
41
  user_message: user message to include in message sequence.
40
42
  Used only if `task` is not specified in the constructor.
41
43
  use_tools: whether to use our own ToolMessages mechanism
42
- use_functions_api: whether to use functions native to the LLM API
43
- (e.g. OpenAI's `function_call` mechanism)
44
+ use_functions_api: whether to use functions/tools native to the LLM API
45
+ (e.g. OpenAI's `function_call` or `tool_call` mechanism)
46
+ use_tools_api: When `use_functions_api` is True, if this is also True,
47
+ the OpenAI tool-call API is used, rather than the older/deprecated
48
+ function-call API. However the tool-call API has some tricky aspects,
49
+ hence we set this to False by default.
50
+ enable_orchestration_tool_handling: whether to enable handling of orchestration
51
+ tools, e.g. ForwardTool, DoneTool, PassTool, etc.
44
52
  """
45
53
 
46
54
  system_message: str = "You are a helpful assistant."
47
55
  user_message: Optional[str] = None
48
56
  use_tools: bool = False
49
57
  use_functions_api: bool = True
58
+ use_tools_api: bool = False
59
+ enable_orchestration_tool_handling: bool = True
50
60
 
51
61
  def _set_fn_or_tools(self, fn_available: bool) -> None:
52
62
  """
@@ -138,6 +148,30 @@ class ChatAgent(Agent):
138
148
  self.llm_functions_usable: Set[str] = set()
139
149
  self.llm_function_force: Optional[Dict[str, str]] = None
140
150
 
151
+ if self.config.enable_orchestration_tool_handling:
152
+ # Only enable HANDLING by `agent_response`, NOT LLM generation of these.
153
+ # This is useful where tool-handlers or agent_response generate these
154
+ # tools, and need to be handled.
155
+ # We don't want enable orch tool GENERATION by default, since that
156
+ # might clutter-up the LLM system message unnecessarily.
157
+ from langroid.agent.tools.orchestration import (
158
+ AgentDoneTool,
159
+ AgentSendTool,
160
+ DonePassTool,
161
+ DoneTool,
162
+ ForwardTool,
163
+ PassTool,
164
+ SendTool,
165
+ )
166
+
167
+ self.enable_message(ForwardTool, use=False, handle=True)
168
+ self.enable_message(DoneTool, use=False, handle=True)
169
+ self.enable_message(AgentDoneTool, use=False, handle=True)
170
+ self.enable_message(PassTool, use=False, handle=True)
171
+ self.enable_message(DonePassTool, use=False, handle=True)
172
+ self.enable_message(SendTool, use=False, handle=True)
173
+ self.enable_message(AgentSendTool, use=False, handle=True)
174
+
141
175
  @staticmethod
142
176
  def from_id(id: str) -> "ChatAgent":
143
177
  """
@@ -205,6 +239,23 @@ class ChatAgent(Agent):
205
239
  msgs.append(LLMMessage(role=Role.USER, content=self.user_message))
206
240
  return msgs
207
241
 
242
+ def _drop_msg_update_tool_calls(self, msg: LLMMessage) -> None:
243
+ id2idx = {t.id: i for i, t in enumerate(self.oai_tool_calls)}
244
+ if msg.role == Role.TOOL:
245
+ # dropping tool result, so ADD the corresponding tool-call back
246
+ # to the list of pending calls!
247
+ id = msg.tool_call_id
248
+ if id in self.oai_tool_id2call:
249
+ self.oai_tool_calls.append(self.oai_tool_id2call[id])
250
+ elif msg.tool_calls is not None:
251
+ # dropping a msg with tool-calls, so DROP these from pending list
252
+ # as well as from id -> call map
253
+ for tool_call in msg.tool_calls:
254
+ if tool_call.id in id2idx:
255
+ self.oai_tool_calls.pop(id2idx[tool_call.id])
256
+ if tool_call.id in self.oai_tool_id2call:
257
+ del self.oai_tool_id2call[tool_call.id]
258
+
208
259
  def clear_history(self, start: int = -2) -> None:
209
260
  """
210
261
  Clear the message history, starting at the index `start`
@@ -218,7 +269,10 @@ class ChatAgent(Agent):
218
269
  n = len(self.message_history)
219
270
  start = max(0, n + start)
220
271
  dropped = self.message_history[start:]
221
- for msg in dropped:
272
+ # consider the dropped msgs in REVERSE order, so we are
273
+ # carefully updating self.oai_tool_calls
274
+ for msg in reversed(dropped):
275
+ self._drop_msg_update_tool_calls(msg)
222
276
  # clear out the chat document from the ObjectRegistry
223
277
  ChatDocument.delete_id(msg.chat_document_id)
224
278
  self.message_history = self.message_history[:start]
@@ -245,19 +299,25 @@ class ChatAgent(Agent):
245
299
  Returns:
246
300
  str: formatting rules
247
301
  """
248
- enabled_classes: List[Type[ToolMessage]] = list(self.llm_tools_map.values())
249
- if len(enabled_classes) == 0:
302
+ # ONLY Usable tools (i.e. LLM-generation allowed),
303
+ usable_tool_classes: List[Type[ToolMessage]] = [
304
+ t
305
+ for t in list(self.llm_tools_map.values())
306
+ if not t._handle_only
307
+ and t.default_value("request") in self.llm_tools_usable
308
+ ]
309
+
310
+ if len(usable_tool_classes) == 0:
250
311
  return "You can ask questions in natural language."
251
312
  json_instructions = "\n\n".join(
252
313
  [
253
314
  msg_cls.json_instructions(tool=self.config.use_tools)
254
- for _, msg_cls in enumerate(enabled_classes)
255
- if msg_cls.default_value("request") in self.llm_tools_usable
315
+ for msg_cls in usable_tool_classes
256
316
  ]
257
317
  )
258
318
  # if any of the enabled classes has json_group_instructions, then use that,
259
319
  # else fall back to ToolMessage.json_group_instructions
260
- for msg_cls in enabled_classes:
320
+ for msg_cls in usable_tool_classes:
261
321
  if hasattr(msg_cls, "json_group_instructions") and callable(
262
322
  getattr(msg_cls, "json_group_instructions")
263
323
  ):
@@ -393,9 +453,16 @@ class ChatAgent(Agent):
393
453
  # remove leading and trailing newlines and other whitespace
394
454
  return LLMMessage(role=Role.SYSTEM, content=content.strip())
395
455
 
456
+ def unhanded_tools(self) -> set[str]:
457
+ """The set of tools that are known but not handled.
458
+ Useful in task flow: an agent can refuse to accept an incoming msg
459
+ when it only has unhandled tools.
460
+ """
461
+ return self.llm_tools_known - self.llm_tools_handled
462
+
396
463
  def enable_message(
397
464
  self,
398
- message_class: Optional[Type[ToolMessage]],
465
+ message_class: Optional[Type[ToolMessage] | List[Type[ToolMessage]]],
399
466
  use: bool = True,
400
467
  handle: bool = True,
401
468
  force: bool = False,
@@ -408,8 +475,10 @@ class ChatAgent(Agent):
408
475
  - tool HANDLING (i.e. the agent can handle JSON from this tool),
409
476
 
410
477
  Args:
411
- message_class: The ToolMessage class to enable,
478
+ message_class: The ToolMessage class OR List of such classes to enable,
412
479
  for USE, or HANDLING, or both.
480
+ If this is a list of ToolMessage classes, then the remain args are
481
+ applied to all classes.
413
482
  Optional; if None, then apply the enabling to all tools in the
414
483
  agent's toolset that have been enabled so far.
415
484
  use: IF True, allow the agent (LLM) to use this tool (or all tools),
@@ -421,12 +490,23 @@ class ChatAgent(Agent):
421
490
  `force` is ignored if `message_class` is None.
422
491
  require_recipient: whether to require that recipient be specified
423
492
  when using the tool message (only applies if `use` is True).
424
- require_defaults: whether to include fields that have default values,
493
+ include_defaults: whether to include fields that have default values,
425
494
  in the "properties" section of the JSON format instructions.
426
495
  (Normally the OpenAI completion API ignores these fields,
427
496
  but the Assistant fn-calling seems to pay attn to these,
428
497
  and if we don't want this, we should set this to False.)
429
498
  """
499
+ if message_class is not None and isinstance(message_class, list):
500
+ for mc in message_class:
501
+ self.enable_message(
502
+ mc,
503
+ use=use,
504
+ handle=handle,
505
+ force=force,
506
+ require_recipient=require_recipient,
507
+ include_defaults=include_defaults,
508
+ )
509
+ return None
430
510
  if require_recipient and message_class is not None:
431
511
  message_class = message_class.require_recipient()
432
512
  super().enable_message_handling(message_class) # enables handling only
@@ -441,6 +521,8 @@ class ChatAgent(Agent):
441
521
  self.llm_function_force = None
442
522
 
443
523
  for t in tools:
524
+ self.llm_tools_known.add(t)
525
+
444
526
  if handle:
445
527
  self.llm_tools_handled.add(t)
446
528
  self.llm_functions_handled.add(t)
@@ -519,9 +601,14 @@ class ChatAgent(Agent):
519
601
  hist, output_len = self._prep_llm_messages(message)
520
602
  if len(hist) == 0:
521
603
  return None
604
+ tool_choice = (
605
+ "auto"
606
+ if isinstance(message, str)
607
+ else (message.oai_tool_choice if message is not None else "auto")
608
+ )
522
609
  with StreamingIfAllowed(self.llm, self.llm.get_stream()):
523
- response = self.llm_response_messages(hist, output_len)
524
- self.message_history.append(ChatDocument.to_LLMMessage(response))
610
+ response = self.llm_response_messages(hist, output_len, tool_choice)
611
+ self.message_history.extend(ChatDocument.to_LLMMessage(response))
525
612
  response.metadata.msg_idx = len(self.message_history) - 1
526
613
  response.metadata.agent_id = self.id
527
614
  # Preserve trail of tool_ids for OpenAI Assistant fn-calls
@@ -543,9 +630,16 @@ class ChatAgent(Agent):
543
630
  hist, output_len = self._prep_llm_messages(message)
544
631
  if len(hist) == 0:
545
632
  return None
633
+ tool_choice = (
634
+ "auto"
635
+ if isinstance(message, str)
636
+ else (message.oai_tool_choice if message is not None else "auto")
637
+ )
546
638
  with StreamingIfAllowed(self.llm, self.llm.get_stream()):
547
- response = await self.llm_response_messages_async(hist, output_len)
548
- self.message_history.append(ChatDocument.to_LLMMessage(response))
639
+ response = await self.llm_response_messages_async(
640
+ hist, output_len, tool_choice
641
+ )
642
+ self.message_history.extend(ChatDocument.to_LLMMessage(response))
549
643
  response.metadata.msg_idx = len(self.message_history) - 1
550
644
  response.metadata.agent_id = self.id
551
645
  # Preserve trail of tool_ids for OpenAI Assistant fn-calls
@@ -622,8 +716,18 @@ class ChatAgent(Agent):
622
716
  ):
623
717
  # either the message is a str, or it is a fresh ChatDocument
624
718
  # different from the last message in the history
625
- llm_msg = ChatDocument.to_LLMMessage(message)
626
- self.message_history.append(llm_msg)
719
+ llm_msgs = ChatDocument.to_LLMMessage(message, self.oai_tool_calls)
720
+ # LLM only responds to the content, so only those msgs with
721
+ # non-empty content should be kept
722
+ llm_msgs = [m for m in llm_msgs if m.content != ""]
723
+ if len(llm_msgs) == 0:
724
+ return [], 0
725
+ # process tools if any
726
+ done_tools = [m.tool_call_id for m in llm_msgs if m.role == Role.TOOL]
727
+ self.oai_tool_calls = [
728
+ t for t in self.oai_tool_calls if t.id not in done_tools
729
+ ]
730
+ self.message_history.extend(llm_msgs)
627
731
 
628
732
  hist = self.message_history
629
733
  output_len = self.config.llm.max_output_tokens
@@ -707,18 +811,47 @@ class ChatAgent(Agent):
707
811
 
708
812
  def _function_args(
709
813
  self,
710
- ) -> Tuple[Optional[List[LLMFunctionSpec]], str | Dict[str, str]]:
814
+ ) -> Tuple[
815
+ Optional[List[LLMFunctionSpec]],
816
+ str | Dict[str, str],
817
+ Optional[List[OpenAIToolSpec]],
818
+ Optional[Dict[str, Dict[str, str] | str]],
819
+ ]:
820
+ """Get function/tool spec arguments for OpenAI-compatible LLM API call"""
711
821
  functions: Optional[List[LLMFunctionSpec]] = None
712
822
  fun_call: str | Dict[str, str] = "none"
823
+ tools: Optional[List[OpenAIToolSpec]] = None
824
+ force_tool: Optional[Dict[str, Dict[str, str] | str]] = None
713
825
  if self.config.use_functions_api and len(self.llm_functions_usable) > 0:
714
- functions = [self.llm_functions_map[f] for f in self.llm_functions_usable]
715
- fun_call = (
716
- "auto" if self.llm_function_force is None else self.llm_function_force
717
- )
718
- return functions, fun_call
826
+ if not self.config.use_tools_api:
827
+ functions = [
828
+ self.llm_functions_map[f] for f in self.llm_functions_usable
829
+ ]
830
+ fun_call = (
831
+ "auto"
832
+ if self.llm_function_force is None
833
+ else self.llm_function_force
834
+ )
835
+ else:
836
+ tools = [
837
+ OpenAIToolSpec(type="function", function=self.llm_functions_map[f])
838
+ for f in self.llm_functions_usable
839
+ ]
840
+ force_tool = (
841
+ None
842
+ if self.llm_function_force is None
843
+ else {
844
+ "type": "function",
845
+ "function": {"name": self.llm_function_force["name"]},
846
+ }
847
+ )
848
+ return functions, fun_call, tools, force_tool
719
849
 
720
850
  def llm_response_messages(
721
- self, messages: List[LLMMessage], output_len: Optional[int] = None
851
+ self,
852
+ messages: List[LLMMessage],
853
+ output_len: Optional[int] = None,
854
+ tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
722
855
  ) -> ChatDocument:
723
856
  """
724
857
  Respond to a series of messages, e.g. with OpenAI ChatCompletion
@@ -748,11 +881,13 @@ class ChatAgent(Agent):
748
881
  stack.enter_context(cm)
749
882
  if self.llm.get_stream() and not settings.quiet:
750
883
  console.print(f"[green]{self.indent}", end="")
751
- functions, fun_call = self._function_args()
884
+ functions, fun_call, tools, force_tool = self._function_args()
752
885
  assert self.llm is not None
753
886
  response = self.llm.chat(
754
887
  messages,
755
888
  output_len,
889
+ tools=tools,
890
+ tool_choice=force_tool or tool_choice,
756
891
  functions=functions,
757
892
  function_call=fun_call,
758
893
  )
@@ -775,23 +910,24 @@ class ChatAgent(Agent):
775
910
  print_response_stats=self.config.show_stats and not settings.quiet,
776
911
  )
777
912
  chat_doc = ChatDocument.from_LLMResponse(response, displayed=True)
913
+ self.oai_tool_calls = response.oai_tool_calls or []
914
+ self.oai_tool_id2call.update(
915
+ {t.id: t for t in self.oai_tool_calls if t.id is not None}
916
+ )
778
917
  return chat_doc
779
918
 
780
919
  async def llm_response_messages_async(
781
- self, messages: List[LLMMessage], output_len: Optional[int] = None
920
+ self,
921
+ messages: List[LLMMessage],
922
+ output_len: Optional[int] = None,
923
+ tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
782
924
  ) -> ChatDocument:
783
925
  """
784
926
  Async version of `llm_response_messages`. See there for details.
785
927
  """
786
928
  assert self.config.llm is not None and self.llm is not None
787
929
  output_len = output_len or self.config.llm.max_output_tokens
788
- functions: Optional[List[LLMFunctionSpec]] = None
789
- fun_call: str | Dict[str, str] = "none"
790
- if self.config.use_functions_api and len(self.llm_functions_usable) > 0:
791
- functions = [self.llm_functions_map[f] for f in self.llm_functions_usable]
792
- fun_call = (
793
- "auto" if self.llm_function_force is None else self.llm_function_force
794
- )
930
+ functions, fun_call, tools, force_tool = self._function_args()
795
931
  assert self.llm is not None
796
932
 
797
933
  streamer = noop_fn
@@ -802,6 +938,8 @@ class ChatAgent(Agent):
802
938
  response = await self.llm.achat(
803
939
  messages,
804
940
  output_len,
941
+ tools=tools,
942
+ tool_choice=force_tool or tool_choice,
805
943
  functions=functions,
806
944
  function_call=fun_call,
807
945
  )
@@ -824,6 +962,10 @@ class ChatAgent(Agent):
824
962
  print_response_stats=self.config.show_stats and not settings.quiet,
825
963
  )
826
964
  chat_doc = ChatDocument.from_LLMResponse(response, displayed=True)
965
+ self.oai_tool_calls = response.oai_tool_calls or []
966
+ self.oai_tool_id2call.update(
967
+ {t.id: t for t in self.oai_tool_calls if t.id is not None}
968
+ )
827
969
  return chat_doc
828
970
 
829
971
  def _render_llm_response(
@@ -847,6 +989,7 @@ class ChatAgent(Agent):
847
989
  if isinstance(response, ChatDocument)
848
990
  else ChatDocument.from_LLMResponse(response, displayed=True)
849
991
  )
992
+ # TODO: prepend TOOL: or OAI-TOOL: if it's a tool-call
850
993
  print(cached + "[green]" + escape(str(response)))
851
994
  self.callbacks.show_llm_response(
852
995
  content=str(response),
@@ -923,8 +1066,14 @@ class ChatAgent(Agent):
923
1066
  # If there is a response, then we will have two additional
924
1067
  # messages in the message history, i.e. the user message and the
925
1068
  # assistant response. We want to (carefully) remove these two messages.
926
- self.message_history.pop() if len(self.message_history) > n_msgs else None
927
- self.message_history.pop() if len(self.message_history) > n_msgs else None
1069
+ if len(self.message_history) > n_msgs:
1070
+ msg = self.message_history.pop()
1071
+ self._drop_msg_update_tool_calls(msg)
1072
+
1073
+ if len(self.message_history) > n_msgs:
1074
+ msg = self.message_history.pop()
1075
+ self._drop_msg_update_tool_calls(msg)
1076
+
928
1077
  return response
929
1078
 
930
1079
  async def llm_response_forget_async(self, message: str) -> ChatDocument:
@@ -941,8 +1090,13 @@ class ChatAgent(Agent):
941
1090
  # If there is a response, then we will have two additional
942
1091
  # messages in the message history, i.e. the user message and the
943
1092
  # assistant response. We want to (carefully) remove these two messages.
944
- self.message_history.pop() if len(self.message_history) > n_msgs else None
945
- self.message_history.pop() if len(self.message_history) > n_msgs else None
1093
+ if len(self.message_history) > n_msgs:
1094
+ msg = self.message_history.pop()
1095
+ self._drop_msg_update_tool_calls(msg)
1096
+
1097
+ if len(self.message_history) > n_msgs:
1098
+ msg = self.message_history.pop()
1099
+ self._drop_msg_update_tool_calls(msg)
946
1100
  return response
947
1101
 
948
1102
  def chat_num_tokens(self, messages: Optional[List[LLMMessage]] = None) -> int: