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,285 @@
1
+ import re
2
+ from datetime import datetime
3
+ from enum import StrEnum
4
+ from functools import wraps
5
+ from typing import Protocol, TypedDict
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
10
+ from unique_toolkit.chat.service import ChatService
11
+ from unique_toolkit.content.schemas import ContentReference
12
+ from unique_toolkit.language_model.schemas import (
13
+ LanguageModelFunction,
14
+ LanguageModelStreamResponse,
15
+ )
16
+
17
+ ARROW = "→ "
18
+ DUMMY_REFERENCE_PLACEHOLDER = "<sup></sup>"
19
+
20
+
21
+ class ProgressState(StrEnum):
22
+ STARTED = "started"
23
+ RUNNING = "running"
24
+ FAILED = "failed"
25
+ FINISHED = "finished"
26
+
27
+
28
+ class ToolExecutionStatus(BaseModel):
29
+ name: str
30
+ message: str
31
+ state: ProgressState
32
+ references: list[ContentReference] = []
33
+ timestamp: datetime = Field(default_factory=datetime.now)
34
+
35
+
36
+ class StateToDisplayTemplate(TypedDict):
37
+ started: str
38
+ running: str
39
+ failed: str
40
+ finished: str
41
+
42
+
43
+ _DEFAULT_STATE_TO_DISPLAY_TEMPLATE: StateToDisplayTemplate = {
44
+ "started": "{arrow}**{{tool_name}}** ⚪: {{message}}".format(arrow=ARROW),
45
+ "running": "{arrow}**{{tool_name}}** 🟡: {{message}}".format(arrow=ARROW),
46
+ "finished": "{arrow}**{{tool_name}}** 🟢: {{message}}".format(arrow=ARROW),
47
+ "failed": "{arrow}**{{tool_name}}** 🔴: {{message}}".format(arrow=ARROW),
48
+ }
49
+
50
+
51
+ state_to_display_template_description = """
52
+ Display templates for the different progress states.
53
+ The template is a string that will be used to display the progress status.
54
+ It can contain the following placeholders:
55
+ - `{tool_name}`: The name of the tool
56
+ - `{message}`: The message to display (sent by the tool)
57
+ """.strip()
58
+
59
+
60
+ class ToolProgressReporterConfig(BaseModel):
61
+ model_config = get_configuration_dict()
62
+
63
+ state_to_display_template: StateToDisplayTemplate = Field(
64
+ default=_DEFAULT_STATE_TO_DISPLAY_TEMPLATE,
65
+ description=state_to_display_template_description,
66
+ title="Display Templates",
67
+ )
68
+
69
+
70
+ class ToolProgressReporter:
71
+ def __init__(
72
+ self,
73
+ chat_service: ChatService,
74
+ config: ToolProgressReporterConfig | None = None,
75
+ ):
76
+ self.chat_service = chat_service
77
+ self.tool_statuses: dict[str, ToolExecutionStatus] = {}
78
+ self._progress_start_text = ""
79
+ self._requires_new_assistant_message = False
80
+ self._config = config or ToolProgressReporterConfig()
81
+
82
+ @property
83
+ def requires_new_assistant_message(self):
84
+ return self._requires_new_assistant_message
85
+
86
+ @requires_new_assistant_message.setter
87
+ def requires_new_assistant_message(self, value: bool):
88
+ self._requires_new_assistant_message = value
89
+
90
+ @property
91
+ def tool_statuses_is_empty(self):
92
+ return len(self.tool_statuses) == 0
93
+
94
+ def empty_tool_statuses_if_stream_has_text(
95
+ self, stream_response: LanguageModelStreamResponse
96
+ ):
97
+ if stream_response.message.text:
98
+ self.tool_statuses = {}
99
+
100
+ async def notify_from_tool_call(
101
+ self,
102
+ tool_call: LanguageModelFunction,
103
+ name: str,
104
+ message: str,
105
+ state: ProgressState,
106
+ references: list[ContentReference] = [],
107
+ requires_new_assistant_message: bool = False,
108
+ ):
109
+ """
110
+ Notifies about a tool call execution status and updates the assistant message.
111
+
112
+ Args:
113
+ tool_call (LanguageModelFunction): The tool call being executed
114
+ name (str): Name of the tool being executed
115
+ message (str): Status message to display
116
+ state (ProgressState): Current execution state of the tool
117
+ references (list[ContentReference], optional): List of content references. Defaults to [].
118
+ requires_new_assistant_message (bool, optional): Whether a new assistant message is needed when tool call is finished.
119
+ Defaults to False. If yes, the agentic steps will remain in chat history and will be overwritten by the stream response.
120
+ """
121
+ self.tool_statuses[tool_call.id] = ToolExecutionStatus(
122
+ name=name,
123
+ message=message,
124
+ state=state,
125
+ references=references,
126
+ timestamp=self._get_timestamp_for_tool_call(tool_call),
127
+ )
128
+ self.requires_new_assistant_message = (
129
+ self.requires_new_assistant_message or requires_new_assistant_message
130
+ )
131
+ await self.publish()
132
+
133
+ async def publish(self):
134
+ messages = []
135
+ all_references = []
136
+ for item in sorted(self.tool_statuses.values(), key=lambda x: x.timestamp):
137
+ references = item.references
138
+ start_number = len(all_references) + 1
139
+ message = self._replace_placeholders(item.message, start_number)
140
+ references = self._correct_reference_sequence(references, start_number)
141
+ all_references.extend(references)
142
+
143
+ display_message = self._get_tool_status_display_message(
144
+ name=item.name, message=message, state=item.state
145
+ )
146
+ if display_message is not None:
147
+ messages.append(display_message)
148
+
149
+ await self.chat_service.modify_assistant_message_async(
150
+ content=self._progress_start_text + "\n\n" + "\n\n".join(messages),
151
+ references=all_references,
152
+ )
153
+
154
+ @staticmethod
155
+ def _replace_placeholders(message: str, start_number: int = 1) -> str:
156
+ counter = start_number
157
+
158
+ def replace_match(match):
159
+ nonlocal counter
160
+ result = f"<sup>{counter}</sup>"
161
+ counter += 1
162
+ return result
163
+
164
+ return re.sub(r"<sup></sup>", replace_match, message)
165
+
166
+ @staticmethod
167
+ def _correct_reference_sequence(
168
+ references: list[ContentReference], start_number: int = 1
169
+ ) -> list[ContentReference]:
170
+ for i, reference in enumerate(references, start_number):
171
+ reference.sequence_number = i
172
+ return references
173
+
174
+ def _get_timestamp_for_tool_call(
175
+ self, tool_call: LanguageModelFunction
176
+ ) -> datetime:
177
+ """
178
+ Keep the same timestamp if the tool call is already in the statuses.
179
+ This ensures the display order stays consistent.
180
+ """
181
+ if tool_call.id in self.tool_statuses:
182
+ return self.tool_statuses[tool_call.id].timestamp
183
+
184
+ return datetime.now()
185
+
186
+ def _get_tool_status_display_message(
187
+ self, name: str, message: str, state: ProgressState
188
+ ) -> str | None:
189
+ display_message = self._config.state_to_display_template[state.value].format(
190
+ tool_name=name,
191
+ message=message,
192
+ )
193
+ # Don't display empty messages
194
+ if display_message.strip() == "":
195
+ return None
196
+
197
+ return display_message
198
+
199
+
200
+ class ToolWithToolProgressReporter(Protocol):
201
+ tool_progress_reporter: ToolProgressReporter
202
+
203
+
204
+ def track_tool_progress(
205
+ message: str,
206
+ on_start_state: ProgressState = ProgressState.RUNNING,
207
+ on_success_state: ProgressState = ProgressState.RUNNING,
208
+ on_success_message: str | None = None,
209
+ on_error_message: str = "Unexpected error occurred",
210
+ requires_new_assistant_message: bool = False,
211
+ ):
212
+ """
213
+ Decorator to add progress reporting and status tracking steps to tool functions. Can be used with async and sync functions.
214
+
215
+ Args:
216
+ name (str): Display name for the tool progress status
217
+ message (str): Message to show during tool execution
218
+ on_error_message (str, optional): Message to show if tool execution fails. Defaults to empty string.
219
+ on_success_state (ProgressState, optional): State to set after successful execution. Defaults to RUNNING.
220
+ requires_new_assistant_message (bool, optional): Whether to create a new assistant message. Defaults to False.
221
+
222
+ The decorator will:
223
+ 1. Show a RUNNING status when the tool starts executing
224
+ 2. Update the status to on_success_state if execution succeeds
225
+ 3. Update the status to FAILED if execution fails
226
+ 4. Include any references from the tool result in the status update if the result has a 'references' attribute or item.
227
+ 5. Create a new assistant message if requires_new_assistant_message is True
228
+
229
+ The decorated function must be a method of a class that implements ToolWithToolProgressReporter.
230
+ """
231
+
232
+ def decorator(func):
233
+ @wraps(func) # Preserve the original function's metadata
234
+ async def async_wrapper(
235
+ self: ToolWithToolProgressReporter,
236
+ tool_call: LanguageModelFunction,
237
+ notification_tool_name: str,
238
+ *args,
239
+ **kwargs,
240
+ ):
241
+ try:
242
+ # Start status
243
+ await self.tool_progress_reporter.notify_from_tool_call(
244
+ tool_call=tool_call,
245
+ name=notification_tool_name,
246
+ message=message,
247
+ state=on_start_state,
248
+ )
249
+
250
+ # Execute the tool function
251
+ result = await func(
252
+ self, tool_call, notification_tool_name, *args, **kwargs
253
+ )
254
+
255
+ # Success status
256
+ await self.tool_progress_reporter.notify_from_tool_call(
257
+ tool_call=tool_call,
258
+ name=notification_tool_name,
259
+ message=on_success_message or message,
260
+ state=on_success_state,
261
+ references=_get_references_from_results(result),
262
+ requires_new_assistant_message=requires_new_assistant_message,
263
+ )
264
+ return result
265
+
266
+ except Exception as e:
267
+ # Failure status
268
+ await self.tool_progress_reporter.notify_from_tool_call(
269
+ tool_call=tool_call,
270
+ name=notification_tool_name,
271
+ message=on_error_message,
272
+ state=ProgressState.FAILED,
273
+ requires_new_assistant_message=requires_new_assistant_message,
274
+ )
275
+ raise e
276
+
277
+ return async_wrapper
278
+
279
+ return decorator
280
+
281
+
282
+ def _get_references_from_results(result):
283
+ if isinstance(result, dict):
284
+ return result.get("references", [])
285
+ return getattr(result, "references", [])
@@ -0,0 +1,19 @@
1
+ """Utilities for tools."""
2
+
3
+ from unique_toolkit.agentic.tools.utils.execution.execution import (
4
+ Result,
5
+ SafeTaskExecutor,
6
+ failsafe,
7
+ failsafe_async,
8
+ safe_execute,
9
+ safe_execute_async,
10
+ )
11
+
12
+ __all__ = [
13
+ "failsafe",
14
+ "failsafe_async",
15
+ "safe_execute",
16
+ "safe_execute_async",
17
+ "SafeTaskExecutor",
18
+ "Result",
19
+ ]
@@ -0,0 +1 @@
1
+ """Execution utilities for tools."""
@@ -0,0 +1,286 @@
1
+ import functools
2
+ import logging
3
+ from typing import (
4
+ Awaitable,
5
+ Callable,
6
+ Generic,
7
+ Iterable,
8
+ ParamSpec,
9
+ Type,
10
+ TypeVar,
11
+ cast,
12
+ )
13
+
14
+ # Function types
15
+ P = ParamSpec("P")
16
+ R = TypeVar("R")
17
+
18
+
19
+ _logger = logging.getLogger(__name__)
20
+
21
+
22
+ class Result(Generic[R]):
23
+ def __init__(
24
+ self,
25
+ success: bool,
26
+ result: R | None = None,
27
+ exception: Exception | None = None,
28
+ ) -> None:
29
+ self._success = success
30
+ self._result = result
31
+ self._exception = exception
32
+
33
+ @property
34
+ def exception(self) -> Exception | None:
35
+ return self._exception
36
+
37
+ @property
38
+ def success(self) -> bool:
39
+ return self._success
40
+
41
+ def unpack(self, default: R | None = None) -> R:
42
+ return cast(R, self._result) if self.success else cast(R, default)
43
+
44
+ def __str__(self) -> str:
45
+ return (
46
+ f"Success: {str(self._result)}"
47
+ if self.success
48
+ else f"Failure: {str(self._exception)}"
49
+ )
50
+
51
+
52
+ class SafeTaskExecutor:
53
+ """
54
+ Execute function calls "safely": exceptions are caught and logged,
55
+ and the function result is returned as a `Result` object.
56
+
57
+ Several parameters are available to customize the behavior of the executor:
58
+ - `exceptions`: a list of exceptions that should be caught and logged
59
+ - `ignored_exceptions`: a list of exceptions that should be passed through
60
+ - `log_exceptions`: whether to log exceptions
61
+ - `log_exc_info`: whether to log exception info
62
+ - `logger`: a logger to use for logging
63
+
64
+
65
+ Usage:
66
+ ```python
67
+ executor = SafeTaskExecutor(
68
+ exceptions=(ValueError,),
69
+ ignored_exceptions=(KeyError,),
70
+ )
71
+
72
+ executor.execute(failing_function, "test")
73
+
74
+ executor.execute_async(async_failing_function, "test")
75
+ ```
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ exceptions: Iterable[Type[Exception]] = (Exception,),
81
+ ignored_exceptions: Iterable[Type[Exception]] = (),
82
+ log_exceptions: bool = True,
83
+ log_exc_info: bool = True,
84
+ logger: logging.Logger | None = None,
85
+ ) -> None:
86
+ self._exceptions = tuple(exceptions)
87
+ self._ignored_exceptions = tuple(ignored_exceptions)
88
+ self._log_exceptions = log_exceptions
89
+ self._log_exc_info = log_exc_info
90
+ self._logger = logger or _logger
91
+
92
+ def execute(
93
+ self, f: Callable[P, R], *args: P.args, **kwargs: P.kwargs
94
+ ) -> Result[R]:
95
+ try:
96
+ return Result(True, f(*args, **kwargs))
97
+ except self._exceptions as e:
98
+ if isinstance(e, self._ignored_exceptions):
99
+ raise e
100
+ if self._log_exceptions:
101
+ self._logger.error(
102
+ f"Error in {f.__name__}: {e}", exc_info=self._log_exc_info
103
+ )
104
+ return Result(False, exception=e)
105
+
106
+ async def execute_async(
107
+ self, f: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs
108
+ ) -> Result[R]:
109
+ try:
110
+ return Result(True, await f(*args, **kwargs))
111
+ except self._exceptions as e:
112
+ if isinstance(e, self._ignored_exceptions):
113
+ raise e
114
+ if self._log_exceptions:
115
+ self._logger.error(
116
+ f"Error in {f.__name__}: {e}", exc_info=self._log_exc_info
117
+ )
118
+ return Result(False, exception=e)
119
+
120
+
121
+ def safe_execute(f: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Result[R]:
122
+ """
123
+ Execute a function call "safely": exceptions are caught and logged,
124
+ and the function result is returned as a `Result` object.
125
+
126
+ Usage:
127
+ ```python
128
+ def failing_function(a : str) -> int:
129
+ raise ValueError(a)
130
+
131
+ result = safe_execute(failing_function, "test")
132
+ print(result)
133
+ >> Failure: ValueError('test')
134
+
135
+ result.success
136
+ >> False
137
+
138
+ result.unpack()
139
+ >> None
140
+
141
+ result.exception
142
+ >> ValueError('test')
143
+
144
+ result.unpack(default=1)
145
+ >> 1
146
+ ```
147
+
148
+ ```python
149
+ def succeeding_function(a : str):
150
+ return a
151
+
152
+
153
+ result = safe_execute(succeeding_function, "test")
154
+
155
+ print(result)
156
+ >> Success: test
157
+
158
+ result.success
159
+ >> True
160
+
161
+ result.unpack()
162
+ >> 'test'
163
+
164
+ result.exception
165
+ >> None
166
+ ```
167
+ """
168
+ return SafeTaskExecutor().execute(f, *args, **kwargs)
169
+
170
+
171
+ async def safe_execute_async(
172
+ f: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs
173
+ ) -> Result[R]:
174
+ """
175
+ Equivalent to `safe_execute` for async functions.
176
+ """
177
+ return await SafeTaskExecutor().execute_async(f, *args, **kwargs)
178
+
179
+
180
+ FailureReturnType = TypeVar("FailureReturnType")
181
+
182
+
183
+ def failsafe(
184
+ failure_return_value: FailureReturnType,
185
+ exceptions: Iterable[Type[Exception]] = (Exception,),
186
+ ignored_exceptions: Iterable[Type[Exception]] = (),
187
+ log_exceptions: bool = True,
188
+ log_exc_info: bool = True,
189
+ logger: logging.Logger | None = None,
190
+ ) -> Callable[[Callable[P, R]], Callable[P, R | FailureReturnType]]:
191
+ """
192
+ Decorator that executes sync functions with failsafe behavior: exceptions are caught and logged,
193
+ and a fallback return value is returned on failure instead of raising the exception.
194
+
195
+ Parameters are the same as SafeTaskExecutor plus:
196
+ - `failure_return_value`: value to return when an exception occurs
197
+
198
+ Usage:
199
+ ```python
200
+ @failsafe(
201
+ failure_return_value="default",
202
+ exceptions=(ValueError,),
203
+ ignored_exceptions=(KeyError,),
204
+ )
205
+ def failing_function(a: str) -> str:
206
+ raise ValueError(a)
207
+
208
+
209
+ result = failing_function("test")
210
+ # Returns "default" instead of raising ValueError
211
+ ```
212
+ """
213
+
214
+ def decorator(func: Callable[P, R]) -> Callable[P, R | FailureReturnType]:
215
+ executor = SafeTaskExecutor(
216
+ exceptions=exceptions,
217
+ ignored_exceptions=ignored_exceptions,
218
+ log_exceptions=log_exceptions,
219
+ log_exc_info=log_exc_info,
220
+ logger=logger,
221
+ )
222
+
223
+ @functools.wraps(func)
224
+ def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R | FailureReturnType:
225
+ result = executor.execute(func, *args, **kwargs)
226
+ return result.unpack(default=cast(R, failure_return_value))
227
+
228
+ return sync_wrapper
229
+
230
+ return decorator
231
+
232
+
233
+ def failsafe_async(
234
+ failure_return_value: FailureReturnType,
235
+ exceptions: Iterable[Type[Exception]] = (Exception,),
236
+ ignored_exceptions: Iterable[Type[Exception]] = (),
237
+ log_exceptions: bool = True,
238
+ log_exc_info: bool = True,
239
+ logger: logging.Logger | None = None,
240
+ ) -> Callable[
241
+ [Callable[P, Awaitable[R]]], Callable[P, Awaitable[R | FailureReturnType]]
242
+ ]:
243
+ """
244
+ Decorator that executes async functions with failsafe behavior: exceptions are caught and logged,
245
+ and a fallback return value is returned on failure instead of raising the exception.
246
+
247
+ Parameters are the same as SafeTaskExecutor plus:
248
+ - `failure_return_value`: value to return when an exception occurs
249
+
250
+ Usage:
251
+ ```python
252
+ @failsafe_async(
253
+ failure_return_value=[],
254
+ exceptions=(ValueError,),
255
+ ignored_exceptions=(KeyError,),
256
+ )
257
+ async def async_failing_function(a: str) -> list:
258
+ raise ValueError(a)
259
+
260
+
261
+ result = await async_failing_function("test")
262
+ # Returns [] instead of raising ValueError
263
+ ```
264
+ """
265
+
266
+ def decorator(
267
+ func: Callable[P, Awaitable[R]],
268
+ ) -> Callable[P, Awaitable[R | FailureReturnType]]:
269
+ executor = SafeTaskExecutor(
270
+ exceptions=exceptions,
271
+ ignored_exceptions=ignored_exceptions,
272
+ log_exceptions=log_exceptions,
273
+ log_exc_info=log_exc_info,
274
+ logger=logger,
275
+ )
276
+
277
+ @functools.wraps(func)
278
+ async def async_wrapper(
279
+ *args: P.args, **kwargs: P.kwargs
280
+ ) -> R | FailureReturnType:
281
+ result = await executor.execute_async(func, *args, **kwargs)
282
+ return result.unpack(default=cast(R, failure_return_value))
283
+
284
+ return async_wrapper
285
+
286
+ return decorator
@@ -0,0 +1,21 @@
1
+ # default schema follows logic in node-ingestion-worker: https://github.com/Unique-AG/monorepo/blob/76b4923611199a80abf9304639b3aa0538ec41ed/node/apps/node-ingestion-worker/src/ingestors/lib/text-manipulations.ts#L181C17-L181C28
2
+ from pydantic import BaseModel
3
+
4
+ from unique_toolkit._common.pydantic_helpers import get_configuration_dict
5
+
6
+ SOURCE_TEMPLATE = "<source${index}>${document}${info}${text}</source${index}>"
7
+ SECTIONS = {
8
+ "document": "<|document|>{}<|/document|>\n",
9
+ "info": "<|info|>{}<|/info|>\n",
10
+ }
11
+
12
+
13
+ class SourceFormatConfig(BaseModel):
14
+ model_config = get_configuration_dict()
15
+ source_template: str = SOURCE_TEMPLATE
16
+ sections: dict[str, str] = SECTIONS
17
+
18
+ @staticmethod
19
+ def template_to_pattern(template: str) -> str:
20
+ """Convert a template string into a regex pattern."""
21
+ return template.replace("{}", "(.*?)").replace("|", r"\|")