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.
- {langroid-0.1.52 → langroid-0.1.54}/PKG-INFO +23 -2
- {langroid-0.1.52 → langroid-0.1.54}/README.md +22 -1
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/base.py +78 -5
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/chat_agent.py +2 -1
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/chat_document.py +8 -3
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/doc_chat_agent.py +11 -11
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/task.py +24 -20
- langroid-0.1.54/langroid/io/refs.md +1 -0
- langroid-0.1.54/langroid/language_models/azure_openai.py +72 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/language_models/base.py +35 -4
- {langroid-0.1.52 → langroid-0.1.54}/langroid/language_models/openai_gpt.py +49 -16
- {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/templates.py +5 -4
- langroid-0.1.54/langroid/utils/output/__init__.py +0 -0
- langroid-0.1.54/langroid/utils/web/__init__.py +0 -0
- langroid-0.1.54/langroid/vector_store/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/pyproject.toml +1 -1
- langroid-0.1.54/setup.py +98 -0
- langroid-0.1.52/setup.py +0 -97
- {langroid-0.1.52 → langroid-0.1.54}/LICENSE +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/helpers.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/junk +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/recipient_validator_agent.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/retriever_agent.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/sql_chat_agent.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/utils/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/sql/utils/description_extractors.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/special/table_chat_agent.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tool_message.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tools/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tools/google_search_tool.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent/tools/recipient_tool.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/agent_config.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/base.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/momento_cachedb.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/cachedb/redis_cachedb.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/base.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/clustering.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/embedding_models/models.py +0 -0
- /langroid-0.1.52/langroid/language_models/__init__.py → /langroid-0.1.54/langroid/io/base.py +0 -0
- /langroid-0.1.52/langroid/parsing/__init__.py → /langroid-0.1.54/langroid/io/cmd_line.py +0 -0
- /langroid-0.1.52/langroid/prompts/__init__.py → /langroid-0.1.54/langroid/io/websocket.py +0 -0
- {langroid-0.1.52/langroid/scripts → langroid-0.1.54/langroid/language_models}/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/language_models/utils.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/mytypes.py +0 -0
- {langroid-0.1.52/langroid/utils → langroid-0.1.54/langroid/parsing}/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/agent_chats.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/code-parsing.md +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/code_parser.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/json.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/para_sentence_split.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/parser.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/pdf_parser.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/repo_loader.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/table_loader.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/url_loader.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/url_loader_cookies.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/urls.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/utils.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/parsing/web_search.py +0 -0
- {langroid-0.1.52/langroid/utils/llms → langroid-0.1.54/langroid/prompts}/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/dialog.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/prompts_config.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/prompts/transforms.py +0 -0
- {langroid-0.1.52/langroid/utils/output → langroid-0.1.54/langroid/scripts}/__init__.py +0 -0
- {langroid-0.1.52/langroid/utils/web → langroid-0.1.54/langroid/utils}/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/configuration.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/constants.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/docker.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/globals.py +0 -0
- {langroid-0.1.52/langroid/vector_store → langroid-0.1.54/langroid/utils/llms}/__init__.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/llms/strings.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/logging.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/output/printing.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/pydantic_utils.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/system.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/web/login.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/utils/web/selenium_login.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/vector_store/base.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/vector_store/chromadb.py +0 -0
- {langroid-0.1.52 → langroid-0.1.54}/langroid/vector_store/qdrant_cloud.py +0 -0
- {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.
|
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
|
-
[](https://pypi.org/project/langroid/)
|
79
79
|
[](https://github.com/langroid/langroid/actions/workflows/pytest.yml)
|
80
80
|
[](https://codecov.io/gh/langroid/langroid)
|
81
81
|
[](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
|
-
[](https://pypi.org/project/langroid/)
|
9
9
|
[](https://github.com/langroid/langroid/actions/workflows/pytest.yml)
|
10
10
|
[](https://codecov.io/gh/langroid/langroid)
|
11
11
|
[](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
|
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
|
-
|
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
|
-
|
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:
|
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,
|
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,
|
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
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
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
|
-
|
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:
|
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.
|
169
|
+
if self.caller is None:
|
169
170
|
return 0
|
170
|
-
|
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
|
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.
|
230
|
-
# msg may have come 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.
|
235
|
-
self.logger = self.
|
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.
|
240
|
-
self.tsv_logger = self.
|
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
|
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 (
|
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
|
525
|
-
self.
|
526
|
-
and self.
|
527
|
-
and self.pending_message.metadata.recipient == self.
|
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:
|
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=
|
202
|
-
).get(config.type,
|
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={
|
399
|
+
metadata={
|
400
|
+
"source": "SOURCE: " + sources,
|
401
|
+
"cached": llm_response.cached,
|
402
|
+
},
|
372
403
|
)
|
373
404
|
|
374
405
|
|