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.
@@ -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._chat_service = ChatService(event)
85
- self._content_service = ContentService.from_event(event)
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
- async def get_history(
214
+
215
+ async def get_history_for_model_call(
154
216
  self,
155
- postprocessing_step: Callable[
156
- [list[LanguageModelMessage]], list[LanguageModelMessage]
157
- ]
158
- | None = None,
159
- ) -> list[LanguageModelMessage]:
160
- """
161
- Get the history of the conversation. The function will retrieve a subset of the full history based on the configuration.
162
-
163
- Returns:
164
- list[LanguageModelMessage]: The history
165
- """
166
- # Get uploaded files
167
- uploaded_files = self._content_service.search_content_on_chat(
168
- chat_id=self._chat_service.chat_id
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
- # Get all message history
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.14
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/history_manager.py,sha256=ScC9CrkJCNbxFbWqTO-vpPlCUAUADpJCX2AqUd4Ff04,10104
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=JofRoTcnB-Azj9X10kmhqqTUUyPp4GRRWBmaiybzaUo,3446
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.14.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
94
- unique_toolkit-0.8.14.dist-info/METADATA,sha256=5QadKjdIsrfsgxMGglGAN_qfn916APhtTcQWRc9S52c,27642
95
- unique_toolkit-0.8.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
96
- unique_toolkit-0.8.14.dist-info/RECORD,,
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,,