unique_toolkit 0.7.7__py3-none-any.whl → 1.23.0__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.

Potentially problematic release.


This version of unique_toolkit might be problematic. Click here for more details.

Files changed (166) hide show
  1. unique_toolkit/__init__.py +28 -1
  2. unique_toolkit/_common/api_calling/human_verification_manager.py +343 -0
  3. unique_toolkit/_common/base_model_type_attribute.py +303 -0
  4. unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
  5. unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
  6. unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
  7. unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
  8. unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
  9. unique_toolkit/_common/default_language_model.py +12 -0
  10. unique_toolkit/_common/docx_generator/__init__.py +7 -0
  11. unique_toolkit/_common/docx_generator/config.py +12 -0
  12. unique_toolkit/_common/docx_generator/schemas.py +80 -0
  13. unique_toolkit/_common/docx_generator/service.py +252 -0
  14. unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
  15. unique_toolkit/_common/endpoint_builder.py +305 -0
  16. unique_toolkit/_common/endpoint_requestor.py +430 -0
  17. unique_toolkit/_common/exception.py +24 -0
  18. unique_toolkit/_common/feature_flags/schema.py +9 -0
  19. unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
  20. unique_toolkit/_common/pydantic_helpers.py +154 -0
  21. unique_toolkit/_common/referencing.py +53 -0
  22. unique_toolkit/_common/string_utilities.py +140 -0
  23. unique_toolkit/_common/tests/test_referencing.py +521 -0
  24. unique_toolkit/_common/tests/test_string_utilities.py +506 -0
  25. unique_toolkit/_common/token/image_token_counting.py +67 -0
  26. unique_toolkit/_common/token/token_counting.py +204 -0
  27. unique_toolkit/_common/utils/__init__.py +1 -0
  28. unique_toolkit/_common/utils/files.py +43 -0
  29. unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
  30. unique_toolkit/_common/utils/structured_output/schema.py +5 -0
  31. unique_toolkit/_common/utils/write_configuration.py +51 -0
  32. unique_toolkit/_common/validators.py +101 -4
  33. unique_toolkit/agentic/__init__.py +1 -0
  34. unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
  35. unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
  36. unique_toolkit/agentic/evaluation/config.py +36 -0
  37. unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
  38. unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
  39. unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
  40. unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
  41. unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
  42. unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
  43. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
  44. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
  45. unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
  46. unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
  47. unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
  48. unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
  49. unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
  50. unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
  51. unique_toolkit/agentic/history_manager/history_manager.py +242 -0
  52. unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
  53. unique_toolkit/agentic/history_manager/utils.py +96 -0
  54. unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
  55. unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
  56. unique_toolkit/agentic/responses_api/__init__.py +19 -0
  57. unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
  58. unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
  59. unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
  60. unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
  61. unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
  62. unique_toolkit/agentic/tools/__init__.py +1 -0
  63. unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
  64. unique_toolkit/agentic/tools/a2a/config.py +17 -0
  65. unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
  66. unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
  67. unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
  68. unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
  69. unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
  70. unique_toolkit/agentic/tools/a2a/manager.py +55 -0
  71. unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
  72. unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
  73. unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
  74. unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
  75. unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
  76. unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
  77. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
  78. unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
  79. unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
  80. unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
  81. unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
  82. unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
  83. unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
  84. unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
  85. unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
  86. unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
  87. unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
  88. unique_toolkit/agentic/tools/config.py +167 -0
  89. unique_toolkit/agentic/tools/factory.py +44 -0
  90. unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
  91. unique_toolkit/agentic/tools/mcp/manager.py +71 -0
  92. unique_toolkit/agentic/tools/mcp/models.py +28 -0
  93. unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
  94. unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
  95. unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
  96. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
  97. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
  98. unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
  99. unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
  100. unique_toolkit/agentic/tools/schemas.py +141 -0
  101. unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
  102. unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
  103. unique_toolkit/agentic/tools/tool.py +183 -0
  104. unique_toolkit/agentic/tools/tool_manager.py +523 -0
  105. unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
  106. unique_toolkit/agentic/tools/utils/__init__.py +19 -0
  107. unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
  108. unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
  109. unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
  110. unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
  111. unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
  112. unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
  113. unique_toolkit/app/__init__.py +6 -0
  114. unique_toolkit/app/dev_util.py +180 -0
  115. unique_toolkit/app/init_sdk.py +32 -1
  116. unique_toolkit/app/schemas.py +198 -31
  117. unique_toolkit/app/unique_settings.py +367 -0
  118. unique_toolkit/chat/__init__.py +8 -1
  119. unique_toolkit/chat/deprecated/service.py +232 -0
  120. unique_toolkit/chat/functions.py +642 -77
  121. unique_toolkit/chat/rendering.py +34 -0
  122. unique_toolkit/chat/responses_api.py +461 -0
  123. unique_toolkit/chat/schemas.py +133 -2
  124. unique_toolkit/chat/service.py +115 -767
  125. unique_toolkit/content/functions.py +153 -4
  126. unique_toolkit/content/schemas.py +122 -15
  127. unique_toolkit/content/service.py +278 -44
  128. unique_toolkit/content/smart_rules.py +301 -0
  129. unique_toolkit/content/utils.py +8 -3
  130. unique_toolkit/embedding/service.py +102 -11
  131. unique_toolkit/framework_utilities/__init__.py +1 -0
  132. unique_toolkit/framework_utilities/langchain/client.py +71 -0
  133. unique_toolkit/framework_utilities/langchain/history.py +19 -0
  134. unique_toolkit/framework_utilities/openai/__init__.py +6 -0
  135. unique_toolkit/framework_utilities/openai/client.py +83 -0
  136. unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
  137. unique_toolkit/framework_utilities/utils.py +23 -0
  138. unique_toolkit/language_model/__init__.py +3 -0
  139. unique_toolkit/language_model/builder.py +27 -11
  140. unique_toolkit/language_model/default_language_model.py +3 -0
  141. unique_toolkit/language_model/functions.py +327 -43
  142. unique_toolkit/language_model/infos.py +992 -50
  143. unique_toolkit/language_model/reference.py +242 -0
  144. unique_toolkit/language_model/schemas.py +475 -48
  145. unique_toolkit/language_model/service.py +228 -27
  146. unique_toolkit/protocols/support.py +145 -0
  147. unique_toolkit/services/__init__.py +7 -0
  148. unique_toolkit/services/chat_service.py +1630 -0
  149. unique_toolkit/services/knowledge_base.py +861 -0
  150. unique_toolkit/short_term_memory/service.py +178 -41
  151. unique_toolkit/smart_rules/__init__.py +0 -0
  152. unique_toolkit/smart_rules/compile.py +56 -0
  153. unique_toolkit/test_utilities/events.py +197 -0
  154. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
  155. unique_toolkit-1.23.0.dist-info/RECORD +182 -0
  156. unique_toolkit/evaluators/__init__.py +0 -1
  157. unique_toolkit/evaluators/config.py +0 -35
  158. unique_toolkit/evaluators/constants.py +0 -1
  159. unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
  160. unique_toolkit/evaluators/context_relevancy/service.py +0 -53
  161. unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
  162. unique_toolkit/evaluators/hallucination/constants.py +0 -41
  163. unique_toolkit-0.7.7.dist-info/RECORD +0 -64
  164. /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
  165. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
  166. {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,297 @@
1
+ import base64
2
+ import mimetypes
3
+ from datetime import datetime
4
+ from enum import StrEnum
5
+
6
+ import numpy as np
7
+ import tiktoken
8
+ from pydantic import RootModel
9
+
10
+ from unique_toolkit._common.token.token_counting import (
11
+ num_tokens_per_language_model_message,
12
+ )
13
+ from unique_toolkit.app import ChatEventUserMessage
14
+ from unique_toolkit.chat.schemas import ChatMessage
15
+ from unique_toolkit.chat.schemas import ChatMessageRole as ChatRole
16
+ from unique_toolkit.chat.service import ChatService
17
+ from unique_toolkit.content.schemas import Content
18
+ from unique_toolkit.content.service import ContentService
19
+ from unique_toolkit.language_model import LanguageModelMessageRole as LLMRole
20
+ from unique_toolkit.language_model.infos import EncoderName
21
+ from unique_toolkit.language_model.schemas import LanguageModelMessages
22
+
23
+ # TODO: Test this once it moves into the unique toolkit
24
+
25
+ map_chat_llm_message_role = {
26
+ ChatRole.USER: LLMRole.USER,
27
+ ChatRole.ASSISTANT: LLMRole.ASSISTANT,
28
+ }
29
+
30
+
31
+ class ImageMimeType(StrEnum):
32
+ JPEG = "image/jpeg"
33
+ PNG = "image/png"
34
+ GIF = "image/gif"
35
+ BMP = "image/bmp"
36
+ WEBP = "image/webp"
37
+ TIFF = "image/tiff"
38
+ SVG = "image/svg+xml"
39
+
40
+
41
+ class FileMimeType(StrEnum):
42
+ PDF = "application/pdf"
43
+ DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
44
+ DOC = "application/msword"
45
+ XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
46
+ XLS = "application/vnd.ms-excel"
47
+ PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
48
+ CSV = "text/csv"
49
+ HTML = "text/html"
50
+ MD = "text/markdown"
51
+ TXT = "text/plain"
52
+
53
+
54
+ class ChatMessageWithContents(ChatMessage):
55
+ contents: list[Content] = []
56
+
57
+
58
+ class ChatHistoryWithContent(RootModel):
59
+ root: list[ChatMessageWithContents]
60
+
61
+ @classmethod
62
+ def from_chat_history_and_contents(
63
+ cls,
64
+ chat_history: list[ChatMessage],
65
+ chat_contents: list[Content],
66
+ ):
67
+ combined = chat_contents + chat_history
68
+ combined.sort(key=lambda x: x.created_at or datetime.min)
69
+
70
+ grouped_elements = []
71
+ content_container = []
72
+
73
+ # Content is collected and added to the next chat message
74
+ for c in combined:
75
+ if isinstance(c, ChatMessage):
76
+ grouped_elements.append(
77
+ ChatMessageWithContents(
78
+ contents=content_container.copy(),
79
+ **c.model_dump(),
80
+ ),
81
+ )
82
+ content_container.clear()
83
+ else:
84
+ content_container.append(c)
85
+
86
+ return cls(root=grouped_elements)
87
+
88
+ def __iter__(self):
89
+ return iter(self.root)
90
+
91
+ def __getitem__(self, item):
92
+ return self.root[item]
93
+
94
+
95
+ def is_image_content(filename: str) -> bool:
96
+ mimetype, _ = mimetypes.guess_type(filename)
97
+
98
+ if not mimetype:
99
+ return False
100
+
101
+ return mimetype in ImageMimeType.__members__.values()
102
+
103
+
104
+ def is_file_content(filename: str) -> bool:
105
+ mimetype, _ = mimetypes.guess_type(filename)
106
+
107
+ if not mimetype:
108
+ return False
109
+
110
+ return mimetype in FileMimeType.__members__.values()
111
+
112
+
113
+ def get_chat_history_with_contents(
114
+ user_message: ChatEventUserMessage,
115
+ chat_id: str,
116
+ chat_history: list[ChatMessage],
117
+ content_service: ContentService,
118
+ ) -> ChatHistoryWithContent:
119
+ last_user_message = ChatMessage(
120
+ id=user_message.id,
121
+ chat_id=chat_id,
122
+ text=user_message.text,
123
+ originalText=user_message.original_text,
124
+ role=ChatRole.USER,
125
+ gpt_request=None,
126
+ created_at=datetime.fromisoformat(user_message.created_at),
127
+ )
128
+ if len(chat_history) > 0 and last_user_message.id == chat_history[-1].id:
129
+ pass
130
+ else:
131
+ chat_history.append(last_user_message)
132
+
133
+ chat_contents = content_service.search_contents(
134
+ where={
135
+ "ownerId": {
136
+ "equals": chat_id,
137
+ },
138
+ },
139
+ )
140
+
141
+ return ChatHistoryWithContent.from_chat_history_and_contents(
142
+ chat_history,
143
+ chat_contents,
144
+ )
145
+
146
+
147
+ def download_encoded_images(
148
+ contents: list[Content],
149
+ content_service: ContentService,
150
+ chat_id: str,
151
+ ) -> list[str]:
152
+ base64_encoded_images = []
153
+ for im in contents:
154
+ if is_image_content(im.key):
155
+ try:
156
+ file_bytes = content_service.download_content_to_bytes(
157
+ content_id=im.id,
158
+ chat_id=chat_id,
159
+ )
160
+
161
+ mime_type, _ = mimetypes.guess_type(im.key)
162
+ encoded_string = base64.b64encode(file_bytes).decode("utf-8")
163
+ image_string = f"data:{mime_type};base64," + encoded_string
164
+ base64_encoded_images.append(image_string)
165
+ except Exception as e:
166
+ print(e)
167
+ return base64_encoded_images
168
+
169
+
170
+ class FileContentSerialization(StrEnum):
171
+ NONE = "none"
172
+ FILE_NAME = "file_name"
173
+
174
+
175
+ class ImageContentInclusion(StrEnum):
176
+ NONE = "none"
177
+ ALL = "all"
178
+
179
+
180
+ def file_content_serialization(
181
+ file_contents: list[Content],
182
+ file_content_serialization: FileContentSerialization,
183
+ ) -> str:
184
+ match file_content_serialization:
185
+ case FileContentSerialization.NONE:
186
+ return ""
187
+ case FileContentSerialization.FILE_NAME:
188
+ file_names = [
189
+ f"- Uploaded file: {f.key} at {f.created_at}" for f in file_contents
190
+ ]
191
+ return "\n".join(
192
+ [
193
+ "Files Uploaded to Chat can be accessed by internal search tool if available:\n",
194
+ ]
195
+ + file_names,
196
+ )
197
+
198
+
199
+ def get_full_history_with_contents(
200
+ user_message: ChatEventUserMessage,
201
+ chat_id: str,
202
+ chat_service: ChatService,
203
+ content_service: ContentService,
204
+ include_images: ImageContentInclusion = ImageContentInclusion.ALL,
205
+ file_content_serialization_type: FileContentSerialization = FileContentSerialization.FILE_NAME,
206
+ ) -> LanguageModelMessages:
207
+ grouped_elements = get_chat_history_with_contents(
208
+ user_message=user_message,
209
+ chat_id=chat_id,
210
+ chat_history=chat_service.get_full_history(),
211
+ content_service=content_service,
212
+ )
213
+
214
+ builder = LanguageModelMessages([]).builder()
215
+ for c in grouped_elements:
216
+ # LanguageModelUserMessage has not field original content
217
+ text = c.original_content if c.original_content else c.content
218
+ if text is None:
219
+ if c.role == ChatRole.USER:
220
+ raise ValueError(
221
+ "Content or original_content of LanguageModelMessages should exist.",
222
+ )
223
+ text = ""
224
+
225
+ if len(c.contents) > 0:
226
+ file_contents = [co for co in c.contents if is_file_content(co.key)]
227
+ image_contents = [co for co in c.contents if is_image_content(co.key)]
228
+
229
+ content = (
230
+ text
231
+ + "\n\n"
232
+ + file_content_serialization(
233
+ file_contents,
234
+ file_content_serialization_type,
235
+ )
236
+ )
237
+ content = content.strip()
238
+
239
+ if include_images and len(image_contents) > 0:
240
+ builder.image_message_append(
241
+ content=content,
242
+ images=download_encoded_images(
243
+ contents=image_contents,
244
+ content_service=content_service,
245
+ chat_id=chat_id,
246
+ ),
247
+ role=map_chat_llm_message_role[c.role],
248
+ )
249
+ else:
250
+ builder.message_append(
251
+ role=map_chat_llm_message_role[c.role],
252
+ content=content,
253
+ )
254
+ else:
255
+ builder.message_append(
256
+ role=map_chat_llm_message_role[c.role],
257
+ content=text,
258
+ )
259
+ return builder.build()
260
+
261
+
262
+ def get_full_history_as_llm_messages(
263
+ chat_service: ChatService,
264
+ ) -> LanguageModelMessages:
265
+ chat_history = chat_service.get_full_history()
266
+
267
+ map_chat_llm_message_role = {
268
+ ChatRole.USER: LLMRole.USER,
269
+ ChatRole.ASSISTANT: LLMRole.ASSISTANT,
270
+ }
271
+
272
+ builder = LanguageModelMessages([]).builder()
273
+ for c in chat_history:
274
+ builder.message_append(
275
+ role=map_chat_llm_message_role[c.role],
276
+ content=c.content or "",
277
+ )
278
+ return builder.build()
279
+
280
+
281
+ def limit_to_token_window(
282
+ messages: LanguageModelMessages,
283
+ token_limit: int,
284
+ encoding_name: EncoderName = EncoderName.O200K_BASE,
285
+ ) -> LanguageModelMessages:
286
+ encoder = tiktoken.get_encoding(encoding_name)
287
+ token_per_message_reversed = num_tokens_per_language_model_message(
288
+ messages,
289
+ encode=encoder.encode,
290
+ )
291
+
292
+ to_take: list[bool] = (np.cumsum(token_per_message_reversed) < token_limit).tolist()
293
+ to_take.reverse()
294
+
295
+ return LanguageModelMessages(
296
+ root=[m for m, tt in zip(messages, to_take, strict=False) if tt],
297
+ )
@@ -0,0 +1,242 @@
1
+ from logging import Logger
2
+ from typing import Annotated, Awaitable, Callable
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from unique_toolkit._common.feature_flags.schema import (
7
+ FeatureExtendedSourceSerialization,
8
+ )
9
+ from unique_toolkit._common.validators import LMI
10
+ from unique_toolkit.agentic.history_manager.loop_token_reducer import LoopTokenReducer
11
+ from unique_toolkit.agentic.history_manager.utils import transform_chunks_to_string
12
+ from unique_toolkit.agentic.reference_manager.reference_manager import ReferenceManager
13
+ from unique_toolkit.agentic.tools.config import get_configuration_dict
14
+ from unique_toolkit.agentic.tools.schemas import ToolCallResponse
15
+ from unique_toolkit.app.schemas import ChatEvent
16
+ from unique_toolkit.language_model.default_language_model import DEFAULT_GPT_4o
17
+ from unique_toolkit.language_model.infos import LanguageModelInfo
18
+ from unique_toolkit.language_model.schemas import (
19
+ LanguageModelAssistantMessage,
20
+ LanguageModelFunction,
21
+ LanguageModelMessage,
22
+ LanguageModelMessages,
23
+ LanguageModelToolMessage,
24
+ )
25
+
26
+ DeactivatedNone = Annotated[
27
+ None,
28
+ Field(title="Deactivated", description="None"),
29
+ ]
30
+
31
+
32
+ class UploadedContentConfig(BaseModel):
33
+ model_config = get_configuration_dict()
34
+
35
+ user_context_window_limit_warning: str = Field(
36
+ default="The uploaded content is too large to fit into the ai model. "
37
+ "Unique AI will search for relevant sections in the material and if needed combine the data with knowledge base content",
38
+ description="Message to show when using the Internal Search instead of upload and chat tool due to context window limit. Jinja template.",
39
+ )
40
+ percent_for_uploaded_content: float = Field(
41
+ default=0.6,
42
+ ge=0.0,
43
+ le=1.0,
44
+ description="The fraction of the max input tokens that will be reserved for the uploaded content.",
45
+ )
46
+
47
+
48
+ class ExperimentalFeatures(FeatureExtendedSourceSerialization): ...
49
+
50
+
51
+ class HistoryManagerConfig(BaseModel):
52
+ experimental_features: ExperimentalFeatures = Field(
53
+ default=ExperimentalFeatures(),
54
+ description="Experimental features for the history manager.",
55
+ )
56
+
57
+ percent_of_max_tokens_for_history: float = Field(
58
+ default=0.2,
59
+ ge=0.0,
60
+ lt=1.0,
61
+ description="The fraction of the max input tokens that will be reserved for the history.",
62
+ )
63
+
64
+ language_model: LMI = LanguageModelInfo.from_name(DEFAULT_GPT_4o)
65
+
66
+ @property
67
+ def max_history_tokens(self) -> int:
68
+ return int(
69
+ self.language_model.token_limits.token_limit_input
70
+ * self.percent_of_max_tokens_for_history,
71
+ )
72
+
73
+ uploaded_content_config: (
74
+ Annotated[
75
+ UploadedContentConfig,
76
+ Field(title="Active"),
77
+ ]
78
+ | DeactivatedNone
79
+ ) = UploadedContentConfig()
80
+
81
+
82
+ class HistoryManager:
83
+ """
84
+ Manages the history of tool calls and conversation loops.
85
+
86
+ This class is responsible for:
87
+ - Storing and maintaining the history of tool call results and conversation messages.
88
+ - Merging uploaded content with the conversation history for a unified view.
89
+ - Limiting the history to fit within a configurable token window for efficient processing.
90
+ - Providing methods to retrieve, manipulate, and append to the conversation history.
91
+ - Handling post-processing steps to clean or modify the history as needed.
92
+
93
+ Key Features:
94
+ - Tool Call History: Tracks the results of tool calls and appends them to the conversation history.
95
+ - Loop History: Maintains a record of conversation loops, including assistant and user messages.
96
+ - History Merging: Combines uploaded files and chat messages into a cohesive history.
97
+ - Token Window Management: Ensures the history stays within a specified token limit for optimal performance.
98
+ - Post-Processing Support: Allows for custom transformations or cleanup of the conversation history.
99
+
100
+ The HistoryManager serves as the backbone for managing and retrieving conversation history in a structured and efficient manner.
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ logger: Logger,
106
+ event: ChatEvent,
107
+ config: HistoryManagerConfig,
108
+ language_model: LMI,
109
+ reference_manager: ReferenceManager,
110
+ ):
111
+ self._config = config
112
+ self._logger = logger
113
+ self._language_model = language_model
114
+ self._token_reducer = LoopTokenReducer(
115
+ logger=self._logger,
116
+ event=event,
117
+ max_history_tokens=self._config.max_history_tokens,
118
+ has_uploaded_content_config=bool(self._config.uploaded_content_config),
119
+ language_model=self._language_model,
120
+ reference_manager=reference_manager,
121
+ )
122
+ self._tool_call_result_history: list[ToolCallResponse] = []
123
+ self._tool_calls: list[LanguageModelFunction] = []
124
+ self._loop_history: list[LanguageModelMessage] = []
125
+ self._source_enumerator = 0
126
+
127
+ def add_tool_call(self, tool_call: LanguageModelFunction) -> None:
128
+ self._tool_calls.append(tool_call)
129
+
130
+ def get_tool_calls(self) -> list[LanguageModelFunction]:
131
+ return self._tool_calls
132
+
133
+ def has_no_loop_messages(self) -> bool:
134
+ return len(self._loop_history) == 0
135
+
136
+ def add_tool_call_results(self, tool_call_results: list[ToolCallResponse]):
137
+ for tool_response in tool_call_results:
138
+ if not tool_response.successful:
139
+ self._loop_history.append(
140
+ LanguageModelToolMessage(
141
+ name=tool_response.name,
142
+ tool_call_id=tool_response.id,
143
+ content=f"Tool call {tool_response.name} failed with error: {tool_response.error_message}",
144
+ )
145
+ )
146
+ continue
147
+ self._append_tool_call_result_to_history(tool_response)
148
+
149
+ def _append_tool_call_result_to_history(
150
+ self,
151
+ tool_response: ToolCallResponse,
152
+ ) -> None:
153
+ tool_call_result_for_history = self._get_tool_call_result_for_loop_history(
154
+ tool_response=tool_response
155
+ )
156
+ self._loop_history.append(tool_call_result_for_history)
157
+
158
+ def _get_tool_call_result_for_loop_history(
159
+ self,
160
+ tool_response: ToolCallResponse,
161
+ ) -> LanguageModelMessage:
162
+ self._logger.debug(
163
+ f"Appending tool call result to history: {tool_response.name}"
164
+ )
165
+
166
+ if tool_response.content != "":
167
+ return LanguageModelToolMessage(
168
+ content=tool_response.content,
169
+ tool_call_id=tool_response.id, # type: ignore
170
+ name=tool_response.name,
171
+ )
172
+
173
+ content_chunks = (
174
+ tool_response.content_chunks or []
175
+ ) # it can be that the tool response does not have content chunks
176
+
177
+ # Transform content chunks into sources to be appended to tool result
178
+ stringified_sources, sources = transform_chunks_to_string(
179
+ content_chunks,
180
+ self._source_enumerator,
181
+ )
182
+
183
+ self._source_enumerator += len(
184
+ sources
185
+ ) # To make sure all sources have unique source numbers
186
+
187
+ # Append the result to the history
188
+ return LanguageModelToolMessage(
189
+ content=stringified_sources,
190
+ tool_call_id=tool_response.id, # type: ignore
191
+ name=tool_response.name,
192
+ )
193
+
194
+ def _append_tool_calls_to_history(
195
+ self, tool_calls: list[LanguageModelFunction]
196
+ ) -> None:
197
+ self._loop_history.append(
198
+ LanguageModelAssistantMessage.from_functions(tool_calls=tool_calls)
199
+ )
200
+
201
+ def add_assistant_message(self, message: LanguageModelAssistantMessage) -> None:
202
+ self._loop_history.append(message)
203
+
204
+ async def get_history_for_model_call(
205
+ self,
206
+ original_user_message: str,
207
+ rendered_user_message_string: str,
208
+ rendered_system_message_string: str,
209
+ remove_from_text: Callable[[str], Awaitable[str]],
210
+ ) -> LanguageModelMessages:
211
+ self._logger.info("Getting history for model call -> ")
212
+
213
+ messages = await self._token_reducer.get_history_for_model_call(
214
+ original_user_message=original_user_message,
215
+ rendered_user_message_string=rendered_user_message_string,
216
+ rendered_system_message_string=rendered_system_message_string,
217
+ loop_history=self._loop_history,
218
+ remove_from_text=remove_from_text,
219
+ )
220
+ return messages
221
+
222
+ async def get_user_visible_chat_history(
223
+ self,
224
+ assistant_message_text: str | None = None,
225
+ remove_from_text: Callable[[str], Awaitable[str]] | None = None,
226
+ ) -> LanguageModelMessages:
227
+ """Get the user visible chat history.
228
+
229
+ Args:
230
+ assistant_message_text (str | None): The latest assistant message to append to the history, as this is not extracted from the history.
231
+ If None, the history will be returned without the latest assistant message.
232
+ remove_from_text (Callable[[str], Awaitable[str]] | None): A function to remove text from the history before returning it.
233
+
234
+ Returns:
235
+ LanguageModelMessages: The user visible chat history.
236
+ """
237
+ history = await self._token_reducer.get_history_from_db(remove_from_text)
238
+ if assistant_message_text:
239
+ history.append(
240
+ LanguageModelAssistantMessage(content=assistant_message_text)
241
+ )
242
+ return LanguageModelMessages(history)