unique_toolkit 0.8.13__tar.gz → 0.8.15__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.
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/CHANGELOG.md +8 -2
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/PKG-INFO +9 -3
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/pyproject.toml +1 -1
- unique_toolkit-0.8.15/unique_toolkit/_common/default_language_model.py +6 -0
- unique_toolkit-0.8.15/unique_toolkit/_common/token/image_token_counting.py +67 -0
- unique_toolkit-0.8.15/unique_toolkit/_common/token/token_counting.py +196 -0
- unique_toolkit-0.8.15/unique_toolkit/history_manager/history_construction_with_contents.py +307 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/history_manager/history_manager.py +85 -111
- unique_toolkit-0.8.15/unique_toolkit/history_manager/loop_token_reducer.py +457 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/infos.py +119 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/reference_manager/reference_manager.py +15 -2
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/LICENSE +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/README.md +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/_common/_base_service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/_common/_time_utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/_common/exception.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/_common/validate_required_values.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/_common/validators.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/dev_util.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/init_logging.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/init_sdk.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/performance/async_tasks.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/performance/async_wrapper.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/unique_settings.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/app/verification.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/chat/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/chat/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/chat/functions.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/chat/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/chat/service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/chat/state.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/chat/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/content/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/content/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/content/functions.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/content/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/content/service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/content/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/embedding/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/embedding/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/embedding/functions.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/embedding/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/embedding/service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/embedding/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evals/evaluation_manager.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evals/exception.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evals/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/config.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/context_relevancy/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/context_relevancy/prompts.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/context_relevancy/service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/context_relevancy/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/exception.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/hallucination/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/hallucination/prompts.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/hallucination/service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/hallucination/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/output_parser.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/evaluators/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/framework_utilities/langchain/client.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/framework_utilities/langchain/history.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/framework_utilities/openai/client.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/framework_utilities/openai/message_builder.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/framework_utilities/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/history_manager/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/builder.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/functions.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/prompt.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/reference.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/language_model/utils.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/postprocessor/postprocessor_manager.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/protocols/support.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/short_term_memory/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/short_term_memory/constants.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/short_term_memory/functions.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/short_term_memory/persistent_short_term_memory_manager.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/short_term_memory/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/short_term_memory/service.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/smart_rules/__init__.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/smart_rules/compile.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/thinking_manager/thinking_manager.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/agent_chunks_handler.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/config.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/factory.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/schemas.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/test/test_tool_progress_reporter.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/tool.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/tool_manager.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/tool_progress_reporter.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/utils/execution/execution.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/utils/source_handling/schema.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/utils/source_handling/source_formatting.py +0 -0
- {unique_toolkit-0.8.13 → unique_toolkit-0.8.15}/unique_toolkit/tools/utils/source_handling/tests/test_source_formatting.py +0 -0
|
@@ -5,7 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.8.
|
|
8
|
+
## [0.8.15] - 2025-08-19
|
|
9
|
+
- Added history loading from database for History Manager
|
|
10
|
+
|
|
11
|
+
## [0.8.14] - 2025-08-19
|
|
12
|
+
- Including GPT-5 series deployed via LiteLLM into language model info
|
|
13
|
+
|
|
14
|
+
## [0.8.13] - 2025-08-18
|
|
9
15
|
- Adding initial versions of
|
|
10
16
|
- Evaluation Manager
|
|
11
17
|
- History Manager
|
|
@@ -13,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
19
|
- Thinking Manager
|
|
14
20
|
- Updated tool manager
|
|
15
21
|
|
|
16
|
-
## [0.8.
|
|
22
|
+
## [0.8.12] - 2025-08-18
|
|
17
23
|
- Fix no tool call respoonse in ChatMessage -> Open Ai messages translation
|
|
18
24
|
- Add simple append method to OpenAIMessageBuilder
|
|
19
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: unique_toolkit
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.15
|
|
4
4
|
Summary:
|
|
5
5
|
License: Proprietary
|
|
6
6
|
Author: Martin Fadler
|
|
@@ -114,7 +114,13 @@ All notable changes to this project will be documented in this file.
|
|
|
114
114
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
115
115
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
116
116
|
|
|
117
|
-
## [0.8.
|
|
117
|
+
## [0.8.15] - 2025-08-19
|
|
118
|
+
- Added history loading from database for History Manager
|
|
119
|
+
|
|
120
|
+
## [0.8.14] - 2025-08-19
|
|
121
|
+
- Including GPT-5 series deployed via LiteLLM into language model info
|
|
122
|
+
|
|
123
|
+
## [0.8.13] - 2025-08-18
|
|
118
124
|
- Adding initial versions of
|
|
119
125
|
- Evaluation Manager
|
|
120
126
|
- History Manager
|
|
@@ -122,7 +128,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
122
128
|
- Thinking Manager
|
|
123
129
|
- Updated tool manager
|
|
124
130
|
|
|
125
|
-
## [0.8.
|
|
131
|
+
## [0.8.12] - 2025-08-18
|
|
126
132
|
- Fix no tool call respoonse in ChatMessage -> Open Ai messages translation
|
|
127
133
|
- Add simple append method to OpenAIMessageBuilder
|
|
128
134
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from unique_toolkit.language_model.infos import LanguageModelName
|
|
2
|
+
|
|
3
|
+
DEFAULT_GPT_35_TURBO = LanguageModelName.AZURE_GPT_35_TURBO_0125
|
|
4
|
+
DEFAULT_GPT_4o = LanguageModelName.AZURE_GPT_4o_2024_1120
|
|
5
|
+
DEFAULT_GPT_4o_STRUCTURED_OUTPUT = LanguageModelName.AZURE_GPT_4o_2024_0806
|
|
6
|
+
DEFAULT_GPT_4o_MINI = LanguageModelName.AZURE_GPT_4o_MINI_2024_0718
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import math
|
|
3
|
+
import re
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
|
|
7
|
+
from PIL import Image
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DetailLevel(Enum):
|
|
11
|
+
LOW = "low"
|
|
12
|
+
HIGH = "high"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# https://platform.openai.com/docs/guides/vision/calculating-costs#calculating-costs
|
|
16
|
+
def calculate_image_tokens(width, height, detail: DetailLevel):
|
|
17
|
+
"""
|
|
18
|
+
Calculate the token cost of an image based on its dimensions and detail level.
|
|
19
|
+
NOTE: While we followed the documentation provided by openai to calculate image token cost, in practice,
|
|
20
|
+
we notice that this function overestimate the number of tokens consumed by the model.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
- width (int): The width of the image in pixels.
|
|
24
|
+
- height (int): The height of the image in pixels.
|
|
25
|
+
- detail (str): The detail level, either "low" or "high".
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
- int: The token cost of the image.
|
|
29
|
+
"""
|
|
30
|
+
# Base cost for low detail
|
|
31
|
+
if detail == DetailLevel.LOW:
|
|
32
|
+
return 85
|
|
33
|
+
|
|
34
|
+
# Scaling for high detail
|
|
35
|
+
# Scale down to fit within 2048x2048 square
|
|
36
|
+
max_long_dim = 2048
|
|
37
|
+
long_dim = max(width, height)
|
|
38
|
+
if long_dim > max_long_dim:
|
|
39
|
+
scale_factor = long_dim / max_long_dim
|
|
40
|
+
width = int(width / scale_factor)
|
|
41
|
+
height = int(height / scale_factor)
|
|
42
|
+
|
|
43
|
+
# Scale down the shortest side to 768
|
|
44
|
+
max_short_dim = 768
|
|
45
|
+
short_dim = min(width, height)
|
|
46
|
+
if short_dim > max_short_dim:
|
|
47
|
+
scale_factor = short_dim / max_short_dim
|
|
48
|
+
width = int(width / scale_factor)
|
|
49
|
+
height = int(height / scale_factor)
|
|
50
|
+
|
|
51
|
+
# Step 3: Calculate the number of 512x512 tiles
|
|
52
|
+
tiles = math.ceil(width / 512) * math.ceil(height / 512)
|
|
53
|
+
# Step 4: Compute token cost
|
|
54
|
+
token_cost = (tiles * 170) + 85
|
|
55
|
+
return token_cost
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def calculate_image_tokens_from_base64(base64_string: str):
|
|
59
|
+
base64_string = remove_base64_header(base64_string)
|
|
60
|
+
image = Image.open(BytesIO(base64.b64decode(base64_string)))
|
|
61
|
+
# DETAIL LEVEL HIGH IS THE DEFAULT TO BE ON THE SAFE SIDE
|
|
62
|
+
return calculate_image_tokens(image.width, image.height, DetailLevel.HIGH)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def remove_base64_header(base64_string: str):
|
|
66
|
+
header_pattern = r"^data:image/\w+;base64,"
|
|
67
|
+
return re.sub(header_pattern, "", base64_string)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Original source
|
|
2
|
+
# https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from unique_toolkit.language_model import (
|
|
9
|
+
LanguageModelMessage,
|
|
10
|
+
LanguageModelMessages,
|
|
11
|
+
LanguageModelName,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from _common.utils.token.image_token_counting import (
|
|
15
|
+
calculate_image_tokens_from_base64,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpecialToolCallingTokens(BaseModel):
|
|
20
|
+
func_init: int = 0
|
|
21
|
+
prop_init: int = 0
|
|
22
|
+
prop_key: int = 0
|
|
23
|
+
enum_init: int = 0
|
|
24
|
+
enum_item: int = 0
|
|
25
|
+
func_end: int = 0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_special_token(model: LanguageModelName) -> SpecialToolCallingTokens:
|
|
29
|
+
special_token = SpecialToolCallingTokens()
|
|
30
|
+
|
|
31
|
+
match model:
|
|
32
|
+
case (
|
|
33
|
+
LanguageModelName.AZURE_GPT_4o_2024_0513
|
|
34
|
+
| LanguageModelName.AZURE_GPT_4o_2024_0806
|
|
35
|
+
| LanguageModelName.AZURE_GPT_4o_MINI_2024_0718
|
|
36
|
+
| LanguageModelName.AZURE_GPT_4o_2024_1120
|
|
37
|
+
):
|
|
38
|
+
special_token.func_init = 7
|
|
39
|
+
special_token.prop_init = 3
|
|
40
|
+
special_token.prop_key = 3
|
|
41
|
+
special_token.enum_init = -3
|
|
42
|
+
special_token.enum_item = 3
|
|
43
|
+
special_token.func_end = 12
|
|
44
|
+
|
|
45
|
+
case (
|
|
46
|
+
LanguageModelName.AZURE_GPT_35_TURBO_0125
|
|
47
|
+
| LanguageModelName.AZURE_GPT_4_0613
|
|
48
|
+
| LanguageModelName.AZURE_GPT_4_32K_0613
|
|
49
|
+
| LanguageModelName.AZURE_GPT_4_TURBO_2024_0409
|
|
50
|
+
):
|
|
51
|
+
special_token.func_init = 10
|
|
52
|
+
special_token.prop_init = 3
|
|
53
|
+
special_token.prop_key = 3
|
|
54
|
+
special_token.enum_init = -3
|
|
55
|
+
special_token.enum_item = 3
|
|
56
|
+
special_token.func_end = 12
|
|
57
|
+
|
|
58
|
+
case _:
|
|
59
|
+
raise NotImplementedError(
|
|
60
|
+
f"""num_tokens_for_tools() is not implemented for model {model}."""
|
|
61
|
+
)
|
|
62
|
+
return special_token
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def num_tokens_per_messages(
|
|
66
|
+
messages: list[dict[str, str]], encode: Callable[[str], list[int]]
|
|
67
|
+
) -> list[int]:
|
|
68
|
+
"""Return the number of tokens used by a list of messages."""
|
|
69
|
+
|
|
70
|
+
num_token_per_message = []
|
|
71
|
+
for message in messages:
|
|
72
|
+
num_tokens = 3 # extra_tokens_per_message
|
|
73
|
+
for key, value in message.items():
|
|
74
|
+
if isinstance(value, str):
|
|
75
|
+
num_tokens += len(encode(value))
|
|
76
|
+
elif isinstance(value, list):
|
|
77
|
+
# NOTE: The result returned by the function below is not 100% accurate.
|
|
78
|
+
num_tokens += handle_message_with_images(value, encode)
|
|
79
|
+
if key == "name":
|
|
80
|
+
num_tokens += 1 # extra_tokens_per_name
|
|
81
|
+
|
|
82
|
+
num_token_per_message.append(num_tokens)
|
|
83
|
+
|
|
84
|
+
return num_token_per_message
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def num_tokens_from_messages(
|
|
88
|
+
messages: list[dict[str, str]], encode: Callable[[str], list[int]]
|
|
89
|
+
) -> int:
|
|
90
|
+
"""Return the number of tokens used by a list of messages."""
|
|
91
|
+
|
|
92
|
+
num_tokens_per_message = num_tokens_per_messages(messages, encode)
|
|
93
|
+
num_tokens = sum(num_tokens_per_message) + 3
|
|
94
|
+
|
|
95
|
+
return num_tokens
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def num_tokens_for_tools(
|
|
99
|
+
functions: list[dict[str, Any]],
|
|
100
|
+
special_token: SpecialToolCallingTokens,
|
|
101
|
+
encode: Callable[[str], list[int]],
|
|
102
|
+
):
|
|
103
|
+
def num_token_function_enum(
|
|
104
|
+
properties: dict[str, Any], encode: Callable[[str], list[int]]
|
|
105
|
+
):
|
|
106
|
+
enum_token_count = 0
|
|
107
|
+
enum_token_count += special_token.enum_init
|
|
108
|
+
for item in properties[key]["enum"]:
|
|
109
|
+
enum_token_count += special_token.enum_item
|
|
110
|
+
enum_token_count += len(encode(item))
|
|
111
|
+
|
|
112
|
+
return enum_token_count
|
|
113
|
+
|
|
114
|
+
func_token_count = 0
|
|
115
|
+
if len(functions) > 0:
|
|
116
|
+
for func in functions:
|
|
117
|
+
func_token_count += special_token.func_init
|
|
118
|
+
function = func.get("function", {})
|
|
119
|
+
func_token_count += len(
|
|
120
|
+
encode(
|
|
121
|
+
function.get("name", "")
|
|
122
|
+
+ ":"
|
|
123
|
+
+ function.get("description", "").rstrip(".").rstrip()
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
if len(function.get("parameters", {}).get("properties", "")) > 0:
|
|
127
|
+
properties = function.get("parameters", {}).get(
|
|
128
|
+
"properties", ""
|
|
129
|
+
)
|
|
130
|
+
func_token_count += special_token.prop_init
|
|
131
|
+
|
|
132
|
+
for key in list(properties.keys()):
|
|
133
|
+
func_token_count += special_token.prop_key
|
|
134
|
+
|
|
135
|
+
if "enum" in properties[key].keys():
|
|
136
|
+
func_token_count += num_token_function_enum(
|
|
137
|
+
properties, encode
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
func_token_count += len(
|
|
141
|
+
encode(
|
|
142
|
+
f"{key}:{properties[key]['type']}:{properties[key]['description'].rstrip('.').rstrip()}"
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
func_token_count += special_token.func_end
|
|
147
|
+
|
|
148
|
+
return func_token_count
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def handle_message_with_images(
|
|
152
|
+
message: list[dict], encode: Callable[[str], list[int]]
|
|
153
|
+
):
|
|
154
|
+
token_count = 0
|
|
155
|
+
for item in message:
|
|
156
|
+
if item.get("type") == "image_url":
|
|
157
|
+
image_url = item.get("imageUrl", {}).get("url")
|
|
158
|
+
if image_url:
|
|
159
|
+
token_count += calculate_image_tokens_from_base64(image_url)
|
|
160
|
+
elif item.get("type") == "text":
|
|
161
|
+
token_count += len(encode(item.get("text", "")))
|
|
162
|
+
return token_count
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def messages_to_openai_messages(
|
|
166
|
+
messages: LanguageModelMessages | list[LanguageModelMessage],
|
|
167
|
+
):
|
|
168
|
+
if isinstance(messages, list):
|
|
169
|
+
messages = LanguageModelMessages(messages)
|
|
170
|
+
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
k: v
|
|
174
|
+
for k, v in m.items()
|
|
175
|
+
if (k in ["content", "role"] and v is not None)
|
|
176
|
+
}
|
|
177
|
+
for m in json.loads(messages.model_dump_json())
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def num_tokens_per_language_model_message(
|
|
182
|
+
messages: LanguageModelMessages | list[LanguageModelMessage],
|
|
183
|
+
encode: Callable[[str], list[int]],
|
|
184
|
+
) -> list[int]:
|
|
185
|
+
return num_tokens_per_messages(
|
|
186
|
+
messages=messages_to_openai_messages(messages), encode=encode
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def num_token_for_language_model_messages(
|
|
191
|
+
messages: LanguageModelMessages | list[LanguageModelMessage],
|
|
192
|
+
encode: Callable[[str], list[int]],
|
|
193
|
+
) -> int:
|
|
194
|
+
return num_tokens_from_messages(
|
|
195
|
+
messages_to_openai_messages(messages), encode
|
|
196
|
+
)
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import mimetypes
|
|
3
|
+
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import tiktoken
|
|
9
|
+
|
|
10
|
+
from pydantic import RootModel
|
|
11
|
+
|
|
12
|
+
from _common.token.token_counting import num_tokens_per_language_model_message
|
|
13
|
+
from chat.service import ChatService
|
|
14
|
+
from content.service import ContentService
|
|
15
|
+
from language_model.schemas import LanguageModelMessages
|
|
16
|
+
from unique_toolkit.app import ChatEventUserMessage
|
|
17
|
+
from unique_toolkit.chat.schemas import ChatMessage
|
|
18
|
+
from unique_toolkit.chat.schemas import ChatMessageRole as ChatRole
|
|
19
|
+
from unique_toolkit.content.schemas import Content
|
|
20
|
+
from unique_toolkit.language_model import LanguageModelMessageRole as LLMRole
|
|
21
|
+
from unique_toolkit.language_model.infos import EncoderName
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# TODO: Test this once it moves into the unique toolkit
|
|
26
|
+
|
|
27
|
+
map_chat_llm_message_role = {
|
|
28
|
+
ChatRole.USER: LLMRole.USER,
|
|
29
|
+
ChatRole.ASSISTANT: LLMRole.ASSISTANT,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ImageMimeType(StrEnum):
|
|
34
|
+
JPEG = "image/jpeg"
|
|
35
|
+
PNG = "image/png"
|
|
36
|
+
GIF = "image/gif"
|
|
37
|
+
BMP = "image/bmp"
|
|
38
|
+
WEBP = "image/webp"
|
|
39
|
+
TIFF = "image/tiff"
|
|
40
|
+
SVG = "image/svg+xml"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FileMimeType(StrEnum):
|
|
44
|
+
PDF = "application/pdf"
|
|
45
|
+
DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
46
|
+
DOC = "application/msword"
|
|
47
|
+
XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
48
|
+
XLS = "application/vnd.ms-excel"
|
|
49
|
+
PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
50
|
+
CSV = "text/csv"
|
|
51
|
+
HTML = "text/html"
|
|
52
|
+
MD = "text/markdown"
|
|
53
|
+
TXT = "text/plain"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ChatMessageWithContents(ChatMessage):
|
|
57
|
+
contents: list[Content] = []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChatHistoryWithContent(RootModel):
|
|
61
|
+
root: list[ChatMessageWithContents]
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_chat_history_and_contents(
|
|
65
|
+
cls,
|
|
66
|
+
chat_history: list[ChatMessage],
|
|
67
|
+
chat_contents: list[Content],
|
|
68
|
+
):
|
|
69
|
+
combined = chat_contents + chat_history
|
|
70
|
+
combined.sort(key=lambda x: x.created_at or datetime.min)
|
|
71
|
+
|
|
72
|
+
grouped_elements = []
|
|
73
|
+
content_container = []
|
|
74
|
+
|
|
75
|
+
# Content is collected and added to the next chat message
|
|
76
|
+
for c in combined:
|
|
77
|
+
if isinstance(c, ChatMessage):
|
|
78
|
+
grouped_elements.append(
|
|
79
|
+
ChatMessageWithContents(
|
|
80
|
+
contents=content_container.copy(),
|
|
81
|
+
**c.model_dump(),
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
content_container.clear()
|
|
85
|
+
else:
|
|
86
|
+
content_container.append(c)
|
|
87
|
+
|
|
88
|
+
return cls(root=grouped_elements)
|
|
89
|
+
|
|
90
|
+
def __iter__(self):
|
|
91
|
+
return iter(self.root)
|
|
92
|
+
|
|
93
|
+
def __getitem__(self, item):
|
|
94
|
+
return self.root[item]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_image_content(filename: str) -> bool:
|
|
98
|
+
mimetype, _ = mimetypes.guess_type(filename)
|
|
99
|
+
|
|
100
|
+
if not mimetype:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
return mimetype in ImageMimeType.__members__.values()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def is_file_content(filename: str) -> bool:
|
|
107
|
+
mimetype, _ = mimetypes.guess_type(filename)
|
|
108
|
+
|
|
109
|
+
if not mimetype:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
return mimetype in FileMimeType.__members__.values()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_chat_history_with_contents(
|
|
116
|
+
user_message: ChatEventUserMessage,
|
|
117
|
+
chat_id: str,
|
|
118
|
+
chat_history: list[ChatMessage],
|
|
119
|
+
content_service: ContentService,
|
|
120
|
+
) -> ChatHistoryWithContent:
|
|
121
|
+
last_user_message = ChatMessage(
|
|
122
|
+
id=user_message.id,
|
|
123
|
+
chat_id=chat_id,
|
|
124
|
+
text=user_message.text,
|
|
125
|
+
originalText=user_message.original_text,
|
|
126
|
+
role=ChatRole.USER,
|
|
127
|
+
gpt_request=None,
|
|
128
|
+
created_at=datetime.fromisoformat(user_message.created_at),
|
|
129
|
+
)
|
|
130
|
+
if len(chat_history) > 0 and last_user_message.id == chat_history[-1].id:
|
|
131
|
+
pass
|
|
132
|
+
else:
|
|
133
|
+
chat_history.append(last_user_message)
|
|
134
|
+
|
|
135
|
+
chat_contents = content_service.search_contents(
|
|
136
|
+
where={
|
|
137
|
+
"ownerId": {
|
|
138
|
+
"equals": chat_id,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return ChatHistoryWithContent.from_chat_history_and_contents(
|
|
144
|
+
chat_history,
|
|
145
|
+
chat_contents,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def download_encoded_images(
|
|
150
|
+
contents: list[Content],
|
|
151
|
+
content_service: ContentService,
|
|
152
|
+
chat_id: str,
|
|
153
|
+
) -> list[str]:
|
|
154
|
+
base64_encoded_images = []
|
|
155
|
+
for im in contents:
|
|
156
|
+
if is_image_content(im.key):
|
|
157
|
+
try:
|
|
158
|
+
file_bytes = content_service.download_content_to_bytes(
|
|
159
|
+
content_id=im.id,
|
|
160
|
+
chat_id=chat_id,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
mime_type, _ = mimetypes.guess_type(im.key)
|
|
164
|
+
encoded_string = base64.b64encode(file_bytes).decode("utf-8")
|
|
165
|
+
image_string = f"data:{mime_type};base64," + encoded_string
|
|
166
|
+
base64_encoded_images.append(image_string)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
print(e)
|
|
169
|
+
return base64_encoded_images
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class FileContentSerialization(StrEnum):
|
|
173
|
+
NONE = "none"
|
|
174
|
+
FILE_NAME = "file_name"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ImageContentInclusion(StrEnum):
|
|
178
|
+
NONE = "none"
|
|
179
|
+
ALL = "all"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def file_content_serialization(
|
|
183
|
+
file_contents: list[Content],
|
|
184
|
+
file_content_serialization: FileContentSerialization,
|
|
185
|
+
) -> str:
|
|
186
|
+
match file_content_serialization:
|
|
187
|
+
case FileContentSerialization.NONE:
|
|
188
|
+
return ""
|
|
189
|
+
case FileContentSerialization.FILE_NAME:
|
|
190
|
+
file_names = [
|
|
191
|
+
f"- Uploaded file: {f.key} at {f.created_at}"
|
|
192
|
+
for f in file_contents
|
|
193
|
+
]
|
|
194
|
+
return "\n".join(
|
|
195
|
+
[
|
|
196
|
+
"Files Uploaded to Chat can be accessed by internal search tool if available:\n",
|
|
197
|
+
]
|
|
198
|
+
+ file_names,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_full_history_with_contents(
|
|
203
|
+
user_message: ChatEventUserMessage,
|
|
204
|
+
chat_id: str,
|
|
205
|
+
chat_service: ChatService,
|
|
206
|
+
content_service: ContentService,
|
|
207
|
+
include_images: ImageContentInclusion = ImageContentInclusion.ALL,
|
|
208
|
+
file_content_serialization_type: FileContentSerialization = FileContentSerialization.FILE_NAME,
|
|
209
|
+
) -> LanguageModelMessages:
|
|
210
|
+
grouped_elements = get_chat_history_with_contents(
|
|
211
|
+
user_message=user_message,
|
|
212
|
+
chat_id=chat_id,
|
|
213
|
+
chat_history=chat_service.get_full_history(),
|
|
214
|
+
content_service=content_service,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
builder = LanguageModelMessages([]).builder()
|
|
218
|
+
for c in grouped_elements:
|
|
219
|
+
# LanguageModelUserMessage has not field original content
|
|
220
|
+
text = c.original_content if c.original_content else c.content
|
|
221
|
+
if text is None:
|
|
222
|
+
if c.role == ChatRole.USER:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
"Content or original_content of LanguageModelMessages should exist.",
|
|
225
|
+
)
|
|
226
|
+
text = ""
|
|
227
|
+
|
|
228
|
+
if len(c.contents) > 0:
|
|
229
|
+
file_contents = [
|
|
230
|
+
co for co in c.contents if is_file_content(co.key)
|
|
231
|
+
]
|
|
232
|
+
image_contents = [
|
|
233
|
+
co for co in c.contents if is_image_content(co.key)
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
content = (
|
|
237
|
+
text
|
|
238
|
+
+ "\n\n"
|
|
239
|
+
+ file_content_serialization(
|
|
240
|
+
file_contents,
|
|
241
|
+
file_content_serialization_type,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
content = content.strip()
|
|
245
|
+
|
|
246
|
+
if include_images and len(image_contents) > 0:
|
|
247
|
+
builder.image_message_append(
|
|
248
|
+
content=content,
|
|
249
|
+
images=download_encoded_images(
|
|
250
|
+
contents=image_contents,
|
|
251
|
+
content_service=content_service,
|
|
252
|
+
chat_id=chat_id,
|
|
253
|
+
),
|
|
254
|
+
role=map_chat_llm_message_role[c.role],
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
builder.message_append(
|
|
258
|
+
role=map_chat_llm_message_role[c.role],
|
|
259
|
+
content=content,
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
builder.message_append(
|
|
263
|
+
role=map_chat_llm_message_role[c.role],
|
|
264
|
+
content=text,
|
|
265
|
+
)
|
|
266
|
+
return builder.build()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_full_history_as_llm_messages(
|
|
270
|
+
chat_service: ChatService,
|
|
271
|
+
) -> LanguageModelMessages:
|
|
272
|
+
chat_history = chat_service.get_full_history()
|
|
273
|
+
|
|
274
|
+
map_chat_llm_message_role = {
|
|
275
|
+
ChatRole.USER: LLMRole.USER,
|
|
276
|
+
ChatRole.ASSISTANT: LLMRole.ASSISTANT,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
builder = LanguageModelMessages([]).builder()
|
|
280
|
+
for c in chat_history:
|
|
281
|
+
builder.message_append(
|
|
282
|
+
role=map_chat_llm_message_role[c.role],
|
|
283
|
+
content=c.content or "",
|
|
284
|
+
)
|
|
285
|
+
return builder.build()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def limit_to_token_window(
|
|
290
|
+
messages: LanguageModelMessages,
|
|
291
|
+
token_limit: int,
|
|
292
|
+
encoding_name: EncoderName = EncoderName.O200K_BASE,
|
|
293
|
+
) -> LanguageModelMessages:
|
|
294
|
+
encoder = tiktoken.get_encoding(encoding_name)
|
|
295
|
+
token_per_message_reversed = num_tokens_per_language_model_message(
|
|
296
|
+
messages,
|
|
297
|
+
encode=encoder.encode,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
to_take: list[bool] = (
|
|
301
|
+
np.cumsum(token_per_message_reversed) < token_limit
|
|
302
|
+
).tolist()
|
|
303
|
+
to_take.reverse()
|
|
304
|
+
|
|
305
|
+
return LanguageModelMessages(
|
|
306
|
+
root=[m for m, tt in zip(messages, to_take, strict=False) if tt],
|
|
307
|
+
)
|