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.
- unique_toolkit/__init__.py +28 -1
- unique_toolkit/_common/api_calling/human_verification_manager.py +343 -0
- unique_toolkit/_common/base_model_type_attribute.py +303 -0
- unique_toolkit/_common/chunk_relevancy_sorter/config.py +49 -0
- unique_toolkit/_common/chunk_relevancy_sorter/exception.py +5 -0
- unique_toolkit/_common/chunk_relevancy_sorter/schemas.py +46 -0
- unique_toolkit/_common/chunk_relevancy_sorter/service.py +374 -0
- unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py +275 -0
- unique_toolkit/_common/default_language_model.py +12 -0
- unique_toolkit/_common/docx_generator/__init__.py +7 -0
- unique_toolkit/_common/docx_generator/config.py +12 -0
- unique_toolkit/_common/docx_generator/schemas.py +80 -0
- unique_toolkit/_common/docx_generator/service.py +252 -0
- unique_toolkit/_common/docx_generator/template/Doc Template.docx +0 -0
- unique_toolkit/_common/endpoint_builder.py +305 -0
- unique_toolkit/_common/endpoint_requestor.py +430 -0
- unique_toolkit/_common/exception.py +24 -0
- unique_toolkit/_common/feature_flags/schema.py +9 -0
- unique_toolkit/_common/pydantic/rjsf_tags.py +936 -0
- unique_toolkit/_common/pydantic_helpers.py +154 -0
- unique_toolkit/_common/referencing.py +53 -0
- unique_toolkit/_common/string_utilities.py +140 -0
- unique_toolkit/_common/tests/test_referencing.py +521 -0
- unique_toolkit/_common/tests/test_string_utilities.py +506 -0
- unique_toolkit/_common/token/image_token_counting.py +67 -0
- unique_toolkit/_common/token/token_counting.py +204 -0
- unique_toolkit/_common/utils/__init__.py +1 -0
- unique_toolkit/_common/utils/files.py +43 -0
- unique_toolkit/_common/utils/structured_output/__init__.py +1 -0
- unique_toolkit/_common/utils/structured_output/schema.py +5 -0
- unique_toolkit/_common/utils/write_configuration.py +51 -0
- unique_toolkit/_common/validators.py +101 -4
- unique_toolkit/agentic/__init__.py +1 -0
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py +28 -0
- unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py +278 -0
- unique_toolkit/agentic/evaluation/config.py +36 -0
- unique_toolkit/{evaluators → agentic/evaluation}/context_relevancy/prompts.py +25 -0
- unique_toolkit/agentic/evaluation/context_relevancy/schema.py +80 -0
- unique_toolkit/agentic/evaluation/context_relevancy/service.py +273 -0
- unique_toolkit/agentic/evaluation/evaluation_manager.py +218 -0
- unique_toolkit/agentic/evaluation/hallucination/constants.py +61 -0
- unique_toolkit/agentic/evaluation/hallucination/hallucination_evaluation.py +111 -0
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/prompts.py +1 -1
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/service.py +16 -15
- unique_toolkit/{evaluators → agentic/evaluation}/hallucination/utils.py +30 -20
- unique_toolkit/{evaluators → agentic/evaluation}/output_parser.py +20 -2
- unique_toolkit/{evaluators → agentic/evaluation}/schemas.py +27 -7
- unique_toolkit/agentic/evaluation/tests/test_context_relevancy_service.py +253 -0
- unique_toolkit/agentic/evaluation/tests/test_output_parser.py +87 -0
- unique_toolkit/agentic/history_manager/history_construction_with_contents.py +297 -0
- unique_toolkit/agentic/history_manager/history_manager.py +242 -0
- unique_toolkit/agentic/history_manager/loop_token_reducer.py +484 -0
- unique_toolkit/agentic/history_manager/utils.py +96 -0
- unique_toolkit/agentic/postprocessor/postprocessor_manager.py +212 -0
- unique_toolkit/agentic/reference_manager/reference_manager.py +103 -0
- unique_toolkit/agentic/responses_api/__init__.py +19 -0
- unique_toolkit/agentic/responses_api/postprocessors/code_display.py +63 -0
- unique_toolkit/agentic/responses_api/postprocessors/generated_files.py +145 -0
- unique_toolkit/agentic/responses_api/stream_handler.py +15 -0
- unique_toolkit/agentic/short_term_memory_manager/persistent_short_term_memory_manager.py +141 -0
- unique_toolkit/agentic/thinking_manager/thinking_manager.py +103 -0
- unique_toolkit/agentic/tools/__init__.py +1 -0
- unique_toolkit/agentic/tools/a2a/__init__.py +36 -0
- unique_toolkit/agentic/tools/a2a/config.py +17 -0
- unique_toolkit/agentic/tools/a2a/evaluation/__init__.py +15 -0
- unique_toolkit/agentic/tools/a2a/evaluation/_utils.py +66 -0
- unique_toolkit/agentic/tools/a2a/evaluation/config.py +55 -0
- unique_toolkit/agentic/tools/a2a/evaluation/evaluator.py +260 -0
- unique_toolkit/agentic/tools/a2a/evaluation/summarization_user_message.j2 +9 -0
- unique_toolkit/agentic/tools/a2a/manager.py +55 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/__init__.py +21 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_display_utils.py +185 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/_ref_utils.py +73 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/config.py +45 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/display.py +180 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/references.py +101 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_display_utils.py +1335 -0
- unique_toolkit/agentic/tools/a2a/postprocessing/test/test_ref_utils.py +603 -0
- unique_toolkit/agentic/tools/a2a/prompts.py +46 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/__init__.py +6 -0
- unique_toolkit/agentic/tools/a2a/response_watcher/service.py +91 -0
- unique_toolkit/agentic/tools/a2a/tool/__init__.py +4 -0
- unique_toolkit/agentic/tools/a2a/tool/_memory.py +26 -0
- unique_toolkit/agentic/tools/a2a/tool/_schema.py +9 -0
- unique_toolkit/agentic/tools/a2a/tool/config.py +73 -0
- unique_toolkit/agentic/tools/a2a/tool/service.py +306 -0
- unique_toolkit/agentic/tools/agent_chunks_hanlder.py +65 -0
- unique_toolkit/agentic/tools/config.py +167 -0
- unique_toolkit/agentic/tools/factory.py +44 -0
- unique_toolkit/agentic/tools/mcp/__init__.py +4 -0
- unique_toolkit/agentic/tools/mcp/manager.py +71 -0
- unique_toolkit/agentic/tools/mcp/models.py +28 -0
- unique_toolkit/agentic/tools/mcp/tool_wrapper.py +234 -0
- unique_toolkit/agentic/tools/openai_builtin/__init__.py +11 -0
- unique_toolkit/agentic/tools/openai_builtin/base.py +30 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/__init__.py +8 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py +57 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/service.py +230 -0
- unique_toolkit/agentic/tools/openai_builtin/manager.py +62 -0
- unique_toolkit/agentic/tools/schemas.py +141 -0
- unique_toolkit/agentic/tools/test/test_mcp_manager.py +536 -0
- unique_toolkit/agentic/tools/test/test_tool_progress_reporter.py +445 -0
- unique_toolkit/agentic/tools/tool.py +183 -0
- unique_toolkit/agentic/tools/tool_manager.py +523 -0
- unique_toolkit/agentic/tools/tool_progress_reporter.py +285 -0
- unique_toolkit/agentic/tools/utils/__init__.py +19 -0
- unique_toolkit/agentic/tools/utils/execution/__init__.py +1 -0
- unique_toolkit/agentic/tools/utils/execution/execution.py +286 -0
- unique_toolkit/agentic/tools/utils/source_handling/__init__.py +0 -0
- unique_toolkit/agentic/tools/utils/source_handling/schema.py +21 -0
- unique_toolkit/agentic/tools/utils/source_handling/source_formatting.py +207 -0
- unique_toolkit/agentic/tools/utils/source_handling/tests/test_source_formatting.py +216 -0
- unique_toolkit/app/__init__.py +6 -0
- unique_toolkit/app/dev_util.py +180 -0
- unique_toolkit/app/init_sdk.py +32 -1
- unique_toolkit/app/schemas.py +198 -31
- unique_toolkit/app/unique_settings.py +367 -0
- unique_toolkit/chat/__init__.py +8 -1
- unique_toolkit/chat/deprecated/service.py +232 -0
- unique_toolkit/chat/functions.py +642 -77
- unique_toolkit/chat/rendering.py +34 -0
- unique_toolkit/chat/responses_api.py +461 -0
- unique_toolkit/chat/schemas.py +133 -2
- unique_toolkit/chat/service.py +115 -767
- unique_toolkit/content/functions.py +153 -4
- unique_toolkit/content/schemas.py +122 -15
- unique_toolkit/content/service.py +278 -44
- unique_toolkit/content/smart_rules.py +301 -0
- unique_toolkit/content/utils.py +8 -3
- unique_toolkit/embedding/service.py +102 -11
- unique_toolkit/framework_utilities/__init__.py +1 -0
- unique_toolkit/framework_utilities/langchain/client.py +71 -0
- unique_toolkit/framework_utilities/langchain/history.py +19 -0
- unique_toolkit/framework_utilities/openai/__init__.py +6 -0
- unique_toolkit/framework_utilities/openai/client.py +83 -0
- unique_toolkit/framework_utilities/openai/message_builder.py +229 -0
- unique_toolkit/framework_utilities/utils.py +23 -0
- unique_toolkit/language_model/__init__.py +3 -0
- unique_toolkit/language_model/builder.py +27 -11
- unique_toolkit/language_model/default_language_model.py +3 -0
- unique_toolkit/language_model/functions.py +327 -43
- unique_toolkit/language_model/infos.py +992 -50
- unique_toolkit/language_model/reference.py +242 -0
- unique_toolkit/language_model/schemas.py +475 -48
- unique_toolkit/language_model/service.py +228 -27
- unique_toolkit/protocols/support.py +145 -0
- unique_toolkit/services/__init__.py +7 -0
- unique_toolkit/services/chat_service.py +1630 -0
- unique_toolkit/services/knowledge_base.py +861 -0
- unique_toolkit/short_term_memory/service.py +178 -41
- unique_toolkit/smart_rules/__init__.py +0 -0
- unique_toolkit/smart_rules/compile.py +56 -0
- unique_toolkit/test_utilities/events.py +197 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/METADATA +606 -7
- unique_toolkit-1.23.0.dist-info/RECORD +182 -0
- unique_toolkit/evaluators/__init__.py +0 -1
- unique_toolkit/evaluators/config.py +0 -35
- unique_toolkit/evaluators/constants.py +0 -1
- unique_toolkit/evaluators/context_relevancy/constants.py +0 -32
- unique_toolkit/evaluators/context_relevancy/service.py +0 -53
- unique_toolkit/evaluators/context_relevancy/utils.py +0 -142
- unique_toolkit/evaluators/hallucination/constants.py +0 -41
- unique_toolkit-0.7.7.dist-info/RECORD +0 -64
- /unique_toolkit/{evaluators → agentic/evaluation}/exception.py +0 -0
- {unique_toolkit-0.7.7.dist-info → unique_toolkit-1.23.0.dist-info}/LICENSE +0 -0
- {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)
|