langroid 0.50.2__py3-none-any.whl → 0.50.3__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.
langroid/agent/base.py CHANGED
@@ -1929,10 +1929,13 @@ class Agent(ABC):
1929
1929
  print_response_stats: bool = True,
1930
1930
  ) -> None:
1931
1931
  """
1932
- Updates `response.usage` obj (token usage and cost fields).the usage memebr
1933
- It updates the cost after checking the cache and updates the
1934
- tokens (prompts and completion) if the response stream is True, because OpenAI
1935
- doesn't returns these fields.
1932
+ Updates `response.usage` obj (token usage and cost fields) if needed.
1933
+ An update is needed only if:
1934
+ - stream is True (i.e. streaming was enabled), and
1935
+ - the response was NOT obtained from cached, and
1936
+ - the API did NOT provide the usage/cost fields during streaming
1937
+ (As of Sep 2024, the OpenAI API started providing these; for other APIs
1938
+ this may not necessarily be the case).
1936
1939
 
1937
1940
  Args:
1938
1941
  response (LLMResponse): LLMResponse object
@@ -1945,10 +1948,11 @@ class Agent(ABC):
1945
1948
  if response is None or self.llm is None:
1946
1949
  return
1947
1950
 
1951
+ no_usage_info = response.usage is None or response.usage.prompt_tokens == 0
1948
1952
  # Note: If response was not streamed, then
1949
1953
  # `response.usage` would already have been set by the API,
1950
1954
  # so we only need to update in the stream case.
1951
- if stream:
1955
+ if stream and no_usage_info:
1952
1956
  # usage, cost = 0 when response is from cache
1953
1957
  prompt_tokens = 0
1954
1958
  completion_tokens = 0
File without changes
@@ -216,7 +216,7 @@ class LLMTokenUsage(BaseModel):
216
216
  prompt_tokens: int = 0
217
217
  completion_tokens: int = 0
218
218
  cost: float = 0.0
219
- calls: int = 0 # how many API calls
219
+ calls: int = 0 # how many API calls - not used as of 2025-04-04
220
220
 
221
221
  def reset(self) -> None:
222
222
  self.prompt_tokens = 0
@@ -780,22 +780,39 @@ class OpenAIGPT(LanguageModel):
780
780
  reasoning: str = "",
781
781
  function_args: str = "",
782
782
  function_name: str = "",
783
- ) -> Tuple[bool, bool, str, str]:
783
+ ) -> Tuple[bool, bool, str, str, Dict[str, int]]:
784
784
  """Process state vars while processing a streaming API response.
785
785
  Returns a tuple consisting of:
786
786
  - is_break: whether to break out of the loop
787
787
  - has_function: whether the response contains a function_call
788
788
  - function_name: name of the function
789
789
  - function_args: args of the function
