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
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import sys
6
6
  import warnings
7
+ from collections import defaultdict
7
8
  from enum import Enum
8
9
  from functools import cache
9
10
  from itertools import chain
@@ -37,7 +38,10 @@ from langroid.language_models.base import (
37
38
  LLMMessage,
38
39
  LLMResponse,
39
40
  LLMTokenUsage,
41
+ OpenAIToolCall,
42
+ OpenAIToolSpec,
40
43
  Role,
44
+ ToolChoiceTypes,
41
45
  )
42
46
  from langroid.language_models.config import HFPromptFormatterConfig
43
47
  from langroid.language_models.prompt_formatter.hf_formatter import (
@@ -544,7 +548,7 @@ class OpenAIGPT(LanguageModel):
544
548
  Order of priority:
545
549
  - (1) Params (mainly max_tokens) in the chat/achat/generate/agenerate call
546
550
  (these are passed in via kwargs)
547
- - (2) Params in OpenAIGPTConfi.params (of class OpenAICallParams)
551
+ - (2) Params in OpenAIGPTConfig.params (of class OpenAICallParams)
548
552
  - (3) Specific Params in OpenAIGPTConfig (just temperature for now)
549
553
  """
550
554
  params = dict(
@@ -614,6 +618,7 @@ class OpenAIGPT(LanguageModel):
614
618
  self,
615
619
  event,
616
620
  chat: bool = False,
621
+ tool_deltas: List[Dict[str, Any]] = [],
617
622
  has_function: bool = False,
618
623
  completion: str = "",
619
624
  function_args: str = "",
@@ -637,6 +642,7 @@ class OpenAIGPT(LanguageModel):
637
642
  choices = [{}]
638
643
  event_args = ""
639
644
  event_fn_name = ""
645
+ event_tool_deltas: Optional[List[Dict[str, Any]]] = None
640
646
 
641
647
  # The first two events in the stream of Azure OpenAI is useless.
642
648
  # In the 1st: choices list is empty, in the 2nd: the dict delta has null content
@@ -648,6 +654,10 @@ class OpenAIGPT(LanguageModel):
648
654
  event_fn_name = delta["function_call"]["name"]
649
655
  if "arguments" in delta["function_call"]:
650
656
  event_args = delta["function_call"]["arguments"]
657
+ if "tool_calls" in delta and delta["tool_calls"] is not None:
658
+ # it's a list of deltas, usually just one
659
+ event_tool_deltas = delta["tool_calls"]
660
+ tool_deltas += event_tool_deltas
651
661
  else:
652
662
  event_text = choices[0]["text"]
653
663
  if event_text:
@@ -670,7 +680,31 @@ class OpenAIGPT(LanguageModel):
670
680
  sys.stdout.write(Colors().GREEN + event_args)
671
681
  sys.stdout.flush()
672
682
  self.config.streamer(event_args)
673
- if choices[0].get("finish_reason", "") in ["stop", "function_call"]:
683
+
684
+ if event_tool_deltas is not None:
685
+ # print out streaming tool calls
686
+ for td in event_tool_deltas:
687
+ if td["function"]["name"] is not None:
688
+ tool_fn_name = td["function"]["name"]
689
+ if not is_async:
690
+ sys.stdout.write(
691
+ Colors().GREEN + "OAI-TOOL: " + tool_fn_name + ": "
692
+ )
693
+ sys.stdout.flush()
694
+ self.config.streamer(tool_fn_name)
695
+ if td["function"]["arguments"] != "":
696
+ tool_fn_args = td["function"]["arguments"]
697
+ if not is_async:
698
+ sys.stdout.write(Colors().GREEN + tool_fn_args)
699
+ sys.stdout.flush()
700
+ self.config.streamer(tool_fn_args)
701
+
702
+ # show this delta in the stream
703
+ if choices[0].get("finish_reason", "") in [
704
+ "stop",
705
+ "function_call",
706
+ "tool_calls",
707
+ ]:
674
708
  # for function_call, finish_reason does not necessarily
675
709
  # contain "function_call" as mentioned in the docs.
676
710
  # So we check for "stop" or "function_call" here.
@@ -699,6 +733,7 @@ class OpenAIGPT(LanguageModel):
699
733
  sys.stdout.write(Colors().GREEN)
700
734
  sys.stdout.flush()
701
735
  has_function = False
736
+ tool_deltas: List[Dict[str, Any]] = []
702
737
  try:
703
738
  for event in response:
704
739
  (
@@ -710,6 +745,7 @@ class OpenAIGPT(LanguageModel):
710
745
  ) = self._process_stream_event(
711
746
  event,
712
747
  chat=chat,
748
+ tool_deltas=tool_deltas,
713
749
  has_function=has_function,
714
750
  completion=completion,
715
751
  function_args=function_args,
@@ -725,11 +761,11 @@ class OpenAIGPT(LanguageModel):
725
761
 
726
762
  return self._create_stream_response(
727
763
  chat=chat,
764
+ tool_deltas=tool_deltas,
728
765
  has_function=has_function,
729
766
  completion=completion,
730
767
  function_args=function_args,
731
768
  function_name=function_name,
732
- is_async=False,
733
769
  )
734
770
 
735
771
  @async_retry_with_exponential_backoff
@@ -754,6 +790,7 @@ class OpenAIGPT(LanguageModel):
754
790
  sys.stdout.write(Colors().GREEN)
755
791
  sys.stdout.flush()
756
792
  has_function = False
793
+ tool_deltas: List[Dict[str, Any]] = []
757
794
  try:
758
795
  async for event in response:
759
796
  (
@@ -765,6 +802,7 @@ class OpenAIGPT(LanguageModel):
765
802
  ) = self._process_stream_event(
766
803
  event,
767
804
  chat=chat,
805
+ tool_deltas=tool_deltas,
768
806
  has_function=has_function,
769
807
  completion=completion,
770
808
  function_args=function_args,
@@ -780,52 +818,182 @@ class OpenAIGPT(LanguageModel):
780
818
 
781
819
  return self._create_stream_response(
782
820
  chat=chat,
821
+ tool_deltas=tool_deltas,
783
822
  has_function=has_function,
784
823
  completion=completion,
785
824
  function_args=function_args,
786
825
  function_name=function_name,
787
- is_async=True,
788
826
  )
789
827
 
828
+ @staticmethod
829
+ def tool_deltas_to_tools(tools: List[Dict[str, Any]]) -> Tuple[
830
+ str,
831
+ List[OpenAIToolCall],
832
+ List[Dict[str, Any]],
833
+ ]:
834
+ """
835
+ Convert accumulated tool-call deltas to OpenAIToolCall objects.
836
+ Adapted from this excellent code:
837
+ https://community.openai.com/t/help-for-function-calls-with-streaming/627170/2
838
+
839
+ Args:
840
+ tools: list of tool deltas received from streaming API
841
+
842
+ Returns:
843
+ str: plain text corresponding to tool calls that failed to parse
844
+ List[OpenAIToolCall]: list of OpenAIToolCall objects
845
+ List[Dict[str, Any]]: list of tool dicts
846
+ (to reconstruct OpenAI API response, so it can be cached)
847
+ """
848
+ # Initialize a dictionary with default values
849
+
850
+ # idx -> dict repr of tool
851
+ # (used to simulate OpenAIResponse object later, and also to
852
+ # accumulate function args as strings)
853
+ idx2tool_dict: Dict[str, Dict[str, Any]] = defaultdict(
854
+ lambda: {
855
+ "id": None,
856
+ "function": {"arguments": "", "name": None},
857
+ "type": None,
858
+ }
859
+ )
860
+
861
+ for tool_delta in tools:
862
+ if tool_delta["id"] is not None:
863
+ idx2tool_dict[tool_delta["index"]]["id"] = tool_delta["id"]
864
+
865
+ if tool_delta["function"]["name"] is not None:
866
+ idx2tool_dict[tool_delta["index"]]["function"]["name"] = tool_delta[
867
+ "function"
868
+ ]["name"]
869
+
870
+ idx2tool_dict[tool_delta["index"]]["function"]["arguments"] += tool_delta[
871
+ "function"
872
+ ]["arguments"]
873
+
874
+ if tool_delta["type"] is not None:
875
+ idx2tool_dict[tool_delta["index"]]["type"] = tool_delta["type"]
876
+
877
+ # (try to) parse the fn args of each tool
878
+ contents: List[str] = []
879
+ good_indices = []
880
+ id2args: Dict[str, None | Dict[str, Any]] = {}
881
+ for idx, tool_dict in idx2tool_dict.items():
882
+ failed_content, args_dict = OpenAIGPT._parse_function_args(
883
+ tool_dict["function"]["arguments"]
884
+ )
885
+ # used to build tool_calls_list below
886
+ id2args[tool_dict["id"]] = args_dict or None # if {}, store as None
887
+ if failed_content != "":
888
+ contents.append(failed_content)
889
+ else:
890
+ good_indices.append(idx)
891
+
892
+ # remove the failed tool calls
893
+ idx2tool_dict = {
894
+ idx: tool_dict
895
+ for idx, tool_dict in idx2tool_dict.items()
896
+ if idx in good_indices
897
+ }
898
+
899
+ # create OpenAIToolCall list
900
+ tool_calls_list = [
901
+ OpenAIToolCall(
902
+ id=tool_dict["id"],
903
+ function=LLMFunctionCall(
904
+ name=tool_dict["function"]["name"],
905
+ arguments=id2args.get(tool_dict["id"]),
906
+ ),
907
+ type=tool_dict["type"],
908
+ )
909
+ for tool_dict in idx2tool_dict.values()
910
+ ]
911
+ return "\n".join(contents), tool_calls_list, list(idx2tool_dict.values())
912
+
913
+ @staticmethod
914
+ def _parse_function_args(args: str) -> Tuple[str, Dict[str, Any]]:
915
+ """
916
+ Try to parse the `args` string as function args.
917
+
918
+ Args:
919
+ args: string containing function args
920
+
921
+ Returns:
922
+ Tuple of content, function name and args dict.
923
+ If parsing unsuccessful, returns the original string as content,
924
+ else returns the args dict.
925
+ """
926
+ content = ""
927
+ args_dict = {}
928
+ try:
929
+ stripped_fn_args = args.strip()
930
+ dict_or_list = parse_imperfect_json(stripped_fn_args)
931
+ if not isinstance(dict_or_list, dict):
932
+ raise ValueError(
933
+ f"""
934
+ Invalid function args: {stripped_fn_args}
935
+ parsed as {dict_or_list},
936
+ which is not a valid dict.
937
+ """
938
+ )
939
+ args_dict = dict_or_list
940
+ except (SyntaxError, ValueError) as e:
941
+ logging.warning(
942
+ f"""
943
+ Parsing OpenAI function args failed: {args};
944
+ treating args as normal message. Error detail:
945
+ {e}
946
+ """
947
+ )
948
+ content = args
949
+
950
+ return content, args_dict
951
+
790
952
  def _create_stream_response(
791
953
  self,
792
954
  chat: bool = False,
955
+ tool_deltas: List[Dict[str, Any]] = [],
793
956
  has_function: bool = False,
794
957
  completion: str = "",
795
958
  function_args: str = "",
796
959
  function_name: str = "",
797
- is_async: bool = False,
798
960
  ) -> Tuple[LLMResponse, Dict[str, Any]]:
961
+ """
962
+ Create an LLMResponse object from the streaming API response.
963
+
964
+ Args:
965
+ chat: whether in chat-mode (or else completion-mode)
966
+ tool_deltas: list of tool deltas received from streaming API
967
+ has_function: whether the response contains a function_call
968
+ completion: completion text
969
+ function_args: string representing function args
970
+ function_name: name of the function
971
+ Returns:
972
+ Tuple consisting of:
973
+ LLMResponse object (with message, usage),
974
+ Dict version of OpenAIResponse object (with choices, usage)
975
+ (this is needed so we can cache the response, as if it were
976
+ a non-streaming response)
977
+ """
799
978
  # check if function_call args are valid, if not,
800
979
  # treat this as a normal msg, not a function call
801
- args = {}
980
+ args: Dict[str, Any] = {}
802
981
  if has_function and function_args != "":
803
- try:
804
- stripped_fn_args = function_args.strip()
805
- dict_or_list = parse_imperfect_json(stripped_fn_args)
806
- if not isinstance(dict_or_list, dict):
807
- raise ValueError(
808
- f"""
809
- Invalid function args: {stripped_fn_args}
810
- parsed as {dict_or_list},
811
- which is not a valid dict.
812
- """
813
- )
814
- args = dict_or_list
815
- except (SyntaxError, ValueError) as e:
816
- logging.warning(
817
- f"""
818
- Parsing OpenAI function args failed: {function_args};
819
- treating args as normal message. Error detail:
820
- {e}
821
- """
822
- )
982
+ content, args = self._parse_function_args(function_args)
983
+ completion = completion + content
984
+ if content != "":
823
985
  has_function = False
824
- completion = completion + function_args
825
986
 
826
987
  # mock openai response so we can cache it
827
988
  if chat:
989
+ failed_content, tool_calls, tool_dicts = OpenAIGPT.tool_deltas_to_tools(
990
+ tool_deltas,
991
+ )
992
+ completion = completion + "\n" + failed_content
828
993
  msg: Dict[str, Any] = dict(message=dict(content=completion))
994
+ if len(tool_dicts) > 0:
995
+ msg["message"]["tool_calls"] = tool_dicts
996
+
829
997
  if has_function:
830
998
  function_call = LLMFunctionCall(name=function_name)
831
999
  function_call_dict = function_call.dict()
@@ -839,6 +1007,8 @@ class OpenAIGPT(LanguageModel):
839
1007
  # non-chat mode has no function_call
840
1008
  msg = dict(text=completion)
841
1009
 
1010
+ # create an OpenAIResponse object so we can cache it as if it were
1011
+ # a non-streaming response
842
1012
  openai_response = OpenAIResponse(
843
1013
  choices=[msg],
844
1014
  usage=dict(total_tokens=0),
@@ -847,6 +1017,7 @@ class OpenAIGPT(LanguageModel):
847
1017
  LLMResponse(
848
1018
  message=completion,
849
1019
  cached=False,
1020
+ oai_tool_calls=tool_calls or None, # don't allow empty list [] here
850
1021
  function_call=function_call if has_function else None,
851
1022
  ),
852
1023
  openai_response.dict(),
@@ -1061,16 +1232,19 @@ class OpenAIGPT(LanguageModel):
1061
1232
  self,
1062
1233
  messages: Union[str, List[LLMMessage]],
1063
1234
  max_tokens: int = 200,
1235
+ tools: Optional[List[OpenAIToolSpec]] = None,
1236
+ tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
1064
1237
  functions: Optional[List[LLMFunctionSpec]] = None,
1065
1238
  function_call: str | Dict[str, str] = "auto",
1066
1239
  ) -> LLMResponse:
1067
1240
  self.run_on_first_use()
1068
1241
 
1069
- if functions is not None and not self.is_openai_chat_model():
1242
+ if [functions, tools] != [None, None] and not self.is_openai_chat_model():
1070
1243
  raise ValueError(
1071
1244
  f"""
1072
- `functions` can only be specified for OpenAI chat models;
1073
- {self.config.chat_model} does not support function-calling.
1245
+ `functions` and `tools` can only be specified for OpenAI chat LLMs,
1246
+ or LLMs served via an OpenAI-compatible API.
1247
+ {self.config.chat_model} does not support function-calling or tools.
1074
1248
  Instead, please use Langroid's ToolMessages, which are equivalent.
1075
1249
  In the ChatAgentConfig, set `use_functions_api=False`
1076
1250
  and `use_tools=True`, this will enable ToolMessages.
@@ -1094,7 +1268,9 @@ class OpenAIGPT(LanguageModel):
1094
1268
  prompt = self.config.hf_formatter.format(messages)
1095
1269
  return self.generate(prompt=prompt, max_tokens=max_tokens)
1096
1270
  try:
1097
- return self._chat(messages, max_tokens, functions, function_call)
1271
+ return self._chat(
1272
+ messages, max_tokens, tools, tool_choice, functions, function_call
1273
+ )
1098
1274
  except Exception as e:
1099
1275
  # log and re-raise exception
1100
1276
  logging.error(friendly_error(e, "Error in OpenAIGPT.chat: "))
@@ -1104,16 +1280,18 @@ class OpenAIGPT(LanguageModel):
1104
1280
  self,
1105
1281
  messages: Union[str, List[LLMMessage]],
1106
1282
  max_tokens: int = 200,
1283
+ tools: Optional[List[OpenAIToolSpec]] = None,
1284
+ tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
1107
1285
  functions: Optional[List[LLMFunctionSpec]] = None,
1108
1286
  function_call: str | Dict[str, str] = "auto",
1109
1287
  ) -> LLMResponse:
