unique_orchestrator 1.11.1__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_orchestrator might be problematic. Click here for more details.
- unique_orchestrator/config.py +401 -0
- unique_orchestrator/prompts/generic_reference_prompt.jinja2 +46 -0
- unique_orchestrator/prompts/system_prompt.jinja2 +166 -0
- unique_orchestrator/prompts/user_message_prompt.jinja2 +23 -0
- unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +259 -0
- unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +729 -0
- unique_orchestrator/tests/test_unique_ai_reference_order.py +134 -0
- unique_orchestrator/tests/test_unique_ai_update_debug_info_for_tool_control.py +339 -0
- unique_orchestrator/unique_ai.py +537 -0
- unique_orchestrator/unique_ai_builder.py +568 -0
- unique_orchestrator-1.11.1.dist-info/LICENSE +1 -0
- unique_orchestrator-1.11.1.dist-info/METADATA +199 -0
- unique_orchestrator-1.11.1.dist-info/RECORD +14 -0
- unique_orchestrator-1.11.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.mark.asyncio
|
|
7
|
+
async def test_history_updated_before_reference_extraction(monkeypatch):
|
|
8
|
+
# Lazy import to avoid heavy dependencies at module import time
|
|
9
|
+
from unique_orchestrator.unique_ai import UniqueAI
|
|
10
|
+
|
|
11
|
+
# Create a minimal UniqueAI instance with mocked dependencies
|
|
12
|
+
mock_logger = MagicMock()
|
|
13
|
+
|
|
14
|
+
class DummyEvent:
|
|
15
|
+
class Payload:
|
|
16
|
+
class AssistantMessage:
|
|
17
|
+
id = "assist_1"
|
|
18
|
+
|
|
19
|
+
assistant_message = AssistantMessage()
|
|
20
|
+
user_message = MagicMock()
|
|
21
|
+
user_message.text = "query"
|
|
22
|
+
|
|
23
|
+
payload = Payload()
|
|
24
|
+
|
|
25
|
+
dummy_event = MagicMock()
|
|
26
|
+
dummy_event.payload = DummyEvent.Payload()
|
|
27
|
+
|
|
28
|
+
mock_config = MagicMock()
|
|
29
|
+
mock_config.agent.max_loop_iterations = 1
|
|
30
|
+
mock_config.space.language_model.name = "dummy-model"
|
|
31
|
+
mock_config.agent.experimental.temperature = 0.0
|
|
32
|
+
mock_config.agent.experimental.additional_llm_options = {}
|
|
33
|
+
|
|
34
|
+
# Managers
|
|
35
|
+
mock_history_manager = MagicMock()
|
|
36
|
+
mock_history_manager.has_no_loop_messages.return_value = True
|
|
37
|
+
mock_history_manager._append_tool_calls_to_history = MagicMock()
|
|
38
|
+
mock_history_manager.add_tool_call_results = MagicMock()
|
|
39
|
+
|
|
40
|
+
mock_reference_manager = MagicMock()
|
|
41
|
+
mock_reference_manager.extract_referenceable_chunks = MagicMock()
|
|
42
|
+
mock_reference_manager.get_chunks.return_value = []
|
|
43
|
+
|
|
44
|
+
mock_thinking_manager = MagicMock()
|
|
45
|
+
mock_debug_info_manager = MagicMock()
|
|
46
|
+
mock_debug_info_manager.get.return_value = {}
|
|
47
|
+
mock_debug_info_manager.extract_tool_debug_info = MagicMock()
|
|
48
|
+
|
|
49
|
+
mock_tool_manager = MagicMock()
|
|
50
|
+
mock_tool_manager.get_forced_tools.return_value = []
|
|
51
|
+
mock_tool_manager.get_tool_definitions.return_value = []
|
|
52
|
+
mock_tool_manager.execute_selected_tools = AsyncMock(return_value=[MagicMock()])
|
|
53
|
+
mock_tool_manager.does_a_tool_take_control.return_value = False
|
|
54
|
+
|
|
55
|
+
class DummyStreamResponse:
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self.tool_calls = [MagicMock()]
|
|
58
|
+
self.message = MagicMock()
|
|
59
|
+
self.message.references = []
|
|
60
|
+
self.message.text = ""
|
|
61
|
+
|
|
62
|
+
def is_empty(self):
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
mock_chat_service = MagicMock()
|
|
66
|
+
mock_chat_service.complete_with_references_async = AsyncMock(
|
|
67
|
+
return_value=DummyStreamResponse()
|
|
68
|
+
)
|
|
69
|
+
mock_chat_service.modify_assistant_message_async = AsyncMock(return_value=None)
|
|
70
|
+
mock_chat_service.create_assistant_message_async = AsyncMock(
|
|
71
|
+
return_value=MagicMock(id="assist_new")
|
|
72
|
+
)
|
|
73
|
+
mock_content_service = MagicMock()
|
|
74
|
+
mock_history_manager.get_history_for_model_call = AsyncMock(
|
|
75
|
+
return_value=MagicMock()
|
|
76
|
+
)
|
|
77
|
+
mock_streaming_handler = MagicMock()
|
|
78
|
+
mock_streaming_handler.complete_with_references_async = AsyncMock(
|
|
79
|
+
return_value=DummyStreamResponse()
|
|
80
|
+
)
|
|
81
|
+
mock_message_step_logger = MagicMock()
|
|
82
|
+
|
|
83
|
+
# Instantiate
|
|
84
|
+
ua = UniqueAI(
|
|
85
|
+
logger=mock_logger,
|
|
86
|
+
event=dummy_event,
|
|
87
|
+
config=mock_config,
|
|
88
|
+
chat_service=mock_chat_service,
|
|
89
|
+
content_service=mock_content_service,
|
|
90
|
+
debug_info_manager=mock_debug_info_manager,
|
|
91
|
+
streaming_handler=mock_streaming_handler,
|
|
92
|
+
reference_manager=mock_reference_manager,
|
|
93
|
+
thinking_manager=mock_thinking_manager,
|
|
94
|
+
tool_manager=mock_tool_manager,
|
|
95
|
+
history_manager=mock_history_manager,
|
|
96
|
+
evaluation_manager=MagicMock(),
|
|
97
|
+
postprocessor_manager=MagicMock(),
|
|
98
|
+
message_step_logger=mock_message_step_logger,
|
|
99
|
+
mcp_servers=[],
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Bypass Jinja template compilation by stubbing prompt renderers
|
|
103
|
+
ua._render_user_prompt = AsyncMock(return_value="user") # type: ignore
|
|
104
|
+
ua._render_system_prompt = AsyncMock(return_value="system") # type: ignore
|
|
105
|
+
# Avoid creating new assistant messages path
|
|
106
|
+
ua._thinking_manager.thinking_is_displayed = MagicMock(return_value=True) # type: ignore
|
|
107
|
+
|
|
108
|
+
# Spy on call order by recording sequence
|
|
109
|
+
call_sequence = []
|
|
110
|
+
|
|
111
|
+
def record_history_add(results):
|
|
112
|
+
call_sequence.append("history_add")
|
|
113
|
+
|
|
114
|
+
def record_reference_extract(results):
|
|
115
|
+
call_sequence.append("reference_extract")
|
|
116
|
+
|
|
117
|
+
def record_debug_extract(results, iteration_index):
|
|
118
|
+
call_sequence.append("debug_extract")
|
|
119
|
+
|
|
120
|
+
mock_history_manager.add_tool_call_results.side_effect = record_history_add
|
|
121
|
+
mock_reference_manager.extract_referenceable_chunks.side_effect = (
|
|
122
|
+
record_reference_extract
|
|
123
|
+
)
|
|
124
|
+
mock_debug_info_manager.extract_tool_debug_info.side_effect = record_debug_extract
|
|
125
|
+
|
|
126
|
+
# Run a single iteration
|
|
127
|
+
await ua.run()
|
|
128
|
+
|
|
129
|
+
# Verify order: history first, then references, then debug
|
|
130
|
+
assert call_sequence[:3] == [
|
|
131
|
+
"history_add",
|
|
132
|
+
"reference_extract",
|
|
133
|
+
"debug_extract",
|
|
134
|
+
]
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestUpdateDebugInfoForToolControl:
|
|
7
|
+
"""Test suite for UniqueAI._update_debug_info_if_tool_took_control method"""
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def mock_unique_ai(self):
|
|
11
|
+
"""Create a minimal UniqueAI instance with mocked dependencies"""
|
|
12
|
+
# Lazy import to avoid heavy dependencies at module import time
|
|
13
|
+
from unique_orchestrator.unique_ai import UniqueAI
|
|
14
|
+
|
|
15
|
+
mock_logger = MagicMock()
|
|
16
|
+
|
|
17
|
+
# Create minimal event structure
|
|
18
|
+
dummy_event = MagicMock()
|
|
19
|
+
dummy_event.payload.assistant_message.id = "assist_1"
|
|
20
|
+
dummy_event.payload.user_message.text = "query"
|
|
21
|
+
dummy_event.payload.assistant_id = "assistant_123"
|
|
22
|
+
dummy_event.payload.name = "TestAssistant"
|
|
23
|
+
dummy_event.payload.user_metadata = {"key": "value"}
|
|
24
|
+
dummy_event.payload.tool_parameters = {"param": "value"}
|
|
25
|
+
|
|
26
|
+
# Create minimal config structure
|
|
27
|
+
mock_config = MagicMock()
|
|
28
|
+
mock_config.agent.prompt_config.user_metadata = []
|
|
29
|
+
|
|
30
|
+
# Create minimal required dependencies
|
|
31
|
+
mock_chat_service = MagicMock()
|
|
32
|
+
mock_chat_service.update_debug_info_async = AsyncMock()
|
|
33
|
+
mock_content_service = MagicMock()
|
|
34
|
+
mock_debug_info_manager = MagicMock()
|
|
35
|
+
mock_reference_manager = MagicMock()
|
|
36
|
+
mock_thinking_manager = MagicMock()
|
|
37
|
+
mock_tool_manager = MagicMock()
|
|
38
|
+
mock_history_manager = MagicMock()
|
|
39
|
+
mock_evaluation_manager = MagicMock()
|
|
40
|
+
mock_postprocessor_manager = MagicMock()
|
|
41
|
+
mock_streaming_handler = MagicMock()
|
|
42
|
+
mock_message_step_logger = MagicMock()
|
|
43
|
+
|
|
44
|
+
# Instantiate UniqueAI
|
|
45
|
+
ua = UniqueAI(
|
|
46
|
+
logger=mock_logger,
|
|
47
|
+
event=dummy_event,
|
|
48
|
+
config=mock_config,
|
|
49
|
+
chat_service=mock_chat_service,
|
|
50
|
+
content_service=mock_content_service,
|
|
51
|
+
debug_info_manager=mock_debug_info_manager,
|
|
52
|
+
streaming_handler=mock_streaming_handler,
|
|
53
|
+
reference_manager=mock_reference_manager,
|
|
54
|
+
thinking_manager=mock_thinking_manager,
|
|
55
|
+
tool_manager=mock_tool_manager,
|
|
56
|
+
history_manager=mock_history_manager,
|
|
57
|
+
evaluation_manager=mock_evaluation_manager,
|
|
58
|
+
postprocessor_manager=mock_postprocessor_manager,
|
|
59
|
+
message_step_logger=mock_message_step_logger,
|
|
60
|
+
mcp_servers=[],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return ua
|
|
64
|
+
|
|
65
|
+
@pytest.mark.ai
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_does_not_update_debug_info_when_tool_did_not_take_control(
|
|
68
|
+
self, mock_unique_ai
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control does nothing when
|
|
72
|
+
_tool_took_control is False.
|
|
73
|
+
Why this matters: Debug info should only be updated when a tool takes control
|
|
74
|
+
of the conversation. When no tool takes control, no update should occur.
|
|
75
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to False.
|
|
76
|
+
"""
|
|
77
|
+
# Arrange
|
|
78
|
+
mock_unique_ai._tool_took_control = False
|
|
79
|
+
mock_unique_ai._debug_info_manager.get.return_value = {
|
|
80
|
+
"tools": [{"name": "SearchTool"}]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Act
|
|
84
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
85
|
+
|
|
86
|
+
# Assert
|
|
87
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_not_called()
|
|
88
|
+
|
|
89
|
+
@pytest.mark.ai
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_does_not_update_debug_info_if_tool_took_control_and_deep_research_is_in_tool_names(
|
|
92
|
+
self, mock_unique_ai
|
|
93
|
+
) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control does nothing when
|
|
96
|
+
DeepResearch is among the called tools.
|
|
97
|
+
Why this matters: DeepResearch handles debug info directly since it calls
|
|
98
|
+
the orchestrator multiple times, so we should not update debug info here.
|
|
99
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True
|
|
100
|
+
and DeepResearch in the tools list.
|
|
101
|
+
"""
|
|
102
|
+
# Arrange
|
|
103
|
+
mock_unique_ai._tool_took_control = True
|
|
104
|
+
mock_unique_ai._debug_info_manager.get.return_value = {
|
|
105
|
+
"tools": [{"name": "DeepResearch"}, {"name": "SearchTool"}]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Act
|
|
109
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
110
|
+
|
|
111
|
+
# Assert
|
|
112
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_not_called()
|
|
113
|
+
|
|
114
|
+
@pytest.mark.ai
|
|
115
|
+
@pytest.mark.asyncio
|
|
116
|
+
async def test_does_not_update_debug_info_if_tool_took_control_and_only_deep_research_is_called(
|
|
117
|
+
self, mock_unique_ai
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control does nothing when
|
|
121
|
+
only DeepResearch is the called tool.
|
|
122
|
+
Why this matters: Even when DeepResearch is the only tool, we should still
|
|
123
|
+
skip the debug info update because DeepResearch manages its own debug info.
|
|
124
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True
|
|
125
|
+
and only DeepResearch in the tools list.
|
|
126
|
+
"""
|
|
127
|
+
# Arrange
|
|
128
|
+
mock_unique_ai._tool_took_control = True
|
|
129
|
+
mock_unique_ai._debug_info_manager.get.return_value = {
|
|
130
|
+
"tools": [{"name": "DeepResearch"}]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Act
|
|
134
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
135
|
+
|
|
136
|
+
# Assert
|
|
137
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_not_called()
|
|
138
|
+
|
|
139
|
+
@pytest.mark.ai
|
|
140
|
+
@pytest.mark.asyncio
|
|
141
|
+
async def test_updates_debug_info_when_tool_took_control_without_deep_research(
|
|
142
|
+
self, mock_unique_ai
|
|
143
|
+
) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control updates debug info
|
|
146
|
+
when a tool takes control and DeepResearch is not among the tools.
|
|
147
|
+
Why this matters: When a non-DeepResearch tool takes control, we need to
|
|
148
|
+
update the debug info with the relevant conversation context.
|
|
149
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True
|
|
150
|
+
and no DeepResearch in the tools list.
|
|
151
|
+
"""
|
|
152
|
+
# Arrange
|
|
153
|
+
mock_unique_ai._tool_took_control = True
|
|
154
|
+
debug_info = {"tools": [{"name": "SearchTool"}, {"name": "WebSearch"}]}
|
|
155
|
+
mock_unique_ai._debug_info_manager.get.return_value = debug_info
|
|
156
|
+
|
|
157
|
+
# Act
|
|
158
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
159
|
+
|
|
160
|
+
# Assert
|
|
161
|
+
expected_debug_info_event = {
|
|
162
|
+
"tools": debug_info,
|
|
163
|
+
"assistant": {
|
|
164
|
+
"id": "assistant_123",
|
|
165
|
+
"name": "TestAssistant",
|
|
166
|
+
},
|
|
167
|
+
"chosenModule": "TestAssistant",
|
|
168
|
+
"userMetadata": {"key": "value"},
|
|
169
|
+
"toolParameters": {"param": "value"},
|
|
170
|
+
}
|
|
171
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_called_once_with(
|
|
172
|
+
debug_info=expected_debug_info_event
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@pytest.mark.ai
|
|
176
|
+
@pytest.mark.asyncio
|
|
177
|
+
async def test_updates_debug_info_with_single_tool(self, mock_unique_ai) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control correctly formats
|
|
180
|
+
the debug info event with a single tool.
|
|
181
|
+
Why this matters: The debug info event structure must be correct for the
|
|
182
|
+
frontend to properly display the tool execution context.
|
|
183
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True
|
|
184
|
+
and a single non-DeepResearch tool.
|
|
185
|
+
"""
|
|
186
|
+
# Arrange
|
|
187
|
+
mock_unique_ai._tool_took_control = True
|
|
188
|
+
debug_info = {"tools": [{"name": "SWOT"}]}
|
|
189
|
+
mock_unique_ai._debug_info_manager.get.return_value = debug_info
|
|
190
|
+
|
|
191
|
+
# Act
|
|
192
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
193
|
+
|
|
194
|
+
# Assert
|
|
195
|
+
expected_debug_info_event = {
|
|
196
|
+
"tools": debug_info,
|
|
197
|
+
"assistant": {
|
|
198
|
+
"id": "assistant_123",
|
|
199
|
+
"name": "TestAssistant",
|
|
200
|
+
},
|
|
201
|
+
"chosenModule": "TestAssistant",
|
|
202
|
+
"userMetadata": {"key": "value"},
|
|
203
|
+
"toolParameters": {"param": "value"},
|
|
204
|
+
}
|
|
205
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_called_once_with(
|
|
206
|
+
debug_info=expected_debug_info_event
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@pytest.mark.ai
|
|
210
|
+
@pytest.mark.asyncio
|
|
211
|
+
async def test_updates_debug_info_with_none_user_metadata(
|
|
212
|
+
self, mock_unique_ai
|
|
213
|
+
) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control handles None
|
|
216
|
+
user_metadata correctly.
|
|
217
|
+
Why this matters: User metadata can be None, and the function should handle
|
|
218
|
+
this gracefully without errors.
|
|
219
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True,
|
|
220
|
+
no DeepResearch in tools, and None user_metadata.
|
|
221
|
+
"""
|
|
222
|
+
# Arrange
|
|
223
|
+
mock_unique_ai._tool_took_control = True
|
|
224
|
+
mock_unique_ai._event.payload.user_metadata = None
|
|
225
|
+
debug_info = {"tools": [{"name": "SearchTool"}]}
|
|
226
|
+
mock_unique_ai._debug_info_manager.get.return_value = debug_info
|
|
227
|
+
|
|
228
|
+
# Act
|
|
229
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
230
|
+
|
|
231
|
+
# Assert
|
|
232
|
+
expected_debug_info_event = {
|
|
233
|
+
"tools": debug_info,
|
|
234
|
+
"assistant": {
|
|
235
|
+
"id": "assistant_123",
|
|
236
|
+
"name": "TestAssistant",
|
|
237
|
+
},
|
|
238
|
+
"chosenModule": "TestAssistant",
|
|
239
|
+
"userMetadata": None,
|
|
240
|
+
"toolParameters": {"param": "value"},
|
|
241
|
+
}
|
|
242
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_called_once_with(
|
|
243
|
+
debug_info=expected_debug_info_event
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@pytest.mark.ai
|
|
247
|
+
@pytest.mark.asyncio
|
|
248
|
+
async def test_updates_debug_info_with_none_tool_parameters(
|
|
249
|
+
self, mock_unique_ai
|
|
250
|
+
) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control handles None
|
|
253
|
+
tool_parameters correctly.
|
|
254
|
+
Why this matters: Tool parameters can be None, and the function should handle
|
|
255
|
+
this gracefully without errors.
|
|
256
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True,
|
|
257
|
+
no DeepResearch in tools, and None tool_parameters.
|
|
258
|
+
"""
|
|
259
|
+
# Arrange
|
|
260
|
+
mock_unique_ai._tool_took_control = True
|
|
261
|
+
mock_unique_ai._event.payload.tool_parameters = None
|
|
262
|
+
debug_info = {"tools": [{"name": "SearchTool"}]}
|
|
263
|
+
mock_unique_ai._debug_info_manager.get.return_value = debug_info
|
|
264
|
+
|
|
265
|
+
# Act
|
|
266
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
267
|
+
|
|
268
|
+
# Assert
|
|
269
|
+
expected_debug_info_event = {
|
|
270
|
+
"tools": debug_info,
|
|
271
|
+
"assistant": {
|
|
272
|
+
"id": "assistant_123",
|
|
273
|
+
"name": "TestAssistant",
|
|
274
|
+
},
|
|
275
|
+
"chosenModule": "TestAssistant",
|
|
276
|
+
"userMetadata": {"key": "value"},
|
|
277
|
+
"toolParameters": None,
|
|
278
|
+
}
|
|
279
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_called_once_with(
|
|
280
|
+
debug_info=expected_debug_info_event
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
@pytest.mark.ai
|
|
284
|
+
@pytest.mark.asyncio
|
|
285
|
+
async def test_updates_debug_info_with_empty_tools_list(
|
|
286
|
+
self, mock_unique_ai
|
|
287
|
+
) -> None:
|
|
288
|
+
"""
|
|
289
|
+
Purpose: Verify that _update_debug_info_if_tool_took_control updates debug info
|
|
290
|
+
when the tools list is empty.
|
|
291
|
+
Why this matters: Even with an empty tools list, if _tool_took_control is True,
|
|
292
|
+
the debug info should still be updated (edge case).
|
|
293
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True
|
|
294
|
+
and an empty tools list.
|
|
295
|
+
"""
|
|
296
|
+
# Arrange
|
|
297
|
+
mock_unique_ai._tool_took_control = True
|
|
298
|
+
debug_info = {"tools": []}
|
|
299
|
+
mock_unique_ai._debug_info_manager.get.return_value = debug_info
|
|
300
|
+
|
|
301
|
+
# Act
|
|
302
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
303
|
+
|
|
304
|
+
# Assert
|
|
305
|
+
expected_debug_info_event = {
|
|
306
|
+
"tools": debug_info,
|
|
307
|
+
"assistant": {
|
|
308
|
+
"id": "assistant_123",
|
|
309
|
+
"name": "TestAssistant",
|
|
310
|
+
},
|
|
311
|
+
"chosenModule": "TestAssistant",
|
|
312
|
+
"userMetadata": {"key": "value"},
|
|
313
|
+
"toolParameters": {"param": "value"},
|
|
314
|
+
}
|
|
315
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_called_once_with(
|
|
316
|
+
debug_info=expected_debug_info_event
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@pytest.mark.ai
|
|
320
|
+
@pytest.mark.asyncio
|
|
321
|
+
async def test_deep_research_check_is_case_sensitive(self, mock_unique_ai) -> None:
|
|
322
|
+
"""
|
|
323
|
+
Purpose: Verify that the DeepResearch check is case-sensitive.
|
|
324
|
+
Why this matters: The check for "DeepResearch" should be exact match.
|
|
325
|
+
Tools with different casing (e.g., "deepresearch", "DEEPRESEARCH") should
|
|
326
|
+
not trigger the early return.
|
|
327
|
+
Setup summary: Create a UniqueAI instance with _tool_took_control set to True
|
|
328
|
+
and tools with similar but not exact "DeepResearch" names.
|
|
329
|
+
"""
|
|
330
|
+
# Arrange
|
|
331
|
+
mock_unique_ai._tool_took_control = True
|
|
332
|
+
debug_info = {"tools": [{"name": "deepresearch"}, {"name": "DEEPRESEARCH"}]}
|
|
333
|
+
mock_unique_ai._debug_info_manager.get.return_value = debug_info
|
|
334
|
+
|
|
335
|
+
# Act
|
|
336
|
+
await mock_unique_ai._update_debug_info_if_tool_took_control()
|
|
337
|
+
|
|
338
|
+
# Assert - should update because "DeepResearch" exact match is not found
|
|
339
|
+
mock_unique_ai._chat_service.update_debug_info_async.assert_called_once()
|