790
+ - completion: completion text
791
+ - reasoning: reasoning text
792
+ - usage: usage dict
790
793
  """
791
794
  # convert event obj (of type ChatCompletionChunk) to dict so rest of code,
792
795
  # which expects dicts, works as it did before switching to openai v1.x
793
796
  if not isinstance(event, dict):
794
797
  event = event.model_dump()
795
798
 
799
+ usage = event.get("usage", {}) or {}
796
800
  choices = event.get("choices", [{}])
797
- if len(choices) == 0:
801
+ if choices is None or len(choices) == 0:
798
802
  choices = [{}]
803
+ if len(usage) > 0 and len(choices[0]) == 0:
804
+ # we have a "usage" chunk, and empty choices, so we're done
805
+ # ASSUMPTION: a usage chunk ONLY arrives AFTER all normal completion text!
806
+ # If any API does not follow this, we need to change this code.
807
+ return (
808
+ True,
809
+ has_function,
810
+ function_name,
811
+ function_args,
812
+ completion,
813
+ reasoning,
814
+ usage,
815
+ )
799
816
  event_args = ""
800
817
  event_fn_name = ""
801
818
  event_tool_deltas: Optional[List[Dict[str, Any]]] = None
@@ -876,23 +893,23 @@ class OpenAIGPT(LanguageModel):
876
893
  self.config.streamer(tool_fn_args, StreamEventType.TOOL_ARGS)
877
894
 
878
895
  # show this delta in the stream
879
- if finish_reason in [
896
+ is_break = finish_reason in [
880
897
  "stop",
881
898
  "function_call",
882
899
  "tool_calls",
883
- ]:
884
- # for function_call, finish_reason does not necessarily
885
- # contain "function_call" as mentioned in the docs.
886
- # So we check for "stop" or "function_call" here.
887
- return (
888
- True,
889
- has_function,
890
- function_name,
891
- function_args,
892
- completion,
893
- reasoning,
894
- )
895
- return False, has_function, function_name, function_args, completion, reasoning
900
+ ]
901
+ # for function_call, finish_reason does not necessarily
902
+ # contain "function_call" as mentioned in the docs.
903
+ # So we check for "stop" or "function_call" here.
904
+ return (
905
+ is_break,
906
+ has_function,
907
+ function_name,
908
+ function_args,
909
+ completion,
910
+ reasoning,
911
+ usage,
912
+ )
896
913
 
897
914
  @no_type_check
898
915
  async def _process_stream_event_async(
@@ -912,15 +929,30 @@ class OpenAIGPT(LanguageModel):
912
929
  - has_function: whether the response contains a function_call
913
930
  - function_name: name of the function
914
931
  - function_args: args of the function
932
+ - completion: completion text
933
+ - reasoning: reasoning text
934
+ - usage: usage dict
915
935
  """
916
936
  # convert event obj (of type ChatCompletionChunk) to dict so rest of code,
917
937
  # which expects dicts, works as it did before switching to openai v1.x
918
938
  if not isinstance(event, dict):
919
939
  event = event.model_dump()
920
940
 
941
+ usage = event.get("usage", {}) or {}
921
942
  choices = event.get("choices", [{}])
922
943
  if len(choices) == 0:
923
944
  choices = [{}]
945
+ if len(usage) > 0 and len(choices[0]) == 0:
946
+ # we got usage chunk, and empty choices, so we're done
947
+ return (
948
+ True,
949
+ has_function,
950
+ function_name,
951
+ function_args,
952
+ completion,
953
+ reasoning,
954
+ usage,
955
+ )
924
956
  event_args = ""
925
957
  event_fn_name = ""
926
958
  event_tool_deltas: Optional[List[Dict[str, Any]]] = None
@@ -996,23 +1028,23 @@ class OpenAIGPT(LanguageModel):
996
1028
  )
997
1029
 
998
1030
  # show this delta in the stream
999
- if choices[0].get("finish_reason", "") in [
1031
+ is_break = choices[0].get("finish_reason", "") in [
1000
1032
  "stop",
1001
1033
  "function_call",
1002
1034
  "tool_calls",
1003
- ]:
1004
- # for function_call, finish_reason does not necessarily
1005
- # contain "function_call" as mentioned in the docs.
1006
- # So we check for "stop" or "function_call" here.
1007
- return (
1008
- True,
1009
- has_function,
1010
- function_name,
1011
- function_args,
1012
- completion,
1013
- reasoning,
1014
- )
1015
- return False, has_function, function_name, function_args, completion, reasoning
1035
+ ]
1036
+ # for function_call, finish_reason does not necessarily
1037
+ # contain "function_call" as mentioned in the docs.
1038
+ # So we check for "stop" or "function_call" here.
1039
+ return (
1040
+ is_break,
1041
+ has_function,
1042
+ function_name,
1043
+ function_args,
1044
+ completion,
1045
+ reasoning,
1046
+ usage,
1047
+ )
1016
1048
 
1017
1049
  @retry_with_exponential_backoff