1110
1288
  self.run_on_first_use()
1111
1289
 
1112
- if functions is not None and not self.is_openai_chat_model():
1290
+ if [functions, tools] != [None, None] and not self.is_openai_chat_model():
1113
1291
  raise ValueError(
1114
1292
  f"""
1115
- `functions` can only be specified for OpenAI chat models;
1116
- {self.config.chat_model} does not support function-calling.
1293
+ `functions` and `tools` can only be specified for OpenAI chat models;
1294
+ {self.config.chat_model} does not support function-calling or tools.
1117
1295
  Instead, please use Langroid's ToolMessages, which are equivalent.
1118
1296
  In the ChatAgentConfig, set `use_functions_api=False`
1119
1297
  and `use_tools=True`, this will enable ToolMessages.
@@ -1146,7 +1324,14 @@ class OpenAIGPT(LanguageModel):
1146
1324
  prompt = formatter.format(messages)
1147
1325
  return await self.agenerate(prompt=prompt, max_tokens=max_tokens)
1148
1326
  try:
1149
- result = await self._achat(messages, max_tokens, functions, function_call)
1327
+ result = await self._achat(
1328
+ messages,
1329
+ max_tokens,
1330
+ tools,
1331
+ tool_choice,
1332
+ functions,
1333
+ function_call,
1334
+ )
1150
1335
  return result
1151
1336
  except Exception as e:
1152
1337
  # log and re-raise exception
@@ -1209,9 +1394,12 @@ class OpenAIGPT(LanguageModel):
1209
1394
  self,
1210
1395
  messages: Union[str, List[LLMMessage]],
1211
1396
  max_tokens: int,
1397
+ tools: Optional[List[OpenAIToolSpec]] = None,
1398
+ tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
1212
1399
  functions: Optional[List[LLMFunctionSpec]] = None,
1213
1400
  function_call: str | Dict[str, str] = "auto",
1214
1401
  ) -> Dict[str, Any]:
1402
+ """Prepare args for LLM chat-completion API call"""
1215
1403
  if isinstance(messages, str):
1216
1404
  llm_messages = [
1217
1405
  LLMMessage(role=Role.SYSTEM, content="You are a helpful assistant."),
@@ -1243,6 +1431,19 @@ class OpenAIGPT(LanguageModel):
1243
1431
  function_call=function_call,
1244
1432
  )
1245
1433
  )
1434
+ if tools is not None:
1435
+ args.update(
1436
+ dict(
1437
+ tools=[
1438
+ dict(
1439
+ type="function",
1440
+ function=t.function.dict(),
1441
+ )
1442
+ for t in tools
1443
+ ],
1444
+ tool_choice=tool_choice,
1445
+ )
1446
+ )
1246
1447
  return args
1247
1448
 
1248
1449
  def _process_chat_completion_response(
@@ -1281,6 +1482,7 @@ class OpenAIGPT(LanguageModel):
1281
1482
  """
1282
1483
  message = response["choices"][0]["message"]
1283
1484
  msg = message["content"] or ""
1485
+
1284
1486
  if message.get("function_call") is None:
1285
1487
  fun_call = None
1286
1488
  else:
@@ -1297,10 +1499,24 @@ class OpenAIGPT(LanguageModel):
1297
1499
  args_str = message["function_call"]["arguments"] or ""
1298
1500
  msg_str = message["content"] or ""
1299
1501
  msg = msg_str + args_str
1300
-
1502
+ oai_tool_calls = None
1503
+ if message.get("tool_calls") is not None:
1504
+ oai_tool_calls = []
1505
+ for tool_call_dict in message["tool_calls"]:
1506
+ try:
1507
+ tool_call = OpenAIToolCall.from_dict(tool_call_dict)
1508
+ oai_tool_calls.append(tool_call)
1509
+ except (ValueError, SyntaxError):
1510
+ logging.warning(
1511
+ "Could not parse tool call: "
1512
+ f"{json.dumps(tool_call_dict)} "
1513
+ "treating as normal non-tool message"
1514
+ )
1515
+ msg = msg + "\n" + json.dumps(tool_call_dict)
1301
1516
  return LLMResponse(
1302
1517
  message=msg.strip() if msg is not None else "",
1303
1518
  function_call=fun_call,
1519
+ oai_tool_calls=oai_tool_calls or None, # don't allow empty list [] here
1304
1520
  cached=cached,
1305
1521
  usage=self._get_non_stream_token_usage(cached, response),
1306
1522
  )
@@ -1309,6 +1525,8 @@ class OpenAIGPT(LanguageModel):
1309
1525
  self,
1310
1526
  messages: Union[str, List[LLMMessage]],
1311
1527
  max_tokens: int,
1528
+ tools: Optional[List[OpenAIToolSpec]] = None,
1529
+ tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
1312
1530
  functions: Optional[List[LLMFunctionSpec]] = None,
1313
1531
  function_call: str | Dict[str, str] = "auto",
1314
1532
  ) -> LLMResponse:
