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,484 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from logging import Logger
|
|
3
|
+
from typing import Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
import tiktoken
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from unique_toolkit._common.token.token_counting import (
|
|
9
|
+
num_token_for_language_model_messages,
|
|
10
|
+
)
|
|
11
|
+
from unique_toolkit._common.validators import LMI
|
|
12
|
+
from unique_toolkit.agentic.history_manager.history_construction_with_contents import (
|
|
13
|
+
FileContentSerialization,
|
|
14
|
+
get_full_history_with_contents,
|
|
15
|
+
)
|
|
16
|
+
from unique_toolkit.agentic.reference_manager.reference_manager import ReferenceManager
|
|
17
|
+
from unique_toolkit.app.schemas import ChatEvent
|
|
18
|
+
from unique_toolkit.chat.service import ChatService
|
|
19
|
+
from unique_toolkit.content.schemas import ContentChunk
|
|
20
|
+
from unique_toolkit.content.service import ContentService
|
|
21
|
+
from unique_toolkit.language_model.schemas import (
|
|
22
|
+
LanguageModelAssistantMessage,
|
|
23
|
+
LanguageModelMessage,
|
|
24
|
+
LanguageModelMessageRole,
|
|
25
|
+
LanguageModelMessages,
|
|
26
|
+
LanguageModelSystemMessage,
|
|
27
|
+
LanguageModelToolMessage,
|
|
28
|
+
LanguageModelUserMessage,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
MAX_INPUT_TOKENS_SAFETY_PERCENTAGE = (
|
|
32
|
+
0.1 # 10% safety margin for input tokens we need 10% less does not work.
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SourceReductionResult(BaseModel):
|
|
37
|
+
message: LanguageModelToolMessage
|
|
38
|
+
reduced_chunks: list[ContentChunk]
|
|
39
|
+
chunk_offset: int
|
|
40
|
+
source_offset: int
|
|
41
|
+
|
|
42
|
+
class Config:
|
|
43
|
+
arbitrary_types_allowed = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LoopTokenReducer:
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
logger: Logger,
|
|
50
|
+
event: ChatEvent,
|
|
51
|
+
max_history_tokens: int,
|
|
52
|
+
has_uploaded_content_config: bool,
|
|
53
|
+
reference_manager: ReferenceManager,
|
|
54
|
+
language_model: LMI,
|
|
55
|
+
):
|
|
56
|
+
self._max_history_tokens = max_history_tokens
|
|
57
|
+
self._has_uploaded_content_config = has_uploaded_content_config
|
|
58
|
+
self._logger = logger
|
|
59
|
+
self._reference_manager = reference_manager
|
|
60
|
+
self._language_model = language_model
|
|
61
|
+
self._encoder = self._get_encoder(language_model)
|
|
62
|
+
self._chat_service = ChatService(event)
|
|
63
|
+
self._content_service = ContentService.from_event(event)
|
|
64
|
+
self._user_message = event.payload.user_message
|
|
65
|
+
self._chat_id = event.payload.chat_id
|
|
66
|
+
|
|
67
|
+
def _get_encoder(self, language_model: LMI) -> tiktoken.Encoding:
|
|
68
|
+
name = language_model.encoder_name or "cl100k_base"
|
|
69
|
+
return tiktoken.get_encoding(name)
|
|
70
|
+
|
|
71
|
+
async def get_history_for_model_call(
|
|
72
|
+
self,
|
|
73
|
+
original_user_message: str,
|
|
74
|
+
rendered_user_message_string: str,
|
|
75
|
+
rendered_system_message_string: str,
|
|
76
|
+
loop_history: list[LanguageModelMessage],
|
|
77
|
+
remove_from_text: Callable[[str], Awaitable[str]],
|
|
78
|
+
) -> LanguageModelMessages:
|
|
79
|
+
"""Compose the system and user messages for the plan execution step, which is evaluating if any further tool calls are required."""
|
|
80
|
+
|
|
81
|
+
history_from_db = await self._prep_db_history(
|
|
82
|
+
original_user_message,
|
|
83
|
+
rendered_user_message_string,
|
|
84
|
+
rendered_system_message_string,
|
|
85
|
+
remove_from_text,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
messages = self._construct_history(
|
|
89
|
+
history_from_db,
|
|
90
|
+
loop_history,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
token_count = self._count_message_tokens(messages)
|
|
94
|
+
self._log_token_usage(token_count)
|
|
95
|
+
|
|
96
|
+
while self._exceeds_token_limit(token_count):
|
|
97
|
+
token_count_before_reduction = token_count
|
|
98
|
+
loop_history = self._handle_token_limit_exceeded(loop_history)
|
|
99
|
+
messages = self._construct_history(
|
|
100
|
+
history_from_db,
|
|
101
|
+
loop_history,
|
|
102
|
+
)
|
|
103
|
+
token_count = self._count_message_tokens(messages)
|
|
104
|
+
self._log_token_usage(token_count)
|
|
105
|
+
token_count_after_reduction = token_count
|
|
106
|
+
if token_count_after_reduction >= token_count_before_reduction:
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
token_count = self._count_message_tokens(messages)
|
|
110
|
+
self._logger.info(
|
|
111
|
+
f"Final token count after reduction: {token_count} of model_capacity {self._language_model.token_limits.token_limit_input}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return messages
|
|
115
|
+
|
|
116
|
+
def _exceeds_token_limit(self, token_count: int) -> bool:
|
|
117
|
+
"""Check if token count exceeds the maximum allowed limit and if at least one tool call has more than one source."""
|
|
118
|
+
# At least one tool call should have more than one chunk as answer
|
|
119
|
+
has_multiple_chunks_for_a_tool_call = any(
|
|
120
|
+
len(chunks) > 1
|
|
121
|
+
for chunks in self._reference_manager.get_chunks_of_all_tools()
|
|
122
|
+
)
|
|
123
|
+
max_tokens = int(
|
|
124
|
+
self._language_model.token_limits.token_limit_input
|
|
125
|
+
* (1 - MAX_INPUT_TOKENS_SAFETY_PERCENTAGE)
|
|
126
|
+
)
|
|
127
|
+
# TODO: This is not fully correct at the moment as the token_count
|
|
128
|
+
# include system_prompt and user question already
|
|
129
|
+
# TODO: There is a problem if we exceed but only have one chunk per tool call
|
|
130
|
+
exceeds_limit = token_count > max_tokens
|
|
131
|
+
|
|
132
|
+
return has_multiple_chunks_for_a_tool_call and exceeds_limit
|
|
133
|
+
|
|
134
|
+
def _count_message_tokens(self, messages: LanguageModelMessages) -> int:
|
|
135
|
+
"""Count tokens in messages using the configured encoding model."""
|
|
136
|
+
return num_token_for_language_model_messages(
|
|
137
|
+
messages=messages,
|
|
138
|
+
encode=self._encoder.encode,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _log_token_usage(self, token_count: int) -> None:
|
|
142
|
+
"""Log token usage and update debug info."""
|
|
143
|
+
self._logger.info(f"Token messages: {token_count}")
|
|
144
|
+
# self.agent_debug_info.add("token_messages", token_count)
|
|
145
|
+
|
|
146
|
+
async def _prep_db_history(
|
|
147
|
+
self,
|
|
148
|
+
original_user_message: str,
|
|
149
|
+
rendered_user_message_string: str,
|
|
150
|
+
rendered_system_message_string: str,
|
|
151
|
+
remove_from_text: Callable[[str], Awaitable[str]],
|
|
152
|
+
) -> list[LanguageModelMessage]:
|
|
153
|
+
history_from_db = await self.get_history_from_db(remove_from_text)
|
|
154
|
+
history_from_db = self._replace_user_message(
|
|
155
|
+
history_from_db, original_user_message, rendered_user_message_string
|
|
156
|
+
)
|
|
157
|
+
system_message = LanguageModelSystemMessage(
|
|
158
|
+
content=rendered_system_message_string
|
|
159
|
+
)
|
|
160
|
+
return [system_message] + history_from_db
|
|
161
|
+
|
|
162
|
+
def _construct_history(
|
|
163
|
+
self,
|
|
164
|
+
history_from_db: list[LanguageModelMessage],
|
|
165
|
+
loop_history: list[LanguageModelMessage],
|
|
166
|
+
) -> LanguageModelMessages:
|
|
167
|
+
constructed_history = LanguageModelMessages(
|
|
168
|
+
history_from_db + loop_history,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return constructed_history
|
|
172
|
+
|
|
173
|
+
def _handle_token_limit_exceeded(
|
|
174
|
+
self, loop_history: list[LanguageModelMessage]
|
|
175
|
+
) -> list[LanguageModelMessage]:
|
|
176
|
+
"""Handle case where token limit is exceeded by reducing sources in tool responses."""
|
|
177
|
+
self._logger.warning(
|
|
178
|
+
f"Length of messages is exceeds limit of {self._language_model.token_limits.token_limit_input} tokens. "
|
|
179
|
+
"Reducing number of sources per tool call.",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return self._reduce_message_length_by_reducing_sources_in_tool_response(
|
|
183
|
+
loop_history
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def _replace_user_message(
|
|
187
|
+
self,
|
|
188
|
+
history: list[LanguageModelMessage],
|
|
189
|
+
original_user_message: str,
|
|
190
|
+
rendered_user_message_string: str,
|
|
191
|
+
) -> list[LanguageModelMessage]:
|
|
192
|
+
"""
|
|
193
|
+
Replaces the original user message in the history with the rendered user message string.
|
|
194
|
+
"""
|
|
195
|
+
if history[-1].role == LanguageModelMessageRole.USER:
|
|
196
|
+
m = history[-1]
|
|
197
|
+
|
|
198
|
+
if isinstance(m.content, list):
|
|
199
|
+
# Replace the last text element but be careful not to delete data added when merging with contents
|
|
200
|
+
for t in reversed(m.content):
|
|
201
|
+
field = t.get("type", "")
|
|
202
|
+
if field == "text" and isinstance(field, dict):
|
|
203
|
+
inner_field = field.get("text", "")
|
|
204
|
+
if isinstance(inner_field, str):
|
|
205
|
+
added_to_message_by_history = inner_field.replace(
|
|
206
|
+
original_user_message,
|
|
207
|
+
"",
|
|
208
|
+
)
|
|
209
|
+
t["text"] = (
|
|
210
|
+
rendered_user_message_string
|
|
211
|
+
+ added_to_message_by_history
|
|
212
|
+
)
|
|
213
|
+
break
|
|
214
|
+
elif m.content:
|
|
215
|
+
added_to_message_by_history = m.content.replace(
|
|
216
|
+
original_user_message, ""
|
|
217
|
+
)
|
|
218
|
+
m.content = rendered_user_message_string + added_to_message_by_history
|
|
219
|
+
else:
|
|
220
|
+
history = history + [
|
|
221
|
+
LanguageModelUserMessage(content=rendered_user_message_string),
|
|
222
|
+
]
|
|
223
|
+
return history
|
|
224
|
+
|
|
225
|
+
async def get_history_from_db(
|
|
226
|
+
self, remove_from_text: Callable[[str], Awaitable[str]] | None = None
|
|
227
|
+
) -> list[LanguageModelMessage]:
|
|
228
|
+
"""
|
|
229
|
+
Get the history of the conversation. The function will retrieve a subset of the full history based on the configuration.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
list[LanguageModelMessage]: The history
|
|
233
|
+
"""
|
|
234
|
+
full_history = get_full_history_with_contents(
|
|
235
|
+
user_message=self._user_message,
|
|
236
|
+
chat_id=self._chat_id,
|
|
237
|
+
chat_service=self._chat_service,
|
|
238
|
+
content_service=self._content_service,
|
|
239
|
+
file_content_serialization_type=(
|
|
240
|
+
FileContentSerialization.NONE
|
|
241
|
+
if self._has_uploaded_content_config
|
|
242
|
+
else FileContentSerialization.FILE_NAME
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
if remove_from_text is not None:
|
|
246
|
+
full_history.root = await self._clean_messages(
|
|
247
|
+
full_history.root, remove_from_text
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
limited_history_messages = self._limit_to_token_window(
|
|
251
|
+
full_history.root, self._max_history_tokens
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if len(limited_history_messages) == 0:
|
|
255
|
+
limited_history_messages = full_history.root[-1:]
|
|
256
|
+
|
|
257
|
+
self._logger.info(
|
|
258
|
+
f"Reduced history to {len(limited_history_messages)} messages from {len(full_history.root)}",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return self.ensure_last_message_is_user_message(limited_history_messages)
|
|
262
|
+
|
|
263
|
+
def _limit_to_token_window(
|
|
264
|
+
self, messages: list[LanguageModelMessage], token_limit: int
|
|
265
|
+
) -> list[LanguageModelMessage]:
|
|
266
|
+
selected_messages = []
|
|
267
|
+
token_count = 0
|
|
268
|
+
for msg in messages[::-1]:
|
|
269
|
+
msg_token_count = self._count_message_tokens(
|
|
270
|
+
LanguageModelMessages(root=[msg])
|
|
271
|
+
)
|
|
272
|
+
if token_count + msg_token_count > token_limit:
|
|
273
|
+
break
|
|
274
|
+
selected_messages.append(msg)
|
|
275
|
+
token_count += msg_token_count
|
|
276
|
+
return selected_messages[::-1]
|
|
277
|
+
|
|
278
|
+
async def _clean_messages(
|
|
279
|
+
self,
|
|
280
|
+
messages: list[
|
|
281
|
+
LanguageModelMessage
|
|
282
|
+
| LanguageModelToolMessage
|
|
283
|
+
| LanguageModelAssistantMessage
|
|
284
|
+
| LanguageModelSystemMessage
|
|
285
|
+
| LanguageModelUserMessage
|
|
286
|
+
],
|
|
287
|
+
remove_from_text: Callable[[str], Awaitable[str]],
|
|
288
|
+
) -> list[LanguageModelMessage]:
|
|
289
|
+
for message in messages:
|
|
290
|
+
if isinstance(message.content, str):
|
|
291
|
+
message.content = await remove_from_text(message.content)
|
|
292
|
+
else:
|
|
293
|
+
self._logger.warning(
|
|
294
|
+
f"Skipping message with unsupported content type: {type(message.content)}"
|
|
295
|
+
)
|
|
296
|
+
return messages
|
|
297
|
+
|
|
298
|
+
def ensure_last_message_is_user_message(self, limited_history_messages):
|
|
299
|
+
"""
|
|
300
|
+
As the token limit can be reached in the middle of a gpt_request,
|
|
301
|
+
we move forward to the next user message,to avoid confusing messages for the LLM
|
|
302
|
+
"""
|
|
303
|
+
idx = 0
|
|
304
|
+
for idx, message in enumerate(limited_history_messages):
|
|
305
|
+
if message.role == LanguageModelMessageRole.USER:
|
|
306
|
+
break
|
|
307
|
+
|
|
308
|
+
# 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
|
|
309
|
+
# FIXME: We should remove chunks no longer in history from handler
|
|
310
|
+
return limited_history_messages[idx:]
|
|
311
|
+
|
|
312
|
+
def _reduce_message_length_by_reducing_sources_in_tool_response(
|
|
313
|
+
self,
|
|
314
|
+
history: list[LanguageModelMessage],
|
|
315
|
+
) -> list[LanguageModelMessage]:
|
|
316
|
+
"""
|
|
317
|
+
Reduce the message length by removing the last source result of each tool call.
|
|
318
|
+
If there is only one source for a tool call, the tool call message is returned unchanged.
|
|
319
|
+
"""
|
|
320
|
+
history_reduced: list[LanguageModelMessage] = []
|
|
321
|
+
content_chunks_reduced: list[ContentChunk] = []
|
|
322
|
+
chunk_offset = 0
|
|
323
|
+
source_offset = 0
|
|
324
|
+
|
|
325
|
+
for message in history:
|
|
326
|
+
if self._should_reduce_message(message):
|
|
327
|
+
result = self._reduce_sources_in_tool_message(
|
|
328
|
+
message, # type: ignore
|
|
329
|
+
chunk_offset,
|
|
330
|
+
source_offset,
|
|
331
|
+
)
|
|
332
|
+
content_chunks_reduced.extend(result.reduced_chunks)
|
|
333
|
+
history_reduced.append(result.message)
|
|
334
|
+
chunk_offset = result.chunk_offset
|
|
335
|
+
source_offset = result.source_offset
|
|
336
|
+
else:
|
|
337
|
+
history_reduced.append(message)
|
|
338
|
+
|
|
339
|
+
self._reference_manager.replace(chunks=content_chunks_reduced)
|
|
340
|
+
return history_reduced
|
|
341
|
+
|
|
342
|
+
def _should_reduce_message(self, message: LanguageModelMessage) -> bool:
|
|
343
|
+
"""Determine if a message should have its sources reduced."""
|
|
344
|
+
return message.role == LanguageModelMessageRole.TOOL and isinstance(
|
|
345
|
+
message, LanguageModelToolMessage
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def _reduce_sources_in_tool_message(
|
|
349
|
+
self,
|
|
350
|
+
message: LanguageModelToolMessage,
|
|
351
|
+
chunk_offset: int,
|
|
352
|
+
source_offset: int,
|
|
353
|
+
) -> SourceReductionResult:
|
|
354
|
+
"""
|
|
355
|
+
Reduce the sources in the tool message by removing the last source.
|
|
356
|
+
If there is only one source, the message is returned unchanged.
|
|
357
|
+
"""
|
|
358
|
+
tool_chunks = self._reference_manager.get_chunks_of_tool(message.tool_call_id)
|
|
359
|
+
num_sources = len(tool_chunks)
|
|
360
|
+
|
|
361
|
+
if num_sources == 0:
|
|
362
|
+
return SourceReductionResult(
|
|
363
|
+
message=message,
|
|
364
|
+
reduced_chunks=[],
|
|
365
|
+
chunk_offset=chunk_offset,
|
|
366
|
+
source_offset=source_offset,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Reduce chunks, keeping all but the last one if multiple exist
|
|
370
|
+
if num_sources == 1:
|
|
371
|
+
reduced_chunks = tool_chunks
|
|
372
|
+
content_chunks_reduced = self._reference_manager.get_chunks()[
|
|
373
|
+
chunk_offset : chunk_offset + num_sources
|
|
374
|
+
]
|
|
375
|
+
else:
|
|
376
|
+
reduced_chunks = tool_chunks[:-1]
|
|
377
|
+
content_chunks_reduced = self._reference_manager.get_chunks()[
|
|
378
|
+
chunk_offset : chunk_offset + num_sources - 1
|
|
379
|
+
]
|
|
380
|
+
self._reference_manager.replace_chunks_of_tool(
|
|
381
|
+
message.tool_call_id, reduced_chunks
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Create new message with reduced sources
|
|
385
|
+
new_message = self._create_tool_call_message_with_reduced_sources(
|
|
386
|
+
message=message,
|
|
387
|
+
content_chunks=reduced_chunks,
|
|
388
|
+
source_offset=source_offset,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return SourceReductionResult(
|
|
392
|
+
message=new_message,
|
|
393
|
+
reduced_chunks=content_chunks_reduced,
|
|
394
|
+
chunk_offset=chunk_offset + num_sources,
|
|
395
|
+
source_offset=source_offset + num_sources - (1 if num_sources != 1 else 0),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def _create_tool_call_message_with_reduced_sources(
|
|
399
|
+
self,
|
|
400
|
+
message: LanguageModelToolMessage,
|
|
401
|
+
content_chunks: list[ContentChunk] | None = None,
|
|
402
|
+
source_offset: int = 0,
|
|
403
|
+
) -> LanguageModelToolMessage:
|
|
404
|
+
# Handle special case for TableSearch tool
|
|
405
|
+
if message.name == "TableSearch":
|
|
406
|
+
return self._create_reduced_table_search_message(
|
|
407
|
+
message, content_chunks, source_offset
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Handle empty content case
|
|
411
|
+
if not content_chunks:
|
|
412
|
+
return self._create_reduced_empty_sources_message(message)
|
|
413
|
+
|
|
414
|
+
# Handle standard content chunks
|
|
415
|
+
return self._create_reduced_standard_sources_message(
|
|
416
|
+
message, content_chunks, source_offset
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
def _create_reduced_table_search_message(
|
|
420
|
+
self,
|
|
421
|
+
message: LanguageModelToolMessage,
|
|
422
|
+
content_chunks: list[ContentChunk] | None,
|
|
423
|
+
source_offset: int,
|
|
424
|
+
) -> LanguageModelToolMessage:
|
|
425
|
+
"""
|
|
426
|
+
Create a message for TableSearch tool.
|
|
427
|
+
|
|
428
|
+
Note: TableSearch content consists of a single result with SQL results,
|
|
429
|
+
not content chunks.
|
|
430
|
+
"""
|
|
431
|
+
if not content_chunks:
|
|
432
|
+
content = message.content
|
|
433
|
+
else:
|
|
434
|
+
if isinstance(message.content, str):
|
|
435
|
+
content_dict = json.loads(message.content)
|
|
436
|
+
elif isinstance(message.content, dict):
|
|
437
|
+
content_dict = message.content
|
|
438
|
+
else:
|
|
439
|
+
raise ValueError(f"Unexpected content type: {type(message.content)}")
|
|
440
|
+
|
|
441
|
+
content = json.dumps(
|
|
442
|
+
{
|
|
443
|
+
"source_number": source_offset,
|
|
444
|
+
"content": content_dict.get("content"),
|
|
445
|
+
}
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return LanguageModelToolMessage(
|
|
449
|
+
content=content,
|
|
450
|
+
tool_call_id=message.tool_call_id,
|
|
451
|
+
name=message.name,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
def _create_reduced_empty_sources_message(
|
|
455
|
+
self,
|
|
456
|
+
message: LanguageModelToolMessage,
|
|
457
|
+
) -> LanguageModelToolMessage:
|
|
458
|
+
"""Create a message when no content chunks are available."""
|
|
459
|
+
return LanguageModelToolMessage(
|
|
460
|
+
content="No relevant sources found.",
|
|
461
|
+
tool_call_id=message.tool_call_id,
|
|
462
|
+
name=message.name,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
def _create_reduced_standard_sources_message(
|
|
466
|
+
self,
|
|
467
|
+
message: LanguageModelToolMessage,
|
|
468
|
+
content_chunks: list[ContentChunk],
|
|
469
|
+
source_offset: int,
|
|
470
|
+
) -> LanguageModelToolMessage:
|
|
471
|
+
"""Create a message with standard content chunks."""
|
|
472
|
+
sources = [
|
|
473
|
+
{
|
|
474
|
+
"source_number": source_offset + i,
|
|
475
|
+
"content": chunk.text,
|
|
476
|
+
}
|
|
477
|
+
for i, chunk in enumerate(content_chunks)
|
|
478
|
+
]
|
|
479
|
+
|
|
480
|
+
return LanguageModelToolMessage(
|
|
481
|
+
content=json.dumps(sources),
|
|
482
|
+
tool_call_id=message.tool_call_id,
|
|
483
|
+
name=message.name,
|
|
484
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from unique_toolkit.agentic.tools.schemas import Source
|
|
7
|
+
from unique_toolkit.content.schemas import ContentChunk
|
|
8
|
+
from unique_toolkit.language_model.schemas import (
|
|
9
|
+
LanguageModelAssistantMessage,
|
|
10
|
+
LanguageModelMessage,
|
|
11
|
+
LanguageModelToolMessage,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def convert_tool_interactions_to_content_messages(
|
|
18
|
+
loop_history: list[LanguageModelMessage],
|
|
19
|
+
) -> list[LanguageModelMessage]:
|
|
20
|
+
new_loop_history = []
|
|
21
|
+
copy_loop_history = deepcopy(loop_history)
|
|
22
|
+
|
|
23
|
+
for message in copy_loop_history:
|
|
24
|
+
if isinstance(message, LanguageModelAssistantMessage) and message.tool_calls:
|
|
25
|
+
new_loop_history.append(_convert_tool_call_to_content(message))
|
|
26
|
+
|
|
27
|
+
elif isinstance(message, LanguageModelToolMessage):
|
|
28
|
+
new_loop_history.append(_convert_tool_call_response_to_content(message))
|
|
29
|
+
else:
|
|
30
|
+
new_loop_history.append(message)
|
|
31
|
+
|
|
32
|
+
return new_loop_history
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _convert_tool_call_to_content(
|
|
36
|
+
assistant_message: LanguageModelAssistantMessage,
|
|
37
|
+
) -> LanguageModelAssistantMessage:
|
|
38
|
+
assert assistant_message.tool_calls is not None
|
|
39
|
+
new_content = "The assistant requested the following tool_call:"
|
|
40
|
+
for tool_call in assistant_message.tool_calls:
|
|
41
|
+
new_content += (
|
|
42
|
+
f"\n\n- {tool_call.function.name}: {tool_call.function.arguments}"
|
|
43
|
+
)
|
|
44
|
+
assistant_message.tool_calls = None
|
|
45
|
+
assistant_message.content = new_content
|
|
46
|
+
|
|
47
|
+
return assistant_message
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _convert_tool_call_response_to_content(
|
|
51
|
+
tool_message: LanguageModelToolMessage,
|
|
52
|
+
) -> LanguageModelAssistantMessage:
|
|
53
|
+
new_content = f"The assistant received the following tool_call_response: {tool_message.name}, {tool_message.content}"
|
|
54
|
+
assistant_message = LanguageModelAssistantMessage(
|
|
55
|
+
content=new_content,
|
|
56
|
+
)
|
|
57
|
+
return assistant_message
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def transform_chunks_to_string(
|
|
61
|
+
content_chunks: list[ContentChunk],
|
|
62
|
+
max_source_number: int,
|
|
63
|
+
) -> tuple[str, list[dict[str, Any]]]:
|
|
64
|
+
"""Transform content chunks into a string of sources.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
content_chunks (list[ContentChunk]): The content chunks to transform
|
|
68
|
+
max_source_number (int): The maximum source number to use
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
str: String for the tool call response
|
|
72
|
+
"""
|
|
73
|
+
if not content_chunks:
|
|
74
|
+
return "No relevant sources found.", []
|
|
75
|
+
sources: list[dict[str, Any]] = [
|
|
76
|
+
{
|
|
77
|
+
"source_number": max_source_number + i,
|
|
78
|
+
"content": chunk.text,
|
|
79
|
+
}
|
|
80
|
+
for i, chunk in enumerate(content_chunks)
|
|
81
|
+
]
|
|
82
|
+
return json.dumps(sources), sources
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_sources_from_string(
|
|
86
|
+
source_string: str,
|
|
87
|
+
) -> list[Source] | None:
|
|
88
|
+
"""Transform JSON string from language model tool message in the tool call response into Source objects"""
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# First, try parsing as JSON (new format)
|
|
92
|
+
sources_data = json.loads(source_string)
|
|
93
|
+
return [Source.model_validate(source) for source in sources_data]
|
|
94
|
+
except (json.JSONDecodeError, ValueError):
|
|
95
|
+
logger.warning("Failed to parse source string")
|
|
96
|
+
return None
|