1018
1050
  def _stream_response( # type: ignore
@@ -1038,6 +1070,8 @@ class OpenAIGPT(LanguageModel):
1038
1070
  sys.stdout.flush()
1039
1071
  has_function = False
1040
1072
  tool_deltas: List[Dict[str, Any]] = []
1073
+ token_usage: Dict[str, int] = {}
1074
+ done: bool = False
1041
1075
  try:
1042
1076
  for event in response:
1043
1077
  (
@@ -1047,6 +1081,7 @@ class OpenAIGPT(LanguageModel):
1047
1081
  function_args,
1048
1082
  completion,
1049
1083
  reasoning,
1084
+ usage,
1050
1085
  ) = self._process_stream_event(
1051
1086
  event,
1052
1087
  chat=chat,
@@ -1057,8 +1092,17 @@ class OpenAIGPT(LanguageModel):
1057
1092
  function_args=function_args,
1058
1093
  function_name=function_name,
1059
1094
  )
1095
+ if len(usage) > 0:
1096
+ # capture the token usage when non-empty
1097
+ token_usage = usage
1060
1098
  if is_break:
1061
- break
1099
+ if not self.get_stream() or done:
1100
+ # if not streaming, then we don't wait for last "usage" chunk
1101
+ break
1102
+ else:
1103
+ # mark done, so we quit after the last "usage" chunk
1104
+ done = True
1105
+
1062
1106
  except Exception as e:
1063
1107
  logging.warning("Error while processing stream response: %s", str(e))
1064
1108
 
@@ -1073,6 +1117,7 @@ class OpenAIGPT(LanguageModel):
1073
1117
  reasoning=reasoning,
1074
1118
  function_args=function_args,
1075
1119
  function_name=function_name,
1120
+ usage=token_usage,
1076
1121
  )
1077
1122
 
1078
1123
  @async_retry_with_exponential_backoff
@@ -1100,6 +1145,8 @@ class OpenAIGPT(LanguageModel):
1100
1145
  sys.stdout.flush()
1101
1146
  has_function = False
1102
1147
  tool_deltas: List[Dict[str, Any]] = []
1148
+ token_usage: Dict[str, int] = {}
1149
+ done: bool = False
1103
1150
  try:
1104
1151
  async for event in response:
1105
1152
  (
@@ -1109,6 +1156,7 @@ class OpenAIGPT(LanguageModel):
1109
1156
  function_args,
1110
1157
  completion,
1111
1158
  reasoning,
1159
+ usage,
1112
1160
  ) = await self._process_stream_event_async(
1113
1161
  event,
1114
1162
  chat=chat,
@@ -1119,8 +1167,17 @@ class OpenAIGPT(LanguageModel):
1119
1167
  function_args=function_args,
1120
1168
  function_name=function_name,
1121
1169
  )
1170
+ if len(usage) > 0:
1171
+ # capture the token usage when non-empty
1172
+ token_usage = usage
1122
1173
  if is_break:
1123
- break
1174
+ if not self.get_stream() or done:
1175
+ # if not streaming, then we don't wait for last "usage" chunk
1176
+ break
1177
+ else:
1178
+ # mark done, so we quit after the next "usage" chunk
1179
+ done = True
1180
+
1124
1181
  except Exception as e:
1125
1182
  logging.warning("Error while processing stream response: %s", str(e))
1126
1183
 
@@ -1135,6 +1192,7 @@ class OpenAIGPT(LanguageModel):
1135
1192
  reasoning=reasoning,
1136
1193
  function_args=function_args,
1137
1194
  function_name=function_name,
1195
+ usage=token_usage,
1138
1196
  )
1139
1197
 
1140
1198
  @staticmethod
@@ -1272,6 +1330,7 @@ class OpenAIGPT(LanguageModel):
1272
1330
  reasoning: str = "",
1273
1331
  function_args: str = "",
1274
1332
  function_name: str = "",
1333
+ usage: Dict[str, int] = {},
1275
1334
  ) -> Tuple[LLMResponse, Dict[str, Any]]:
1276
1335
  """
1277
1336
  Create an LLMResponse object from the streaming API response.
@@ -1281,8 +1340,10 @@ class OpenAIGPT(LanguageModel):
1281
1340
  tool_deltas: list of tool deltas received from streaming API
1282
1341
  has_function: whether the response contains a function_call
1283
1342
  completion: completion text
1343
+ reasoning: reasoning text
1284
1344
  function_args: string representing function args
1285
1345
  function_name: name of the function
1346
+ usage: token usage dict
1286
1347
  Returns:
1287
1348
  Tuple consisting of:
1288
1349
  LLMResponse object (with message, usage),
@@ -1347,6 +1408,14 @@ class OpenAIGPT(LanguageModel):
1347
1408
  # don't allow empty list [] here
1348
1409
  oai_tool_calls=tool_calls or None if len(tool_deltas) > 0 else None,
1349
1410
  function_call=function_call if has_function else None,
1411
+ usage=LLMTokenUsage(
1412
+ prompt_tokens=usage.get("prompt_tokens", 0),
1413
+ completion_tokens=usage.get("completion_tokens", 0),
1414
+ cost=self._cost_chat_model(
1415
+ usage.get("prompt_tokens", 0),
1416
+ usage.get("completion_tokens", 0),
1417
+ ),
1418
+ ),
1350
1419
  ),
1351
1420
  openai_response.dict(),
1352
1421
  )
@@ -1833,6 +1902,14 @@ class OpenAIGPT(LanguageModel):
1833
1902
  max_tokens=max_tokens,
1834
1903
  stream=self.get_stream(),
1835
1904
  )
1905
+ if self.get_stream():
1906
+ args.update(
1907
+ dict(
1908
+ # get token-usage numbers in stream mode from OpenAI API,
1909
+ # and possibly other OpenAI-compatible APIs.
1910
+ stream_options=dict(include_usage=True),
1911
+ )
1912
+ )
1836
1913
  args.update(self._openai_api_call_params(args))
1837
1914
  # only include functions-related args if functions are provided
1838
1915
  # since the OpenAI API will throw an error if `functions` is None or []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langroid
3
- Version: 0.50.2
3
+ Version: 0.50.3
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  Author-email: Prasad Chalasani <pchalasani@gmail.com>
6
6
  License: MIT
@@ -3,7 +3,7 @@ langroid/exceptions.py,sha256=OPjece_8cwg94DLPcOGA1ddzy5bGh65pxzcHMnssTz8,2995
3
3
  langroid/mytypes.py,sha256=HIcYAqGeA9OK0Hlscym2FI5Oax9QFljDZoVgRlomhRk,4014
4
4
  langroid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  langroid/agent/__init__.py,sha256=ll0Cubd2DZ-fsCMl7e10hf9ZjFGKzphfBco396IKITY,786
6
- langroid/agent/base.py,sha256=U-UjdpxIFqkzRIB5-LYwHrhMSNI3sDbfnNRqIhrtsyI,79568
6
+ langroid/agent/base.py,sha256=bs5OLCf534mhsdR7Rgf27GqVNuSV2bOVbD46Y86mGFA,79829
7
7
  langroid/agent/batch.py,sha256=vi1r5i1-vN80WfqHDSwjEym_KfGsqPGUtwktmiK1nuk,20635
8
8
  langroid/agent/chat_agent.py,sha256=Z53oleOUcOXVs_UL90spttGoAooe0mrx3tDtOuhKVms,85214
9
9
  langroid/agent/chat_document.py,sha256=xzMtrPbaW-Y-BnF7kuhr2dorsD-D5rMWzfOqJ8HAoo8,17885
@@ -15,6 +15,7 @@ langroid/agent/callbacks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
15
15
  langroid/agent/callbacks/chainlit.py,sha256=UHB6P_J40vsVnssosqkpkOVWRf9NK4TOY0_G2g_Arsg,20900
16
16
  langroid/agent/special/__init__.py,sha256=gik_Xtm_zV7U9s30Mn8UX3Gyuy4jTjQe9zjiE3HWmEo,1273
17
17
  langroid/agent/special/doc_chat_agent.py,sha256=dOL9Y0xAslkwepCdKU8Dc1m5Vk8qgk-gLbU4JzsmTII,65234
18
+ langroid/agent/special/doc_chat_task.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
19
  langroid/agent/special/lance_doc_chat_agent.py,sha256=s8xoRs0gGaFtDYFUSIRchsgDVbS5Q3C2b2mr3V1Fd-Q,10419
19
20
  langroid/agent/special/lance_tools.py,sha256=qS8x4wi8mrqfbYV2ztFzrcxyhHQ0ZWOc-zkYiH7awj0,2105
20
21
  langroid/agent/special/relevance_extractor_agent.py,sha256=zIx8GUdVo1aGW6ASla0NPQjYYIpmriK_TYMijqAx3F8,4796
@@ -68,11 +69,11 @@ langroid/embedding_models/protoc/embeddings_pb2.pyi,sha256=UkNy7BrNsmQm0vLb3NtGX
68
69
  langroid/embedding_models/protoc/embeddings_pb2_grpc.py,sha256=9dYQqkW3JPyBpSEjeGXTNpSqAkC-6FPtBHyteVob2Y8,2452
69
70
  langroid/language_models/__init__.py,sha256=3aD2qC1lz8v12HX4B-dilv27gNxYdGdeu1QvDlkqqHs,1095
70
71
  langroid/language_models/azure_openai.py,sha256=SW0Fp_y6HpERr9l6TtF6CYsKgKwjUf_hSL_2mhTV4wI,5034
71
- langroid/language_models/base.py,sha256=mDYmFCBCLdq8_Uvws4MiewwEgcOCP8Qb0e5yUXr3zpQ,26249
72
+ langroid/language_models/base.py,sha256=aCEHqmxNM2CD5mt3SyMi7Mf8R4IjkyFwGX-IAUqjxmM,26277
72
73
  langroid/language_models/config.py,sha256=9Q8wk5a7RQr8LGMT_0WkpjY8S4ywK06SalVRjXlfCiI,378
73
74
  langroid/language_models/mock_lm.py,sha256=5BgHKDVRWFbUwDT_PFgTZXz9-k8wJSA2e3PZmyDgQ1k,4022
74
75
  langroid/language_models/model_info.py,sha256=tfBBxL0iUf2mVN6CjcvqflzFUVg2oZqOJZexZ8jHTYA,12216
75
- langroid/language_models/openai_gpt.py,sha256=M_jp97Ksp5r3U-d0jCLPLjVmn7IK1mC8Ry4t7k6A5tc,82906
76
+ langroid/language_models/openai_gpt.py,sha256=yNfiWxhH5BxA_mKiw69D3L4Bu__agI6WVg80IF3P5UI,85785
76
77
  langroid/language_models/utils.py,sha256=L4_CbihDMTGcsg0TOG1Yd5JFEto46--h7CX_14m89sQ,5016
77
78
  langroid/language_models/prompt_formatter/__init__.py,sha256=2-5cdE24XoFDhifOLl8yiscohil1ogbP1ECkYdBlBsk,372
78
79
  langroid/language_models/prompt_formatter/base.py,sha256=eDS1sgRNZVnoajwV_ZIha6cba5Dt8xjgzdRbPITwx3Q,1221
@@ -128,7 +129,7 @@ langroid/vector_store/pineconedb.py,sha256=otxXZNaBKb9f_H75HTaU3lMHiaR2NUp5MqwLZ
128
129
  langroid/vector_store/postgres.py,sha256=wHPtIi2qM4fhO4pMQr95pz1ZCe7dTb2hxl4VYspGZoA,16104
129
130
  langroid/vector_store/qdrantdb.py,sha256=O6dSBoDZ0jzfeVBd7LLvsXu083xs2fxXtPa9gGX3JX4,18443
130
131
  langroid/vector_store/weaviatedb.py,sha256=Yn8pg139gOy3zkaPfoTbMXEEBCiLiYa1MU5d_3UA1K4,11847
131
- langroid-0.50.2.dist-info/METADATA,sha256=ttDoi8hgjYIzOjj_bHVN8IMtJcN574iXw4ZN4_q1VUQ,63641
132
- langroid-0.50.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
133
- langroid-0.50.2.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
134
- langroid-0.50.2.dist-info/RECORD,,
132
+ langroid-0.50.3.dist-info/METADATA,sha256=5c4f7md0dqoJqMQuCBZwh3HBvpUS-_rz1liE3LeoPKM,63641
133
+ langroid-0.50.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
134
+ langroid-0.50.3.dist-info/licenses/LICENSE,sha256=EgVbvA6VSYgUlvC3RvPKehSg7MFaxWDsFuzLOsPPfJg,1065
135
+ langroid-0.50.3.dist-info/RECORD,,