@@ -1333,6 +1551,8 @@ class OpenAIGPT(LanguageModel):
1333
1551
  args = self._prep_chat_completion(
1334
1552
  messages,
1335
1553
  max_tokens,
1554
+ tools,
1555
+ tool_choice,
1336
1556
  functions,
1337
1557
  function_call,
1338
1558
  )
@@ -1351,6 +1571,8 @@ class OpenAIGPT(LanguageModel):
1351
1571
  self,
1352
1572
  messages: Union[str, List[LLMMessage]],
1353
1573
  max_tokens: int,
1574
+ tools: Optional[List[OpenAIToolSpec]] = None,
1575
+ tool_choice: ToolChoiceTypes | Dict[str, str | Dict[str, str]] = "auto",
1354
1576
  functions: Optional[List[LLMFunctionSpec]] = None,
1355
1577
  function_call: str | Dict[str, str] = "auto",
1356
1578
  ) -> LLMResponse:
@@ -1360,6 +1582,8 @@ class OpenAIGPT(LanguageModel):
1360
1582
  args = self._prep_chat_completion(
1361
1583
  messages,
1362
1584
  max_tokens,
1585
+ tools,
1586
+ tool_choice,
1363
1587
  functions,
1364
1588
  function_call,
1365
1589
  )
langroid/mytypes.py CHANGED
@@ -76,7 +76,6 @@ class Document(BaseModel):
76
76
  return self.metadata.id
