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,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
|
|
File without changes
|
|
@@ -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"\|")
|