unique_toolkit 0.8.14__py3-none-any.whl → 0.8.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unique_toolkit/_common/default_language_model.py +6 -0
- unique_toolkit/_common/token/image_token_counting.py +67 -0
- unique_toolkit/_common/token/token_counting.py +196 -0
- unique_toolkit/history_manager/history_construction_with_contents.py +307 -0
- unique_toolkit/history_manager/history_manager.py +85 -111
- unique_toolkit/history_manager/loop_token_reducer.py +457 -0
- unique_toolkit/reference_manager/reference_manager.py +15 -2
- {unique_toolkit-0.8.14.dist-info → unique_toolkit-0.8.15.dist-info}/METADATA +4 -1
- {unique_toolkit-0.8.14.dist-info → unique_toolkit-0.8.15.dist-info}/RECORD +11 -6
- {unique_toolkit-0.8.14.dist-info → unique_toolkit-0.8.15.dist-info}/LICENSE +0 -0
- {unique_toolkit-0.8.14.dist-info → unique_toolkit-0.8.15.dist-info}/WHEEL +0 -0
|
@@ -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
|
+
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from logging import Logger
|
|
3
|
-
from typing import Awaitable, Callable
|
|
3
|
+
from typing import Annotated, Awaitable, Callable
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
+
import tiktoken
|
|
7
8
|
from unique_toolkit.app.schemas import ChatEvent
|
|
8
9
|
|
|
9
10
|
|
|
@@ -17,17 +18,54 @@ from unique_toolkit.language_model.schemas import (
|
|
|
17
18
|
LanguageModelFunction,
|
|
18
19
|
LanguageModelMessage,
|
|
19
20
|
LanguageModelMessageRole,
|
|
21
|
+
LanguageModelSystemMessage,
|
|
20
22
|
LanguageModelToolMessage,
|
|
21
23
|
LanguageModelUserMessage,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
26
|
from unique_toolkit.tools.schemas import ToolCallResponse
|
|
25
|
-
from unique_toolkit.content.utils import count_tokens
|
|
26
27
|
from unique_toolkit.history_manager.utils import transform_chunks_to_string
|
|
27
28
|
|
|
29
|
+
from _common.validators import LMI
|
|
30
|
+
from history_manager.loop_token_reducer import LoopTokenReducer
|
|
31
|
+
from reference_manager.reference_manager import ReferenceManager
|
|
32
|
+
from tools.config import get_configuration_dict
|
|
33
|
+
|
|
34
|
+
DeactivatedNone = Annotated[
|
|
35
|
+
None,
|
|
36
|
+
Field(title="Deactivated", description="None"),
|
|
37
|
+
]
|
|
28
38
|
|
|
29
39
|
class HistoryManagerConfig(BaseModel):
|
|
30
40
|
|
|
41
|
+
class InputTokenDistributionConfig(BaseModel):
|
|
42
|
+
model_config = get_configuration_dict(frozen=True)
|
|
43
|
+
|
|
44
|
+
percent_for_history: float = Field(
|
|
45
|
+
default=0.6,
|
|
46
|
+
ge=0.0,
|
|
47
|
+
lt=1.0,
|
|
48
|
+
description="The fraction of the max input tokens that will be reserved for the history.",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def max_history_tokens(self, max_input_token: int) -> int:
|
|
52
|
+
return int(self.percent_for_history * max_input_token)
|
|
53
|
+
|
|
54
|
+
class UploadedContentConfig(BaseModel):
|
|
55
|
+
model_config = get_configuration_dict()
|
|
56
|
+
|
|
57
|
+
user_context_window_limit_warning: str = Field(
|
|
58
|
+
default="The uploaded content is too large to fit into the ai model. "
|
|
59
|
+
"Unique AI will search for relevant sections in the material and if needed combine the data with knowledge base content",
|
|
60
|
+
description="Message to show when using the Internal Search instead of upload and chat tool due to context window limit. Jinja template.",
|
|
61
|
+
)
|
|
62
|
+
percent_for_uploaded_content: float = Field(
|
|
63
|
+
default=0.6,
|
|
64
|
+
ge=0.0,
|
|
65
|
+
lt=1.0,
|
|
66
|
+
description="The fraction of the max input tokens that will be reserved for the uploaded content.",
|
|
67
|
+
)
|
|
68
|
+
|
|
31
69
|
class ExperimentalFeatures(BaseModel):
|
|
32
70
|
def __init__(self, full_sources_serialize_dump: bool = False):
|
|
33
71
|
self.full_sources_serialize_dump = full_sources_serialize_dump
|
|
@@ -48,6 +86,20 @@ class HistoryManagerConfig(BaseModel):
|
|
|
48
86
|
description="The maximum number of tokens to keep in the history.",
|
|
49
87
|
)
|
|
50
88
|
|
|
89
|
+
uploaded_content_config: (
|
|
90
|
+
Annotated[
|
|
91
|
+
UploadedContentConfig,
|
|
92
|
+
Field(title="Active"),
|
|
93
|
+
]
|
|
94
|
+
| DeactivatedNone
|
|
95
|
+
) = UploadedContentConfig()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
input_token_distribution: InputTokenDistributionConfig = Field(
|
|
99
|
+
default=InputTokenDistributionConfig(),
|
|
100
|
+
description="Configuration for the input token distribution.",
|
|
101
|
+
)
|
|
102
|
+
|
|
51
103
|
|
|
52
104
|
class HistoryManager:
|
|
53
105
|
"""
|
|
@@ -78,11 +130,20 @@ class HistoryManager:
|
|
|
78
130
|
logger: Logger,
|
|
79
131
|
event: ChatEvent,
|
|
80
132
|
config: HistoryManagerConfig,
|
|
133
|
+
language_model: LMI,
|
|
134
|
+
reference_manager: ReferenceManager,
|
|
81
135
|
):
|
|
82
136
|
self._config = config
|
|
83
137
|
self._logger = logger
|
|
84
|
-
self.
|
|
85
|
-
self.
|
|
138
|
+
self._language_model = language_model
|
|
139
|
+
self._token_reducer = LoopTokenReducer(
|
|
140
|
+
logger=self._logger,
|
|
141
|
+
event=event,
|
|
142
|
+
config=self._config,
|
|
143
|
+
language_model=self._language_model,
|
|
144
|
+
reference_manager=reference_manager,
|
|
145
|
+
)
|
|
146
|
+
|
|
86
147
|
|
|
87
148
|
def has_no_loop_messages(self) -> bool:
|
|
88
149
|
return len(self._loop_history) == 0
|
|
@@ -150,112 +211,25 @@ class HistoryManager:
|
|
|
150
211
|
def add_assistant_message(self, message: LanguageModelAssistantMessage) -> None:
|
|
151
212
|
self._loop_history.append(message)
|
|
152
213
|
|
|
153
|
-
|
|
214
|
+
|
|
215
|
+
async def get_history_for_model_call(
|
|
154
216
|
self,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
) -> list[
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
217
|
+
original_user_message: str,
|
|
218
|
+
rendered_user_message_string: str,
|
|
219
|
+
rendered_system_message_string: str,
|
|
220
|
+
remove_from_text: Callable[[str], Awaitable[str]]
|
|
221
|
+
) -> list[
|
|
222
|
+
LanguageModelMessage
|
|
223
|
+
| LanguageModelToolMessage
|
|
224
|
+
| LanguageModelAssistantMessage
|
|
225
|
+
| LanguageModelSystemMessage
|
|
226
|
+
| LanguageModelUserMessage
|
|
227
|
+
]:
|
|
228
|
+
messages = await self._token_reducer.get_history_for_model_call(
|
|
229
|
+
original_user_message=original_user_message,
|
|
230
|
+
rendered_user_message_string=rendered_user_message_string,
|
|
231
|
+
rendered_system_message_string=rendered_system_message_string,
|
|
232
|
+
loop_history=self._loop_history,
|
|
233
|
+
remove_from_text=remove_from_text,
|
|
169
234
|
)
|
|
170
|
-
|
|
171
|
-
full_history = await self._chat_service.get_full_history_async()
|
|
172
|
-
|
|
173
|
-
merged_history = self._merge_history_and_uploads(full_history, uploaded_files)
|
|
174
|
-
|
|
175
|
-
if postprocessing_step is not None:
|
|
176
|
-
merged_history = postprocessing_step(merged_history)
|
|
177
|
-
|
|
178
|
-
limited_history = self._limit_to_token_window(
|
|
179
|
-
merged_history, self._config.max_history_tokens
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
# Add current user message if not already in history
|
|
183
|
-
# we grab it fresh from the db so it must contain all the messages this code is not needed anymore below currently it's left in for explainability
|
|
184
|
-
# current_user_msg = LanguageModelUserMessage(
|
|
185
|
-
# content=self.event.payload.user_message.text
|
|
186
|
-
# )
|
|
187
|
-
# if not any(
|
|
188
|
-
# msg.role == LanguageModelMessageRole.USER
|
|
189
|
-
# and msg.content == current_user_msg.content
|
|
190
|
-
# for msg in complete_history
|
|
191
|
-
# ):
|
|
192
|
-
# complete_history.append(current_user_msg)
|
|
193
|
-
|
|
194
|
-
# # Add final assistant response - this should be available when this method is called
|
|
195
|
-
# if (
|
|
196
|
-
# hasattr(self, "loop_response")
|
|
197
|
-
# and self.loop_response
|
|
198
|
-
# and self.loop_response.message.text
|
|
199
|
-
# ):
|
|
200
|
-
# complete_history.append(
|
|
201
|
-
# LanguageModelAssistantMessage(
|
|
202
|
-
# content=self.loop_response.message.text
|
|
203
|
-
# )
|
|
204
|
-
# )
|
|
205
|
-
# else:
|
|
206
|
-
# self.logger.warning(
|
|
207
|
-
# "Called get_complete_conversation_history_after_streaming_no_tool_calls but no loop_response.message.text is available"
|
|
208
|
-
# )
|
|
209
|
-
|
|
210
|
-
return limited_history
|
|
211
|
-
|
|
212
|
-
def _merge_history_and_uploads(
|
|
213
|
-
self, history: list[ChatMessage], uploads: list[Content]
|
|
214
|
-
) -> list[LanguageModelMessage]:
|
|
215
|
-
# Assert that all content have a created_at
|
|
216
|
-
content_with_created_at = [content for content in uploads if content.created_at]
|
|
217
|
-
sorted_history = sorted(
|
|
218
|
-
history + content_with_created_at,
|
|
219
|
-
key=lambda x: x.created_at or datetime.min,
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
msg_builder = MessagesBuilder()
|
|
223
|
-
for msg in sorted_history:
|
|
224
|
-
if isinstance(msg, Content):
|
|
225
|
-
msg_builder.user_message_append(
|
|
226
|
-
f"Uploaded file: {msg.key}, ContentId: {msg.id}"
|
|
227
|
-
)
|
|
228
|
-
else:
|
|
229
|
-
msg_builder.messages.append(
|
|
230
|
-
LanguageModelMessage(
|
|
231
|
-
role=LanguageModelMessageRole(msg.role),
|
|
232
|
-
content=msg.content,
|
|
233
|
-
)
|
|
234
|
-
)
|
|
235
|
-
return msg_builder.messages
|
|
236
|
-
|
|
237
|
-
def _limit_to_token_window(
|
|
238
|
-
self, messages: list[LanguageModelMessage], token_limit: int
|
|
239
|
-
) -> list[LanguageModelMessage]:
|
|
240
|
-
selected_messages = []
|
|
241
|
-
token_count = 0
|
|
242
|
-
for msg in messages[::-1]:
|
|
243
|
-
msg_token_count = count_tokens(str(msg.content))
|
|
244
|
-
if token_count + msg_token_count > token_limit:
|
|
245
|
-
break
|
|
246
|
-
selected_messages.append(msg)
|
|
247
|
-
token_count += msg_token_count
|
|
248
|
-
return selected_messages[::-1]
|
|
249
|
-
|
|
250
|
-
async def remove_post_processing_manipulations(
|
|
251
|
-
self, remove_from_text: Callable[[str], Awaitable[str]]
|
|
252
|
-
) -> list[LanguageModelMessage]:
|
|
253
|
-
messages = await self.get_history()
|
|
254
|
-
for message in messages:
|
|
255
|
-
if isinstance(message.content, str):
|
|
256
|
-
message.content = await remove_from_text(message.content)
|
|
257
|
-
else:
|
|
258
|
-
self._logger.warning(
|
|
259
|
-
f"Skipping message with unsupported content type: {type(message.content)}"
|
|
260
|
-
)
|
|
261
|
-
return messages
|
|
235
|
+
return messages.root
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
from logging import Logger
|
|
4
|
+
from typing import Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
import tiktoken
|
|
8
|
+
from _common.token.token_counting import num_token_for_language_model_messages
|
|
9
|
+
from _common.validators import LMI
|
|
10
|
+
from app.schemas import ChatEvent
|
|
11
|
+
from chat.service import ChatService
|
|
12
|
+
from content.schemas import ContentChunk
|
|
13
|
+
from content.service import ContentService
|
|
14
|
+
from history_manager.history_construction_with_contents import FileContentSerialization, get_full_history_with_contents
|
|
15
|
+
from history_manager.history_manager import HistoryManagerConfig
|
|
16
|
+
from language_model.schemas import LanguageModelAssistantMessage, LanguageModelMessage, LanguageModelMessageRole, LanguageModelMessages, LanguageModelSystemMessage, LanguageModelToolMessage, LanguageModelUserMessage
|
|
17
|
+
from reference_manager.reference_manager import ReferenceManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SourceReductionResult(BaseModel):
|
|
21
|
+
message: LanguageModelToolMessage
|
|
22
|
+
reduced_chunks: list[ContentChunk]
|
|
23
|
+
chunk_offset: int
|
|
24
|
+
source_offset: int
|
|
25
|
+
|
|
26
|
+
class Config:
|
|
27
|
+
arbitrary_types_allowed = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LoopTokenReducer():
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
logger: Logger,
|
|
35
|
+
event: ChatEvent,
|
|
36
|
+
config: HistoryManagerConfig,
|
|
37
|
+
reference_manager: ReferenceManager,
|
|
38
|
+
language_model: LMI
|
|
39
|
+
):
|
|
40
|
+
self._config = config
|
|
41
|
+
self._logger = logger
|
|
42
|
+
self._reference_manager = reference_manager
|
|
43
|
+
self._language_model = language_model
|
|
44
|
+
self._encoder = self._get_encoder(language_model)
|
|
45
|
+
self._chat_service = ChatService(event)
|
|
46
|
+
self._content_service = ContentService.from_event(event)
|
|
47
|
+
self._user_message = event.payload.user_message
|
|
48
|
+
self._chat_id = event.payload.chat_id
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_encoder(self, language_model: LMI) -> tiktoken.Encoding:
|
|
52
|
+
name = language_model.name or "cl100k_base"
|
|
53
|
+
return tiktoken.get_encoding(name)
|
|
54
|
+
|
|
55
|
+
async def get_history_for_model_call( self,
|
|
56
|
+
original_user_message: str,
|
|
57
|
+
rendered_user_message_string: str,
|
|
58
|
+
rendered_system_message_string: str,
|
|
59
|
+
loop_history: list[LanguageModelMessage],
|
|
60
|
+
remove_from_text: Callable[[str], Awaitable[str]]
|
|
61
|
+
) -> LanguageModelMessages:
|
|
62
|
+
"""Compose the system and user messages for the plan execution step, which is evaluating if any further tool calls are required."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
messages = await self._construct_history(
|
|
66
|
+
original_user_message,
|
|
67
|
+
rendered_user_message_string,
|
|
68
|
+
rendered_system_message_string,
|
|
69
|
+
loop_history,
|
|
70
|
+
remove_from_text
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
token_count = self._count_message_tokens(messages)
|
|
74
|
+
self._log_token_usage(token_count)
|
|
75
|
+
|
|
76
|
+
while self._exceeds_token_limit(token_count):
|
|
77
|
+
token_count_before_reduction = token_count
|
|
78
|
+
loop_history = self._handle_token_limit_exceeded(loop_history)
|
|
79
|
+
messages = await self._construct_history(
|
|
80
|
+
original_user_message,
|
|
81
|
+
rendered_user_message_string,
|
|
82
|
+
rendered_system_message_string,
|
|
83
|
+
loop_history,
|
|
84
|
+
remove_from_text
|
|
85
|
+
)
|
|
86
|
+
token_count = self._count_message_tokens(messages)
|
|
87
|
+
self._log_token_usage(token_count)
|
|
88
|
+
token_count_after_reduction = token_count
|
|
89
|
+
if token_count_after_reduction >= token_count_before_reduction:
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
return messages
|
|
93
|
+
|
|
94
|
+
def _exceeds_token_limit(self, token_count: int) -> bool:
|
|
95
|
+
"""Check if token count exceeds the maximum allowed limit and if at least one tool call has more than one source."""
|
|
96
|
+
# At least one tool call should have more than one chunk as answer
|
|
97
|
+
has_multiple_chunks_for_a_tool_call = any(
|
|
98
|
+
len(chunks) > 1
|
|
99
|
+
for chunks in self._reference_manager.get_chunks_of_all_tools()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# TODO: This is not fully correct at the moment as the token_count
|
|
103
|
+
# include system_prompt and user question already
|
|
104
|
+
# TODO: There is a problem if we exceed but only have one chunk per tool call
|
|
105
|
+
exceeds_limit = (
|
|
106
|
+
token_count
|
|
107
|
+
> self._language_model.token_limits.token_limit_input
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return has_multiple_chunks_for_a_tool_call and exceeds_limit
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _count_message_tokens(self, messages: LanguageModelMessages) -> int:
|
|
114
|
+
"""Count tokens in messages using the configured encoding model."""
|
|
115
|
+
return num_token_for_language_model_messages(
|
|
116
|
+
messages=messages,
|
|
117
|
+
encode=self._encoder.encode,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _log_token_usage(self, token_count: int) -> None:
|
|
121
|
+
"""Log token usage and update debug info."""
|
|
122
|
+
self._logger.info(f"Token messages: {token_count}")
|
|
123
|
+
# self.agent_debug_info.add("token_messages", token_count)
|
|
124
|
+
|
|
125
|
+
async def _construct_history(
|
|
126
|
+
self,
|
|
127
|
+
original_user_message: str,
|
|
128
|
+
rendered_user_message_string: str,
|
|
129
|
+
rendered_system_message_string: str,
|
|
130
|
+
loop_history: list[LanguageModelMessage],
|
|
131
|
+
remove_from_text: Callable[[str], Awaitable[str]]
|
|
132
|
+
) -> LanguageModelMessages:
|
|
133
|
+
history_from_db = await self._get_history_from_db(remove_from_text)
|
|
134
|
+
history_from_db = self._replace_user_message(history_from_db, original_user_message, rendered_user_message_string)
|
|
135
|
+
system_message = LanguageModelSystemMessage(content=rendered_system_message_string)
|
|
136
|
+
|
|
137
|
+
constructed_history = LanguageModelMessages(
|
|
138
|
+
[system_message] + history_from_db + loop_history,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
return constructed_history
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _handle_token_limit_exceeded(self,loop_history: list[LanguageModelMessage]) -> list[LanguageModelMessage]:
|
|
146
|
+
"""Handle case where token limit is exceeded by reducing sources in tool responses."""
|
|
147
|
+
self._logger.warning(
|
|
148
|
+
f"Length of messages is exceeds limit of {self._language_model.token_limits.token_limit_input} tokens. "
|
|
149
|
+
"Reducing number of sources per tool call.",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return self._reduce_message_length_by_reducing_sources_in_tool_response(loop_history)
|
|
153
|
+
|
|
154
|
+
def _replace_user_message(
|
|
155
|
+
self,
|
|
156
|
+
history: list[LanguageModelMessage],
|
|
157
|
+
original_user_message: str,
|
|
158
|
+
rendered_user_message_string: str,
|
|
159
|
+
) -> list[LanguageModelMessage]:
|
|
160
|
+
"""
|
|
161
|
+
Replaces the original user message in the history with the rendered user message string.
|
|
162
|
+
"""
|
|
163
|
+
if history[-1].role == LanguageModelMessageRole.USER:
|
|
164
|
+
m = history[-1]
|
|
165
|
+
|
|
166
|
+
if isinstance(m.content, list):
|
|
167
|
+
# Replace the last text element but be careful not to delete data added when merging with contents
|
|
168
|
+
for t in reversed(m.content):
|
|
169
|
+
field = t.get("type", "")
|
|
170
|
+
if field == "text" and isinstance(field, dict):
|
|
171
|
+
inner_field = field.get("text", "")
|
|
172
|
+
if isinstance(inner_field, str):
|
|
173
|
+
added_to_message_by_history = inner_field.replace(
|
|
174
|
+
original_user_message,
|
|
175
|
+
"",
|
|
176
|
+
)
|
|
177
|
+
t["text"] = rendered_user_message_string + added_to_message_by_history
|
|
178
|
+
break
|
|
179
|
+
elif m.content:
|
|
180
|
+
added_to_message_by_history = m.content.replace(original_user_message, "")
|
|
181
|
+
m.content = rendered_user_message_string + added_to_message_by_history
|
|
182
|
+
else:
|
|
183
|
+
history = history + [
|
|
184
|
+
LanguageModelUserMessage(content=rendered_user_message_string),
|
|
185
|
+
]
|
|
186
|
+
return history
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def _get_history_from_db(
|
|
190
|
+
self,
|
|
191
|
+
remove_from_text: Callable[[str], Awaitable[str]]
|
|
192
|
+
) -> list[LanguageModelMessage]:
|
|
193
|
+
"""
|
|
194
|
+
Get the history of the conversation. The function will retrieve a subset of the full history based on the configuration.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
list[LanguageModelMessage]: The history
|
|
198
|
+
"""
|
|
199
|
+
full_history = get_full_history_with_contents(
|
|
200
|
+
user_message=self._user_message,
|
|
201
|
+
chat_id=self._chat_id,
|
|
202
|
+
chat_service=self._chat_service,
|
|
203
|
+
content_service=self._content_service,
|
|
204
|
+
file_content_serialization_type=(
|
|
205
|
+
FileContentSerialization.NONE
|
|
206
|
+
if self._config.uploaded_content_config
|
|
207
|
+
else FileContentSerialization.FILE_NAME
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
full_history.root = await self._clean_messages(full_history.root, remove_from_text)
|
|
212
|
+
|
|
213
|
+
limited_history_messages = self._limit_to_token_window(
|
|
214
|
+
full_history.root,
|
|
215
|
+
self._config.input_token_distribution.max_history_tokens(
|
|
216
|
+
self._language_model.token_limits.token_limit_input,
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if len(limited_history_messages) == 0:
|
|
222
|
+
limited_history_messages = full_history.root[-1:]
|
|
223
|
+
|
|
224
|
+
self._logger.info(
|
|
225
|
+
f"Reduced history to {len(limited_history_messages)} messages from {len(full_history.root)}",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return self.ensure_last_message_is_user_message(limited_history_messages)
|
|
229
|
+
|
|
230
|
+
def _limit_to_token_window(
|
|
231
|
+
self, messages: list[LanguageModelMessage], token_limit: int
|
|
232
|
+
) -> list[LanguageModelMessage]:
|
|
233
|
+
selected_messages = []
|
|
234
|
+
token_count = 0
|
|
235
|
+
for msg in messages[::-1]:
|
|
236
|
+
msg_token_count = self._count_tokens(str(msg.content))
|
|
237
|
+
if token_count + msg_token_count > token_limit:
|
|
238
|
+
break
|
|
239
|
+
selected_messages.append(msg)
|
|
240
|
+
token_count += msg_token_count
|
|
241
|
+
return selected_messages[::-1]
|
|
242
|
+
|
|
243
|
+
async def _clean_messages(
|
|
244
|
+
self,
|
|
245
|
+
messages: list[LanguageModelMessage | LanguageModelToolMessage | LanguageModelAssistantMessage | LanguageModelSystemMessage | LanguageModelUserMessage],
|
|
246
|
+
remove_from_text: Callable[[str], Awaitable[str]]
|
|
247
|
+
) -> list[LanguageModelMessage]:
|
|
248
|
+
for message in messages:
|
|
249
|
+
if isinstance(message.content, str):
|
|
250
|
+
message.content = await remove_from_text(message.content)
|
|
251
|
+
else:
|
|
252
|
+
self._logger.warning(
|
|
253
|
+
f"Skipping message with unsupported content type: {type(message.content)}"
|
|
254
|
+
)
|
|
255
|
+
return messages
|
|
256
|
+
|
|
257
|
+
def _count_tokens(self,text:str) -> int:
|
|
258
|
+
|
|
259
|
+
return len(self._encoder.encode(text))
|
|
260
|
+
|
|
261
|
+
def ensure_last_message_is_user_message(self, limited_history_messages):
|
|
262
|
+
"""
|
|
263
|
+
As the token limit can be reached in the middle of a gpt_request,
|
|
264
|
+
we move forward to the next user message,to avoid confusing messages for the LLM
|
|
265
|
+
"""
|
|
266
|
+
idx = 0
|
|
267
|
+
for idx, message in enumerate(limited_history_messages):
|
|
268
|
+
if message.role == LanguageModelMessageRole.USER:
|
|
269
|
+
break
|
|
270
|
+
|
|
271
|
+
# FIXME: This might reduce the history by a lot if we have a lot of tool calls / references in the history. Could make sense to summarize the messages and include
|
|
272
|
+
# FIXME: We should remove chunks no longer in history from handler
|
|
273
|
+
return limited_history_messages[idx:]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _reduce_message_length_by_reducing_sources_in_tool_response(
|
|
277
|
+
self,
|
|
278
|
+
history: list[LanguageModelMessage],
|
|
279
|
+
) -> list[LanguageModelMessage]:
|
|
280
|
+
"""
|
|
281
|
+
Reduce the message length by removing the last source result of each tool call.
|
|
282
|
+
If there is only one source for a tool call, the tool call message is returned unchanged.
|
|
283
|
+
"""
|
|
284
|
+
history_reduced: list[LanguageModelMessage] = []
|
|
285
|
+
content_chunks_reduced: list[ContentChunk] = []
|
|
286
|
+
chunk_offset = 0
|
|
287
|
+
source_offset = 0
|
|
288
|
+
|
|
289
|
+
for message in history:
|
|
290
|
+
if self._should_reduce_message(message):
|
|
291
|
+
result = self._reduce_sources_in_tool_message(
|
|
292
|
+
message, # type: ignore
|
|
293
|
+
chunk_offset,
|
|
294
|
+
source_offset,
|
|
295
|
+
)
|
|
296
|
+
content_chunks_reduced.extend(result.reduced_chunks)
|
|
297
|
+
history_reduced.append(result.message)
|
|
298
|
+
chunk_offset = result.chunk_offset
|
|
299
|
+
source_offset = result.source_offset
|
|
300
|
+
else:
|
|
301
|
+
history_reduced.append(message)
|
|
302
|
+
|
|
303
|
+
self._reference_manager.replace(chunks=content_chunks_reduced)
|
|
304
|
+
return history_reduced
|
|
305
|
+
|
|
306
|
+
def _should_reduce_message(self, message: LanguageModelMessage) -> bool:
|
|
307
|
+
"""Determine if a message should have its sources reduced."""
|
|
308
|
+
return (
|
|
309
|
+
message.role == LanguageModelMessageRole.TOOL
|
|
310
|
+
and isinstance(message, LanguageModelToolMessage)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _reduce_sources_in_tool_message(
|
|
315
|
+
self,
|
|
316
|
+
message: LanguageModelToolMessage,
|
|
317
|
+
chunk_offset: int,
|
|
318
|
+
source_offset: int,
|
|
319
|
+
) -> SourceReductionResult:
|
|
320
|
+
"""
|
|
321
|
+
Reduce the sources in the tool message by removing the last source.
|
|
322
|
+
If there is only one source, the message is returned unchanged.
|
|
323
|
+
"""
|
|
324
|
+
tool_chunks = self._reference_manager.get_chunks_of_tool(message.tool_call_id)
|
|
325
|
+
num_sources = len(tool_chunks)
|
|
326
|
+
|
|
327
|
+
if num_sources == 0:
|
|
328
|
+
return SourceReductionResult(
|
|
329
|
+
message=message,
|
|
330
|
+
reduced_chunks=[],
|
|
331
|
+
chunk_offset=chunk_offset,
|
|
332
|
+
source_offset=source_offset,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Reduce chunks, keeping all but the last one if multiple exist
|
|
336
|
+
if num_sources == 1:
|
|
337
|
+
reduced_chunks = tool_chunks
|
|
338
|
+
content_chunks_reduced = self._reference_manager.get_chunks()[
|
|
339
|
+
chunk_offset : chunk_offset + num_sources
|
|
340
|
+
]
|
|
341
|
+
else:
|
|
342
|
+
reduced_chunks = tool_chunks[:-1]
|
|
343
|
+
content_chunks_reduced = self._reference_manager.get_chunks()[
|
|
344
|
+
chunk_offset : chunk_offset + num_sources - 1
|
|
345
|
+
]
|
|
346
|
+
self._reference_manager.replace_chunks_of_tool(
|
|
347
|
+
message.tool_call_id,
|
|
348
|
+
reduced_chunks
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Create new message with reduced sources
|
|
352
|
+
new_message = self._create_tool_call_message_with_reduced_sources(
|
|
353
|
+
message=message,
|
|
354
|
+
content_chunks=reduced_chunks,
|
|
355
|
+
source_offset=source_offset,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return SourceReductionResult(
|
|
359
|
+
message=new_message,
|
|
360
|
+
reduced_chunks=content_chunks_reduced,
|
|
361
|
+
chunk_offset=chunk_offset + num_sources,
|
|
362
|
+
source_offset=source_offset
|
|
363
|
+
+ num_sources
|
|
364
|
+
- (1 if num_sources != 1 else 0),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def _create_tool_call_message_with_reduced_sources(
|
|
368
|
+
self,
|
|
369
|
+
message: LanguageModelToolMessage,
|
|
370
|
+
content_chunks: list[ContentChunk] | None = None,
|
|
371
|
+
source_offset: int = 0,
|
|
372
|
+
) -> LanguageModelToolMessage:
|
|
373
|
+
# Handle special case for TableSearch tool
|
|
374
|
+
if message.name == "TableSearch":
|
|
375
|
+
return self._create_reduced_table_search_message(
|
|
376
|
+
message, content_chunks, source_offset
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Handle empty content case
|
|
380
|
+
if not content_chunks:
|
|
381
|
+
return self._create_reduced_empty_sources_message(message)
|
|
382
|
+
|
|
383
|
+
# Handle standard content chunks
|
|
384
|
+
return self._create_reduced_standard_sources_message(
|
|
385
|
+
message, content_chunks, source_offset
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
def _create_reduced_table_search_message(
|
|
389
|
+
self,
|
|
390
|
+
message: LanguageModelToolMessage,
|
|
391
|
+
content_chunks: list[ContentChunk] | None,
|
|
392
|
+
source_offset: int,
|
|
393
|
+
) -> LanguageModelToolMessage:
|
|
394
|
+
"""
|
|
395
|
+
Create a message for TableSearch tool.
|
|
396
|
+
|
|
397
|
+
Note: TableSearch content consists of a single result with SQL results,
|
|
398
|
+
not content chunks.
|
|
399
|
+
"""
|
|
400
|
+
if not content_chunks:
|
|
401
|
+
content = message.content
|
|
402
|
+
else:
|
|
403
|
+
if isinstance(message.content, str):
|
|
404
|
+
content_dict = json.loads(message.content)
|
|
405
|
+
elif isinstance(message.content, dict):
|
|
406
|
+
content_dict = message.content
|
|
407
|
+
else:
|
|
408
|
+
raise ValueError(
|
|
409
|
+
f"Unexpected content type: {type(message.content)}"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
content = json.dumps(
|
|
413
|
+
{
|
|
414
|
+
"source_number": source_offset,
|
|
415
|
+
"content": content_dict.get("content"),
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return LanguageModelToolMessage(
|
|
420
|
+
content=content,
|
|
421
|
+
tool_call_id=message.tool_call_id,
|
|
422
|
+
name=message.name,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _create_reduced_empty_sources_message(
|
|
427
|
+
self,
|
|
428
|
+
message: LanguageModelToolMessage,
|
|
429
|
+
) -> LanguageModelToolMessage:
|
|
430
|
+
"""Create a message when no content chunks are available."""
|
|
431
|
+
return LanguageModelToolMessage(
|
|
432
|
+
content="No relevant sources found.",
|
|
433
|
+
tool_call_id=message.tool_call_id,
|
|
434
|
+
name=message.name,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _create_reduced_standard_sources_message(
|
|
439
|
+
self,
|
|
440
|
+
message: LanguageModelToolMessage,
|
|
441
|
+
content_chunks: list[ContentChunk],
|
|
442
|
+
source_offset: int,
|
|
443
|
+
) -> LanguageModelToolMessage:
|
|
444
|
+
"""Create a message with standard content chunks."""
|
|
445
|
+
sources = [
|
|
446
|
+
{
|
|
447
|
+
"source_number": source_offset + i,
|
|
448
|
+
"content": chunk.text,
|
|
449
|
+
}
|
|
450
|
+
for i, chunk in enumerate(content_chunks)
|
|
451
|
+
]
|
|
452
|
+
|
|
453
|
+
return LanguageModelToolMessage(
|
|
454
|
+
content=str(sources),
|
|
455
|
+
tool_call_id=message.tool_call_id,
|
|
456
|
+
name=message.name,
|
|
457
|
+
)
|
|
@@ -3,7 +3,7 @@ from unique_toolkit.tools.schemas import ToolCallResponse
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class tool_chunks:
|
|
6
|
-
def __init__(self, name: str, chunks: list) -> None:
|
|
6
|
+
def __init__(self, name: str, chunks: list[ContentChunk]) -> None:
|
|
7
7
|
self.name = name
|
|
8
8
|
self.chunks = chunks
|
|
9
9
|
|
|
@@ -47,8 +47,21 @@ class ReferenceManager:
|
|
|
47
47
|
def get_chunks(self) -> list[ContentChunk]:
|
|
48
48
|
return self._chunks
|
|
49
49
|
|
|
50
|
-
def get_tool_chunks(self) -> dict:
|
|
50
|
+
def get_tool_chunks(self) -> dict[str, tool_chunks]:
|
|
51
51
|
return self._tool_chunks
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_chunks_of_all_tools(self) -> list[list[ContentChunk]]:
|
|
55
|
+
return [tool_chunks.chunks for tool_chunks in self._tool_chunks.values()]
|
|
56
|
+
|
|
57
|
+
def get_chunks_of_tool(self, tool_call_id: str) -> list[ContentChunk]:
|
|
58
|
+
return self._tool_chunks.get(tool_call_id, tool_chunks("", [])).chunks
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def replace_chunks_of_tool(self, tool_call_id: str,chunks: list[ContentChunk]) -> None:
|
|
62
|
+
if tool_call_id in self._tool_chunks:
|
|
63
|
+
self._tool_chunks[tool_call_id].chunks = chunks
|
|
64
|
+
|
|
52
65
|
|
|
53
66
|
def replace(self, chunks: list[ContentChunk]):
|
|
54
67
|
self._chunks = chunks
|
|
@@ -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,6 +114,9 @@ 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.15] - 2025-08-19
|
|
118
|
+
- Added history loading from database for History Manager
|
|
119
|
+
|
|
117
120
|
## [0.8.14] - 2025-08-19
|
|
118
121
|
- Including GPT-5 series deployed via LiteLLM into language model info
|
|
119
122
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
unique_toolkit/__init__.py,sha256=waK7W0EK3v2RJ26hawccwVz1i3yHGvHIIu5qgGjEGHQ,583
|
|
2
2
|
unique_toolkit/_common/_base_service.py,sha256=S8H0rAebx7GsOldA7xInLp3aQJt9yEPDQdsGSFRJsGg,276
|
|
3
3
|
unique_toolkit/_common/_time_utils.py,sha256=ztmTovTvr-3w71Ns2VwXC65OKUUh-sQlzbHdKTQWm-w,135
|
|
4
|
+
unique_toolkit/_common/default_language_model.py,sha256=M6OiVfpi21CixfgYFigOcJGqG8r987f2rxHnn0NZ2dc,333
|
|
4
5
|
unique_toolkit/_common/exception.py,sha256=caQIE1btsQnpKCHqL2cgWUSbHup06enQu_Pt7uGUTTE,727
|
|
6
|
+
unique_toolkit/_common/token/image_token_counting.py,sha256=VpFfZyY0GIH27q_Wy4YNjk2algqvbCtJyzuuROoFQPw,2189
|
|
7
|
+
unique_toolkit/_common/token/token_counting.py,sha256=l8tDo5EaD5FIlKz7Zd6CTNYwMhF-UZ2S3Hb-pU5z2UY,6281
|
|
5
8
|
unique_toolkit/_common/validate_required_values.py,sha256=Y_M1ub9gIKP9qZ45F6Zq3ZHtuIqhmOjl8Z2Vd3avg8w,588
|
|
6
9
|
unique_toolkit/_common/validators.py,sha256=uPGPkeygNi3KimWZxKOKYFxwpCxTkhhYBAn-b_5TS_M,2584
|
|
7
10
|
unique_toolkit/app/__init__.py,sha256=ETxYDpEizg_PKmi4JPX_P76ySq-us-xypfAIdKQ1QZU,1284
|
|
@@ -54,7 +57,9 @@ unique_toolkit/framework_utilities/langchain/history.py,sha256=R9RuCeSFNaUO3OZ0G
|
|
|
54
57
|
unique_toolkit/framework_utilities/openai/client.py,sha256=IasxPXlVJHIsZdXHin7yq-5tO4RNLUu9cEuhrgb4ghE,1205
|
|
55
58
|
unique_toolkit/framework_utilities/openai/message_builder.py,sha256=VU6mJm_upLcarJQKFft_t1RlLRncWDxDuLC5LIJ5lQQ,4339
|
|
56
59
|
unique_toolkit/framework_utilities/utils.py,sha256=JK7g2yMfEx3eMprug26769xqNpS5WJcizf8n2zWMBng,789
|
|
57
|
-
unique_toolkit/history_manager/
|
|
60
|
+
unique_toolkit/history_manager/history_construction_with_contents.py,sha256=xKUVnJ4ZJq4-nnO2_35dbDh9d-zfCJfRzuj7v9hXUdM,9049
|
|
61
|
+
unique_toolkit/history_manager/history_manager.py,sha256=ULtsC7cGl92G2fXKIkEajH3tIy_qqWKIK8FudpNKhu4,8834
|
|
62
|
+
unique_toolkit/history_manager/loop_token_reducer.py,sha256=-7Ezk3OLUsrU0Jd9Qc73_PBJZIayz7bVE3awc-q6Se0,17624
|
|
58
63
|
unique_toolkit/history_manager/utils.py,sha256=3GT53SfOQ7g-dN3PHFIPaAab74sUfV28hbUtGMdX-bY,5607
|
|
59
64
|
unique_toolkit/language_model/__init__.py,sha256=lRQyLlbwHbNFf4-0foBU13UGb09lwEeodbVsfsSgaCk,1971
|
|
60
65
|
unique_toolkit/language_model/builder.py,sha256=4OKfwJfj3TrgO1ezc_ewIue6W7BCQ2ZYQXUckWVPPTA,3369
|
|
@@ -68,7 +73,7 @@ unique_toolkit/language_model/service.py,sha256=N_I3VtK5B0G8s5c6TcBVWM7CcLGqakDh
|
|
|
68
73
|
unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
|
|
69
74
|
unique_toolkit/postprocessor/postprocessor_manager.py,sha256=68TAcXMU_ohWOtzo91LntY950HV9I9gGU92-V0Mxmr8,4239
|
|
70
75
|
unique_toolkit/protocols/support.py,sha256=V15WEIFKVMyF1QCnR8vIi4GrJy4dfTCB6d6JlqPZ58o,2341
|
|
71
|
-
unique_toolkit/reference_manager/reference_manager.py,sha256=
|
|
76
|
+
unique_toolkit/reference_manager/reference_manager.py,sha256=WIvZkRgQztkY0zNTM_KIPSqJFT22HIGNexJ4yG3aj5E,3993
|
|
72
77
|
unique_toolkit/short_term_memory/__init__.py,sha256=2mI3AUrffgH7Yt-xS57EGqnHf7jnn6xquoKEhJqk3Wg,185
|
|
73
78
|
unique_toolkit/short_term_memory/constants.py,sha256=698CL6-wjup2MvU19RxSmQk3gX7aqW_OOpZB7sbz_Xg,34
|
|
74
79
|
unique_toolkit/short_term_memory/functions.py,sha256=3WiK-xatY5nh4Dr5zlDUye1k3E6kr41RiscwtTplw5k,4484
|
|
@@ -90,7 +95,7 @@ unique_toolkit/tools/utils/execution/execution.py,sha256=vjG2Y6awsGNtlvyQAGCTthQ
|
|
|
90
95
|
unique_toolkit/tools/utils/source_handling/schema.py,sha256=pvNhtL2daDLpCVIQpfdn6R35GvKmITVLXjZNLAwpgUE,871
|
|
91
96
|
unique_toolkit/tools/utils/source_handling/source_formatting.py,sha256=C7uayNbdkNVJdEARA5CENnHtNY1SU6etlaqbgHNyxaQ,9152
|
|
92
97
|
unique_toolkit/tools/utils/source_handling/tests/test_source_formatting.py,sha256=zu3AJnYH9CMqZPrxKEH3IgI-fM3nlvIBuspJG6W6B18,6978
|
|
93
|
-
unique_toolkit-0.8.
|
|
94
|
-
unique_toolkit-0.8.
|
|
95
|
-
unique_toolkit-0.8.
|
|
96
|
-
unique_toolkit-0.8.
|
|
98
|
+
unique_toolkit-0.8.15.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
|
|
99
|
+
unique_toolkit-0.8.15.dist-info/METADATA,sha256=HC0kUwceqxR_BLx4SUd7-mFMLZ4roX2mIxpTRy19xnE,27726
|
|
100
|
+
unique_toolkit-0.8.15.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
101
|
+
unique_toolkit-0.8.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|