77
77
 
78
78
  def __str__(self) -> str:
79
- # TODO: make metadata a pydantic model to enforce "source"
80
79
  return dedent(
81
80
  f"""
82
81
  CONTENT: {self.content}
@@ -89,11 +89,28 @@ def parse_imperfect_json(json_string: str) -> Union[Dict[str, Any], List[Any]]:
89
89
 
90
90
  # If ast.literal_eval fails or returns non-dict/list, try json.loads
91
91
  try:
92
- str = add_quotes(json_string)
93
- result = json.loads(str)
92
+ json_string = add_quotes(json_string)
93
+ result = json.loads(json_string)
94
94
  if isinstance(result, (dict, list)):
95
95
  return result
96
96
  except json.JSONDecodeError:
97
+ try:
98
+ # fallback on yaml
99
+ yaml_result = yaml.safe_load(json_string)
100
+ if isinstance(yaml_result, (dict, list)):
101
+ return yaml_result
102
+ except yaml.YAMLError:
103
+ pass
104
+
105
+ try:
106
+ # last resort: try to repair the json using a lib
107
+ from json_repair import repair_json
108
+
109
+ repaired_json = repair_json(json_string)
110
+ result = json.loads(repaired_json)
111
+ if isinstance(result, (dict, list)):
112
+ return result
113
+ except Exception:
97
114
  pass
98
115
 
99
116
  # If all methods fail, raise ValueError
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from collections.abc import MutableMapping
2
3
  from contextlib import contextmanager
3
4
  from typing import (
4
5
  Any,
@@ -21,6 +22,24 @@ from langroid.pydantic_v1 import BaseModel, ValidationError, create_model
21
22
  logger = logging.getLogger(__name__)
22
23
 
23
24
 
25
+ def flatten_dict(
26
+ d: MutableMapping[str, Any], parent_key: str = "", sep: str = "."
27
+ ) -> Dict[str, Any]:
28
+ """Flatten a nested dictionary, using a separator in the keys.
29
+ Useful for pydantic_v1 models with nested fields -- first use
30
+ dct = mdl.model_dump()
31
+ to get a nested dictionary, then use this function to flatten it.
32
+ """
33
+ items: List[Tuple[str, Any]] = []
34
+ for k, v in d.items():
35
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
36
+ if isinstance(v, MutableMapping):
37
+ items.extend(flatten_dict(v, new_key, sep=sep).items())
38
+ else:
39
+ items.append((new_key, v))
40
+ return dict(items)
41
+
42
+
24
43
  def has_field(model_class: Type[BaseModel], field_name: str) -> bool:
25
44
  """Check if a Pydantic model class has a field with the given name."""
26
45
  return field_name in model_class.__fields__
@@ -15,6 +15,7 @@ from langroid.utils.configuration import settings
15
15
  from langroid.utils.object_registry import ObjectRegistry
16
16
  from langroid.utils.output.printing import print_long_text
17
17
  from langroid.utils.pandas_utils import stringify
18
+ from langroid.utils.pydantic_utils import flatten_dict
18
19
 
19
20
  logger = logging.getLogger(__name__)
20
21
 
@@ -136,7 +137,8 @@ class VectorStore(ABC):
136
137
  """Compute a result on a set of documents,
137
138
  using a dataframe calc string like `df.groupby('state')['income'].mean()`.
138
139
  """
139
- dicts = [doc.dict() for doc in docs]
140
+ # convert each doc to a dict, using dotted paths for nested fields
141
+ dicts = [flatten_dict(doc.dict(by_alias=True)) for doc in docs]
140
142
  df = pd.DataFrame(dicts)
141
143
 
142
144
  try:
@@ -147,6 +147,8 @@ class LanceDB(VectorStore):
147
147
 
148
148
  def _create_lance_schema(self, doc_cls: Type[Document]) -> Type[BaseModel]:
149
149
  """
150
+ NOTE: NOT USED, but leaving it here as it may be useful.
151
+
150
152
  Create a subclass of LanceModel with fields:
151
153
  - id (str)
152
154
  - Vector field that has dims equal to
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langroid
3
- Version: 0.6.7
3
+ Version: 0.9.0
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  License: MIT
6
6
  Author: Prasad Chalasani
@@ -54,6 +54,7 @@ Requires-Dist: grpcio (>=1.62.1,<2.0.0)
54
54
  Requires-Dist: halo (>=0.0.31,<0.0.32)
55
55
  Requires-Dist: huggingface-hub (>=0.21.2,<0.22.0) ; extra == "hf-transformers" or extra == "all" or extra == "transformers"
56
56
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
57
+ Requires-Dist: json-repair (>=0.27.0,<0.28.0)
57
58
  Requires-Dist: lancedb (>=0.8.2,<0.9.0) ; extra == "vecdbs" or extra == "lancedb"
58
59
  Requires-Dist: litellm (>=1.30.1,<2.0.0) ; extra == "all" or extra == "litellm"
59
60
  Requires-Dist: lxml (>=4.9.3,<5.0.0)
@@ -235,6 +236,8 @@ teacher_task.run()
235
236
  <details>
236
237
  <summary> <b>Click to expand</b></summary>
237
238
 
239
+ - **Aug 2024:**
240
+ - **[0.7.0](https://github.com/langroid/langroid/releases/tag/0.7.0)** OpenAI tools API support.
238
241
  - **Jul 2024:**
239
242
  - **[0.3.0](https://github.com/langroid/langroid/releases/tag/0.3.0)**: Added [FastEmbed](https://qdrant.github.io/fastembed/qdrant/Usage_With_Qdrant/) embeddings from Qdrant
240
243
  - **Jun 2024:**