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.

@@ -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()