langroid 0.1.52__tar.gz → 0.1.54__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 (87) hide show
  1. {langroid-0.1.52 → langroid-0.1.54}/PKG-INFO +23 -2
  2. {langroid-0.1.52 → langroid-0.1.54}/README.md +22 -1
  3. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/base.py +78 -5
  4. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/chat_agent.py +2 -1
  5. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/chat_document.py +8 -3
  6. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/doc_chat_agent.py +11 -11
  7. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/task.py +24 -20
  8. langroid-0.1.54/langroid/io/refs.md +1 -0
  9. langroid-0.1.54/langroid/language_models/azure_openai.py +72 -0
  10. {langroid-0.1.52 → langroid-0.1.54}/langroid/language_models/base.py +35 -4
  11. {langroid-0.1.52 → langroid-0.1.54}/langroid/language_models/openai_gpt.py +49 -16
  12. {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/templates.py +5 -4
  13. langroid-0.1.54/langroid/utils/output/__init__.py +0 -0
  14. langroid-0.1.54/langroid/utils/web/__init__.py +0 -0
  15. langroid-0.1.54/langroid/vector_store/__init__.py +0 -0
  16. {langroid-0.1.52 → langroid-0.1.54}/pyproject.toml +1 -1
  17. langroid-0.1.54/setup.py +98 -0
  18. langroid-0.1.52/setup.py +0 -97
  19. {langroid-0.1.52 → langroid-0.1.54}/LICENSE +0 -0
  20. {langroid-0.1.52 → langroid-0.1.54}/langroid/__init__.py +0 -0
  21. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/__init__.py +0 -0
  22. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/helpers.py +0 -0
  23. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/junk +0 -0
  24. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/__init__.py +0 -0
  25. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/recipient_validator_agent.py +0 -0
  26. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/retriever_agent.py +0 -0
  27. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/__init__.py +0 -0
  28. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/sql_chat_agent.py +0 -0
  29. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/utils/__init__.py +0 -0
  30. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/utils/description_extractors.py +0 -0
  31. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/table_chat_agent.py +0 -0
  32. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tool_message.py +0 -0
  33. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tools/__init__.py +0 -0
  34. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tools/google_search_tool.py +0 -0
  35. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tools/recipient_tool.py +0 -0
  36. {langroid-0.1.52 → langroid-0.1.54}/langroid/agent_config.py +0 -0
  37. {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/__init__.py +0 -0
  38. {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/base.py +0 -0
  39. {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/momento_cachedb.py +0 -0
  40. {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/redis_cachedb.py +0 -0
  41. {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/__init__.py +0 -0
  42. {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/base.py +0 -0
  43. {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/clustering.py +0 -0
  44. {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/models.py +0 -0
  45. /langroid-0.1.52/langroid/language_models/__init__.py → /langroid-0.1.54/langroid/io/base.py +0 -0
  46. /langroid-0.1.52/langroid/parsing/__init__.py → /langroid-0.1.54/langroid/io/cmd_line.py +0 -0
  47. /langroid-0.1.52/langroid/prompts/__init__.py → /langroid-0.1.54/langroid/io/websocket.py +0 -0
  48. {langroid-0.1.52/langroid/scripts → langroid-0.1.54/langroid/language_models}/__init__.py +0 -0
  49. {langroid-0.1.52 → langroid-0.1.54}/langroid/language_models/utils.py +0 -0
  50. {langroid-0.1.52 → langroid-0.1.54}/langroid/mytypes.py +0 -0
  51. {langroid-0.1.52/langroid/utils → langroid-0.1.54/langroid/parsing}/__init__.py +0 -0
  52. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/agent_chats.py +0 -0
  53. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/code-parsing.md +0 -0
  54. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/code_parser.py +0 -0
  55. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/json.py +0 -0
  56. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/para_sentence_split.py +0 -0
  57. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/parser.py +0 -0
  58. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/pdf_parser.py +0 -0
  59. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/repo_loader.py +0 -0
  60. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/table_loader.py +0 -0
  61. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/url_loader.py +0 -0
  62. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/url_loader_cookies.py +0 -0
  63. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/urls.py +0 -0
  64. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/utils.py +0 -0
  65. {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/web_search.py +0 -0
  66. {langroid-0.1.52/langroid/utils/llms → langroid-0.1.54/langroid/prompts}/__init__.py +0 -0
  67. {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/dialog.py +0 -0
  68. {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/prompts_config.py +0 -0
  69. {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/transforms.py +0 -0
  70. {langroid-0.1.52/langroid/utils/output → langroid-0.1.54/langroid/scripts}/__init__.py +0 -0
  71. {langroid-0.1.52/langroid/utils/web → langroid-0.1.54/langroid/utils}/__init__.py +0 -0
  72. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/configuration.py +0 -0
  73. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/constants.py +0 -0
  74. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/docker.py +0 -0
  75. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/globals.py +0 -0
  76. {langroid-0.1.52/langroid/vector_store → langroid-0.1.54/langroid/utils/llms}/__init__.py +0 -0
  77. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/llms/strings.py +0 -0
  78. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/logging.py +0 -0
  79. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/output/printing.py +0 -0
  80. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/pydantic_utils.py +0 -0
  81. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/system.py +0 -0
  82. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/web/login.py +0 -0
  83. {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/web/selenium_login.py +0 -0
  84. {langroid-0.1.52 → langroid-0.1.54}/langroid/vector_store/base.py +0 -0
  85. {langroid-0.1.52 → langroid-0.1.54}/langroid/vector_store/chromadb.py +0 -0
  86. {langroid-0.1.52 → langroid-0.1.54}/langroid/vector_store/qdrant_cloud.py +0 -0
  87. {langroid-0.1.52 → langroid-0.1.54}/langroid/vector_store/qdrantdb.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langroid
3
- Version: 0.1.52
3
+ Version: 0.1.54
4
4
  Summary: Harness LLMs with Multi-Agent Programming
5
5
  License: MIT
6
6
  Author: Prasad Chalasani
@@ -75,7 +75,7 @@ Description-Content-Type: text/markdown
75
75
 
76
76
  <div align="center">
77
77
 
78
- [![PyPI version](https://badge.fury.io/py/langroid.svg)](https://badge.fury.io/py/langroid)
78
+ [![PyPI - Version](https://img.shields.io/pypi/v/langroid)](https://pypi.org/project/langroid/)
79
79
  [![Pytest](https://github.com/langroid/langroid/actions/workflows/pytest.yml/badge.svg)](https://github.com/langroid/langroid/actions/workflows/pytest.yml)
80
80
  [![codecov](https://codecov.io/gh/langroid/langroid/branch/main/graph/badge.svg?token=H94BX5F0TE)](https://codecov.io/gh/langroid/langroid)
81
81
  [![Lint](https://github.com/langroid/langroid/actions/workflows/validate.yml/badge.svg)](https://github.com/langroid/langroid/actions/workflows/validate.yml)
@@ -135,6 +135,7 @@ for ideas on what to contribute.
135
135
  <summary> <b>:fire: Updates/Releases</b></summary>
136
136
 
137
137
  - **Aug 2023:**
138
+ - **[Hierarchical computation](https://langroid.github.io/langroid/examples/agent-tree/)** example using Langroid agents and task orchestration.
138
139
  - **0.1.51:** Support for global state, see [test_global_state.py](tests/main/test_global_state.py).
139
140
  - **:whale: Langroid Docker image**, available, see instructions below.
140
141
  - [**RecipientTool**](langroid/agent/tools/recipient_tool.py) enables (+ enforces) LLM to
@@ -328,6 +329,26 @@ GOOGLE_CSE_ID=your-cse-id
328
329
  ```
329
330
  </details>
330
331
 
332
+ <details>
333
+ <summary><b>Setup instructions for Microsoft Azure OpenAI(click to expand)</b></summary>
334
+ In the root of the repo, copy the `.azure_env_template` file to a new file `.azure_env`:
335
+
336
+ ```bash
337
+ cp .azure_env_template .azure_env
338
+ ```
339
+
340
+ The file `.azure_env` contains four environment variables that are required to use Azure OpenAI: `AZURE_API_KEY`, `OPENAI_API_BASE`, `OPENAI_API_VERSION`, and `OPENAI_DEPLOYMENT_NAME`
341
+
342
+ This page [Microsoft Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line&pivots=programming-language-python#environment-variables)
343
+ provides more information, and you can set each environment variable as follows:
344
+
345
+ - `AZURE_API_KEY`, from the value of `API_KEY`
346
+ - `OPENAI_API_BASE` from the value of `ENDPOINT`, typically looks like `https://your.domain.azure.com`.
347
+ - For `OPENAI_API_VERSION`, you can use the default value in `.azure_env_template`, and latest version can be found [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/whats-new#azure-openai-chat-completion-general-availability-ga)
348
+ - `OPENAI_DEPLOYMENT_NAME` is the deployment name you chose when you deployed the GPT-35-Turbo or GPT-4 models.
349
+
350
+ </details>
351
+
331
352
  ---
332
353
 
333
354
  # :whale: Docker Instructions
@@ -5,7 +5,7 @@
5
5
 
6
6
  <div align="center">
7
7
 
8
- [![PyPI version](https://badge.fury.io/py/langroid.svg)](https://badge.fury.io/py/langroid)
8
+ [![PyPI - Version](https://img.shields.io/pypi/v/langroid)](https://pypi.org/project/langroid/)
9
9
  [![Pytest](https://github.com/langroid/langroid/actions/workflows/pytest.yml/badge.svg)](https://github.com/langroid/langroid/actions/workflows/pytest.yml)
10
10
  [![codecov](https://codecov.io/gh/langroid/langroid/branch/main/graph/badge.svg?token=H94BX5F0TE)](https://codecov.io/gh/langroid/langroid)
11
11
  [![Lint](https://github.com/langroid/langroid/actions/workflows/validate.yml/badge.svg)](https://github.com/langroid/langroid/actions/workflows/validate.yml)
@@ -65,6 +65,7 @@ for ideas on what to contribute.
65
65
  <summary> <b>:fire: Updates/Releases</b></summary>
66
66
 
67
67
  - **Aug 2023:**
68
+ - **[Hierarchical computation](https://langroid.github.io/langroid/examples/agent-tree/)** example using Langroid agents and task orchestration.
68
69
  - **0.1.51:** Support for global state, see [test_global_state.py](tests/main/test_global_state.py).
69
70
  - **:whale: Langroid Docker image**, available, see instructions below.
70
71
  - [**RecipientTool**](langroid/agent/tools/recipient_tool.py) enables (+ enforces) LLM to
@@ -258,6 +259,26 @@ GOOGLE_CSE_ID=your-cse-id
258
259
  ```
259
260
  </details>
260
261
 
262
+ <details>
263
+ <summary><b>Setup instructions for Microsoft Azure OpenAI(click to expand)</b></summary>
264
+ In the root of the repo, copy the `.azure_env_template` file to a new file `.azure_env`:
265
+
266
+ ```bash
267
+ cp .azure_env_template .azure_env
268
+ ```
269
+
270
+ The file `.azure_env` contains four environment variables that are required to use Azure OpenAI: `AZURE_API_KEY`, `OPENAI_API_BASE`, `OPENAI_API_VERSION`, and `OPENAI_DEPLOYMENT_NAME`
271
+
272
+ This page [Microsoft Azure OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line&pivots=programming-language-python#environment-variables)
273
+ provides more information, and you can set each environment variable as follows:
274
+
275
+ - `AZURE_API_KEY`, from the value of `API_KEY`
276
+ - `OPENAI_API_BASE` from the value of `ENDPOINT`, typically looks like `https://your.domain.azure.com`.
277
+ - For `OPENAI_API_VERSION`, you can use the default value in `.azure_env_template`, and latest version can be found [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/whats-new#azure-openai-chat-completion-general-availability-ga)
278
+ - `OPENAI_DEPLOYMENT_NAME` is the deployment name you chose when you deployed the GPT-35-Turbo or GPT-4 models.
279
+
280
+ </details>
281
+
261
282
  ---
262
283
 
263
284
  # :whale: Docker Instructions
@@ -1,9 +1,20 @@
1
1
  import inspect
2
2
  import json
3
3
  import logging
4
+ import textwrap
4
5
  from abc import ABC
5
6
  from contextlib import ExitStack
6
- from typing import Callable, Dict, List, Optional, Set, Tuple, Type, cast, no_type_check
7
+ from typing import (
8
+ Callable,
9
+ Dict,
10
+ List,
11
+ Optional,
12
+ Set,
13
+ Tuple,
14
+ Type,
15
+ cast,
16
+ no_type_check,
17
+ )
7
18
 
8
19
  from pydantic import BaseSettings, ValidationError
9
20
  from rich import print
@@ -15,6 +26,9 @@ from langroid.agent.tool_message import INSTRUCTION, ToolMessage
15
26
  from langroid.language_models.base import (
16
27
  LanguageModel,
17
28
  LLMConfig,
29
+ LLMMessage,
30
+ LLMResponse,
31
+ LLMTokenUsage,
18
32
  )
19
33
  from langroid.mytypes import DocMetaData, Entity
20
34
  from langroid.parsing.json import extract_top_level_json
@@ -60,6 +74,8 @@ class Agent(ABC):
60
74
  self.llm_tools_map: Dict[str, Type[ToolMessage]] = {}
61
75
  self.llm_tools_handled: Set[str] = set()
62
76
  self.llm_tools_usable: Set[str] = set()
77
+ self.total_llm_token_cost = 0.0
78
+ self.total_llm_token_usage = 0
63
79
  self.default_human_response: Optional[str] = None
64
80
  self._indent = ""
65
81
  self.llm = LanguageModel.create(config.llm)
@@ -315,7 +331,7 @@ class Agent(ABC):
315
331
  else:
316
332
  user_msg = Prompt.ask(
317
333
  f"[blue]{self.indent}Human "
318
- f"(respond or q, x to exit current level, "
334
+ "(respond or q, x to exit current level, "
319
335
  f"or hit enter to continue)\n{self.indent}",
320
336
  ).strip()
321
337
 
@@ -410,6 +426,7 @@ class Agent(ABC):
410
426
  if self.llm.get_stream():
411
427
  console.print(f"[green]{self.indent}", end="")
412
428
  response = self.llm.generate(prompt, output_len)
429
+
413
430
  displayed = False
414
431
  if not self.llm.get_stream() or response.cached:
415
432
  # we would have already displayed the msg "live" ONLY if
@@ -417,7 +434,7 @@ class Agent(ABC):
417
434
  console.print(f"[green]{self.indent}", end="")
418
435
  print("[green]" + response.message)
419
436
  displayed = True
420
-
437
+ self.update_token_usage(response, prompt, self.llm.get_stream())
421
438
  return ChatDocument.from_LLMResponse(response, displayed)
422
439
 
423
440
  def get_tool_messages(self, msg: str | ChatDocument) -> List[ToolMessage]:
@@ -594,10 +611,66 @@ class Agent(ABC):
594
611
  result = f"Error in tool/function-call {tool_name} usage: {type(e)}: {e}"
595
612
  return result # type: ignore
596
613
 
597
- def num_tokens(self, prompt: str) -> int:
614
+ def num_tokens(self, prompt: str | List[LLMMessage]) -> int:
598
615
  if self.parser is None:
599
616
  raise ValueError("Parser must be set, to count tokens")
600
- return self.parser.num_tokens(prompt)
617
+ if isinstance(prompt, str):
618
+ return self.parser.num_tokens(prompt)
619
+ else:
620
+ return sum([self.parser.num_tokens(m.content) for m in prompt])
621
+
622
+ def update_token_usage(
623
+ self, response: LLMResponse, prompt: str | List[LLMMessage], stream: bool
624
+ ) -> None:
625
+ """
626
+ Updates `response.usage` obj (token usage and cost fields).the usage memebr
627
+ It updates the cost after checking the cache and updates the
628
+ tokens (prompts and completion) if the response stream is True, because OpenAI
629
+ doesn't returns these fields.
630
+
631
+ Args:
632
+ response (LLMResponse): LLMResponse object
633
+ prompt (str | List[LLMMessage]): prompt or list of LLMMessage objects
634
+ stream (bool): whether to update the usage in the response object
635
+ if the response is not cached.
636
+ """
637
+ if response is not None:
638
+ # Note: If response was not streamed, then
639
+ # `response.usage` would already have been set by the API,
640
+ # so we only need to update in the stream case.
641
+ if stream:
642
+ # usage, cost = 0 when response is from cache
643
+ prompt_tokens = 0
644
+ completion_tokens = 0
645
+ cost = 0.0
646
+ if not response.cached:
647
+ prompt_tokens = self.num_tokens(prompt)
648
+ completion_tokens = self.num_tokens(response.message)
649
+ cost = self.compute_token_cost(prompt_tokens, completion_tokens)
650
+ response.usage = LLMTokenUsage(
651
+ prompt_tokens=prompt_tokens,
652
+ completion_tokens=completion_tokens,
653
+ cost=cost,
654
+ )
655
+
656
+ if settings.debug and response.usage is not None:
657
+ print(
658
+ textwrap.dedent(
659
+ f"""
660
+ Stream: {stream}
661
+ prompt_tokens: {response.usage.prompt_tokens}
662
+ completion_tokens: {response.usage.completion_tokens}
663
+ """.lstrip()
664
+ )
665
+ )
666
+ # update total counters
667
+ if response.usage is not None:
668
+ self.total_llm_token_cost += response.usage.cost
669
+ self.total_llm_token_usage += response.usage.total_tokens
670
+
671
+ def compute_token_cost(self, prompt: int, completion: int) -> float:
672
+ price = cast(LanguageModel, self.llm).chat_cost()
673
+ return (price[0] * prompt + price[1] * completion) / 1000
601
674
 
602
675
  def ask_agent(
603
676
  self,
@@ -453,7 +453,8 @@ class ChatAgent(Agent):
453
453
  else:
454
454
  response_str = response.message
455
455
  print(cached + "[green]" + response_str)
456
-
456
+ stream = self.llm.get_stream() # type: ignore
457
+ self.update_token_usage(response, messages, stream)
457
458
  return ChatDocument.from_LLMResponse(response, displayed)
458
459
 
459
460
  def _llm_response_temp_context(self, message: str, prompt: str) -> ChatDocument:
@@ -7,6 +7,7 @@ from langroid.language_models.base import (
7
7
  LLMFunctionCall,
8
8
  LLMMessage,
9
9
  LLMResponse,
10
+ LLMTokenUsage,
10
11
  Role,
11
12
  )
12
13
  from langroid.mytypes import DocMetaData, Document, Entity
@@ -29,7 +30,7 @@ class ChatDocMetaData(DocMetaData):
29
30
  block: None | Entity = None
30
31
  sender_name: str = ""
31
32
  recipient: str = ""
32
- usage: int = 0
33
+ usage: Optional[LLMTokenUsage]
33
34
  cached: bool = False
34
35
  displayed: bool = False
35
36
 
@@ -119,7 +120,8 @@ class ChatDocument(Document):
119
120
 
120
121
  @staticmethod
121
122
  def from_LLMResponse(
122
- response: LLMResponse, displayed: bool = False
123
+ response: LLMResponse,
124
+ displayed: bool = False,
123
125
  ) -> "ChatDocument":
124
126
  recipient, message = response.get_recipient_and_message()
125
127
  return ChatDocument(
@@ -183,7 +185,10 @@ class ChatDocument(Document):
183
185
  content = message
184
186
 
185
187
  return LLMMessage(
186
- role=sender_role, content=content, function_call=fun_call, name=sender_name
188
+ role=sender_role,
189
+ content=content,
190
+ function_call=fun_call,
191
+ name=sender_name,
187
192
  )
188
193
 
189
194
 
@@ -7,7 +7,7 @@ Functionality includes:
7
7
  """
8
8
  import logging
9
9
  from contextlib import ExitStack
10
- from typing import List, Optional, no_type_check
10
+ from typing import List, Optional, Tuple, no_type_check
11
11
 
12
12
  from rich import print
13
13
  from rich.console import Console
@@ -304,7 +304,7 @@ class DocChatAgent(ChatAgent):
304
304
  )
305
305
 
306
306
  @no_type_check
307
- def get_relevant_extracts(self, query: str) -> List[Document]:
307
+ def get_relevant_extracts(self, query: str) -> Tuple[str, List[Document]]:
308
308
  """
309
309
  Get list of docs or extracts relevant to a query. These could be:
310
310
  - the original docs, if they exist and are not too long, or
@@ -316,6 +316,7 @@ class DocChatAgent(ChatAgent):
316
316
  query (str): query to search for
317
317
 
318
318
  Returns:
319
+ query (str): stand-alone version of input query
319
320
  List[Document]: list of relevant docs
320
321
 
321
322
  """
@@ -341,20 +342,18 @@ class DocChatAgent(ChatAgent):
341
342
  k=self.config.parsing.n_similar_docs,
342
343
  )
343
344
  if len(docs_and_scores) == 0:
344
- return []
345
+ return query, []
345
346
  passages = [
346
347
  Document(content=d.content, metadata=d.metadata)
347
348
  for (d, _) in docs_and_scores
348
349
  ]
349
350
 
350
- # if passages not too long, no need to extract relevant verbatim text
351
- extracts = passages
352
- if self.doc_length(passages) > self.config.max_context_tokens:
353
- with console.status("[cyan]LLM Extracting verbatim passages..."):
354
- with StreamingIfAllowed(self.llm, False):
355
- extracts = self.llm.get_verbatim_extracts(query, passages)
351
+ with console.status("[cyan]LLM Extracting verbatim passages..."):
352
+ with StreamingIfAllowed(self.llm, False):
353
+ extracts = self.llm.get_verbatim_extracts(query, passages)
354
+ extracts = [e for e in extracts if e.content != NO_ANSWER]
356
355
 
357
- return extracts
356
+ return query, extracts
358
357
 
359
358
  @no_type_check
360
359
  def answer_from_docs(self, query: str) -> Document:
@@ -373,7 +372,8 @@ class DocChatAgent(ChatAgent):
373
372
  source="None",
374
373
  ),
375
374
  )
376
- extracts = self.get_relevant_extracts(query)
375
+ # query may be updated to a stand-alone version
376
+ query, extracts = self.get_relevant_extracts(query)
377
377
  if len(extracts) == 0:
378
378
  return response
379
379
  with ExitStack() as stack:
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- from typing import Callable, Dict, List, Optional, Type, cast
4
+ from typing import Callable, Dict, List, Optional, Set, Type, cast
5
5
 
6
6
  from rich import print
7
7
 
@@ -155,7 +155,8 @@ class Task:
155
155
 
156
156
  # other sub_tasks this task can delegate to
157
157
  self.sub_tasks: List[Task] = []
158
- self.parent_task: Optional[Task] = None
158
+ self.parent_task: Set[Task] = set()
159
+ self.caller: Task | None = None # which task called this task's `run` method
159
160
 
160
161
  def __repr__(self) -> str:
161
162
  return f"{self.name}"
@@ -165,10 +166,9 @@ class Task:
165
166
 
166
167
  @property
167
168
  def _level(self) -> int:
168
- if self.parent_task is None:
169
+ if self.caller is None:
169
170
  return 0
170
- else:
171
- return self.parent_task._level + 1
171
+ return self.caller._level + 1
172
172
 
173
173
  @property
174
174
  def _indent(self) -> str:
@@ -199,7 +199,7 @@ class Task:
199
199
  return
200
200
  assert isinstance(task, Task), f"added task must be a Task, not {type(task)}"
201
201
 
202
- task.parent_task = self
202
+ task.parent_task.add(self) # add myself to set of parent tasks of `task`
203
203
  self.sub_tasks.append(task)
204
204
  self.name_sub_task_map[task.name] = task
205
205
  self.responders.append(cast(Responder, task))
@@ -226,18 +226,18 @@ class Task:
226
226
  )
227
227
  else:
228
228
  self.pending_message = msg
229
- if self.pending_message is not None and self.parent_task is not None:
230
- # msg may have come from parent_task, so we pretend this is from
229
+ if self.pending_message is not None and self.caller is not None:
230
+ # msg may have come from `caller`, so we pretend this is from
231
231
  # the CURRENT task's USER entity
232
232
  self.pending_message.metadata.sender = Entity.USER
233
233
 
234
- if self.parent_task is not None and self.parent_task.logger is not None:
235
- self.logger = self.parent_task.logger
234
+ if self.caller is not None and self.caller.logger is not None:
235
+ self.logger = self.caller.logger
236
236
  else:
237
237
  self.logger = RichFileLogger(f"logs/{self.name}.log", color=self.color_log)
238
238
 
239
- if self.parent_task is not None and self.parent_task.tsv_logger is not None:
240
- self.tsv_logger = self.parent_task.tsv_logger
239
+ if self.caller is not None and self.caller.tsv_logger is not None:
240
+ self.tsv_logger = self.caller.tsv_logger
241
241
  else:
242
242
  self.tsv_logger = setup_file_logger("tsv_logger", f"logs/{self.name}.tsv")
243
243
  header = ChatDocLoggerFields().tsv_header()
@@ -250,6 +250,7 @@ class Task:
250
250
  self,
251
251
  msg: Optional[str | ChatDocument] = None,
252
252
  turns: int = -1,
253
+ caller: None | Task = None,
253
254
  ) -> Optional[ChatDocument]:
254
255
  """
255
256
  Loop over `step()` until task is considered done or `turns` is reached.
@@ -264,6 +265,7 @@ class Task:
264
265
  LLM or Human (User).
265
266
  turns (int): number of turns to run the task for;
266
267
  default is -1, which means run until task is done.
268
+ caller (Task|None): the calling task, if any
267
269
 
268
270
  Returns:
269
271
  Optional[ChatDocument]: valid response from the agent
@@ -281,7 +283,7 @@ class Task:
281
283
  ):
282
284
  # this task is not the intended recipient so return None
283
285
  return None
284
-
286
+ self.caller = caller
285
287
  self.init(msg)
286
288
  # sets indentation to be printed prior to any output from agent
287
289
  self.agent.indent = self._indent
@@ -447,20 +449,22 @@ class Task:
447
449
 
448
450
  def response(self, e: Responder, turns: int = -1) -> Optional[ChatDocument]:
449
451
  """
450
- Get response to `self.pending_message` from an entity.
452
+ Get response to `self.pending_message` from a responder.
451
453
  If response is __valid__ (i.e. it ends the current turn of seeking
452
454
  responses):
453
455
  -then return the response as a ChatDocument object,
454
456
  -otherwise return None.
455
457
  Args:
456
- e (Entity): entity to get response from
458
+ e (Responder): responder to get response from.
459
+ turns (int): number of turns to run the task for.
460
+ Default is -1, which means run until task is done.
457
461
  Returns:
458
462
  Optional[ChatDocument]: response to `self.pending_message` from entity if
459
463
  valid, None otherwise
460
464
  """
461
465
  if isinstance(e, Task):
462
466
  actual_turns = e.turns if e.turns > 0 else turns
463
- return e.run(self.pending_message, turns=actual_turns)
467
+ return e.run(self.pending_message, turns=actual_turns, caller=self)
464
468
  else:
465
469
  return self._entity_responder_map[cast(Entity, e)](self.pending_message)
466
470
 
@@ -521,10 +525,10 @@ class Task:
521
525
  self.pending_message is None
522
526
  # LLM decided task is done
523
527
  or DONE in self.pending_message.content
524
- or ( # current task is addressing message to parent task
525
- self.parent_task is not None
526
- and self.parent_task.name != ""
527
- and self.pending_message.metadata.recipient == self.parent_task.name
528
+ or ( # current task is addressing message to caller task
529
+ self.caller is not None
530
+ and self.caller.name != ""
531
+ and self.pending_message.metadata.recipient == self.caller.name
528
532
  )
529
533
  or (
530
534
  # Task controller is "stuck", has nothing to say
@@ -0,0 +1 @@
1
+ https://chat.openai.com/share/7c440b3f-ddbf-4ae6-a26f-ac28d947d403
@@ -0,0 +1,72 @@
1
+ import os
2
+
3
+ import openai
4
+ from dotenv import load_dotenv
5
+
6
+ from langroid.language_models.openai_gpt import OpenAIGPT, OpenAIGPTConfig
7
+
8
+
9
+ class AzureConfig(OpenAIGPTConfig):
10
+ """
11
+ Configuration for Azure OpenAI GPT. You need to supply the env vars listed in
12
+ ``.azure_env_template`` after renaming the file to ``.azure_env``. Because this file
13
+ is used by this class to find the env vars.
14
+ Attributes:
15
+ type (str): should be ``azure``
16
+ api_version (str): can be set inside the ``.azure_env``
17
+ deployment_name (str): can be set inside the ``.azure_env`` and should be based
18
+ the custom name you chose for your deployment when you deployed a model
19
+ """
20
+
21
+ type: str = "azure"
22
+ api_version: str = "2023-07-01-preview"
23
+ deployment_name: str = ""
24
+
25
+
26
+ class AzureGPT(OpenAIGPT):
27
+ """
28
+ Class to access OpenAI LLMs via Azure. These env variables can be obtained from the
29
+ file `.azure_env`. Azure OpenAI doesn't support ``completion``
30
+ Attributes:
31
+ config: AzureConfig object
32
+ api_key: Azure API key
33
+ api_base: Azure API base url
34
+ api_version: Azure API version
35
+ """
36
+
37
+ def __init__(self, config: AzureConfig):
38
+ super().__init__(config)
39
+ self.config: AzureConfig = config
40
+ self.api_type = config.type
41
+ openai.api_type = self.api_type
42
+ load_dotenv(dotenv_path=".azure_env")
43
+ self.api_key = os.getenv("AZURE_API_KEY", "")
44
+ if self.api_key == "":
45
+ raise ValueError(
46
+ """
47
+ AZURE_API_KEY not set in .env file,
48
+ please set it to your Azure API key."""
49
+ )
50
+
51
+ self.api_base = os.getenv("OPENAI_API_BASE", "")
52
+ if self.api_base == "":
53
+ raise ValueError(
54
+ """
55
+ OPENAI_API_BASE not set in .env file,
56
+ please set it to your Azure API key."""
57
+ )
58
+ # we don't need this for ``api_key`` because it's handled inside
59
+ # ``openai_gpt.py`` methods before invoking chat/completion calls
60
+ else:
61
+ openai.api_base = self.api_base
62
+
63
+ self.api_version = os.getenv("OPENAI_API_VERSION", "") or config.api_version
64
+ openai.api_version = self.api_version
65
+
66
+ self.deployment_name = os.getenv("OPENAI_DEPLOYMENT_NAME", "")
67
+ if self.deployment_name == "":
68
+ raise ValueError(
69
+ """
70
+ OPENAI_DEPLOYMENT_NAME not set in .env file,
71
+ please set it to your Azure API key."""
72
+ )
@@ -36,6 +36,9 @@ class LLMConfig(BaseSettings):
36
36
  stream: bool = False # stream output from API?
37
37
  cache_config: None | RedisCacheConfig | MomentoCacheConfig = None
38
38
 
39
+ # Dict of model -> (input/prompt cost, output/completion cost)
40
+ cost_per_1k_tokens: Optional[Dict[str, Tuple[float, float]]] = None
41
+
39
42
 
40
43
  class LLMFunctionCall(BaseModel):
41
44
  """
@@ -63,6 +66,16 @@ class LLMFunctionSpec(BaseModel):
63
66
  parameters: Dict[str, Any]
64
67
 
65
68
 
69
+ class LLMTokenUsage(BaseModel):
70
+ prompt_tokens: int = 0
71
+ completion_tokens: int = 0
72
+ cost: float = 0.0
73
+
74
+ @property
75
+ def total_tokens(self) -> int:
76
+ return self.prompt_tokens + self.completion_tokens
77
+
78
+
66
79
  class Role(str, Enum):
67
80
  USER = "user"
68
81
  SYSTEM = "system"
@@ -116,7 +129,7 @@ class LLMResponse(BaseModel):
116
129
 
117
130
  message: str
118
131
  function_call: Optional[LLMFunctionCall] = None
119
- usage: int
132
+ usage: Optional[LLMTokenUsage]
120
133
  cached: bool = False
121
134
 
122
135
  def to_LLMMessage(self) -> LLMMessage:
@@ -193,13 +206,21 @@ class LanguageModel(ABC):
193
206
  config: configuration for language model
194
207
  Returns: instance of language model
195
208
  """
209
+ from langroid.language_models.azure_openai import AzureGPT
196
210
  from langroid.language_models.openai_gpt import OpenAIGPT
197
211
 
198
212
  if config is None or config.type is None:
199
213
  return None
214
+
215
+ openai: Union[Type[AzureGPT], Type[OpenAIGPT]]
216
+
217
+ if config.type == "azure":
218
+ openai = AzureGPT
219
+ else:
220
+ openai = OpenAIGPT
200
221
  cls = dict(
201
- openai=OpenAIGPT,
202
- ).get(config.type, OpenAIGPT)
222
+ openai=openai,
223
+ ).get(config.type, openai)
203
224
  return cls(config) # type: ignore
204
225
 
205
226
  @abstractmethod
@@ -248,6 +269,13 @@ class LanguageModel(ABC):
248
269
  raise ValueError("No context length specified")
249
270
  return self.config.context_length[self.config.completion_model]
250
271
 
272
+ def chat_cost(self) -> Tuple[float, float]:
273
+ if self.config.chat_model is None:
274
+ raise ValueError("No chat model specified")
275
+ if self.config.cost_per_1k_tokens is None:
276
+ raise ValueError("No cost per 1k tokens specified")
277
+ return self.config.cost_per_1k_tokens[self.config.chat_model]
278
+
251
279
  def followup_to_standalone(
252
280
  self, chat_history: List[Tuple[str, str]], question: str
253
281
  ) -> str:
@@ -368,7 +396,10 @@ class LanguageModel(ABC):
368
396
  sources = ""
369
397
  return Document(
370
398
  content=content,
371
- metadata={"source": "SOURCE: " + sources, "cached": llm_response.cached},
399
+ metadata={
400
+ "source": "SOURCE: " + sources,
401
+ "cached": llm_response.cached,
402
+ },
372
403
  )
373
404
 
374
405