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,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