unique_orchestrator 1.0.1__py3-none-any.whl → 1.7.16__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.
@@ -0,0 +1,496 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from unittest.mock import MagicMock
5
+
6
+ import pytest
7
+
8
+ if TYPE_CHECKING:
9
+ from unique_orchestrator.unique_ai import UniqueAI
10
+
11
+
12
+ class TestLogToolCalls:
13
+ """Test suite for UniqueAI._log_tool_calls method"""
14
+
15
+ @pytest.fixture
16
+ def mock_unique_ai(self, monkeypatch):
17
+ """Create a minimal UniqueAI instance with mocked dependencies"""
18
+ # Mock MessageStepLogger before importing UniqueAI to avoid import errors
19
+ mock_message_step_logger_class = MagicMock()
20
+ mock_service_module = MagicMock()
21
+ mock_service_module.MessageStepLogger = mock_message_step_logger_class
22
+
23
+ # Use monkeypatch to set the module in sys.modules before import
24
+ import sys
25
+
26
+ monkeypatch.setitem(
27
+ sys.modules,
28
+ "unique_toolkit.agentic.message_log_manager.service",
29
+ mock_service_module,
30
+ )
31
+
32
+ # Lazy import to avoid heavy dependencies at module import time
33
+ from unique_orchestrator.unique_ai import UniqueAI
34
+
35
+ mock_logger = MagicMock()
36
+
37
+ # Create minimal event structure
38
+ dummy_event = MagicMock()
39
+ dummy_event.payload.assistant_message.id = "assist_1"
40
+ dummy_event.payload.user_message.text = "query"
41
+
42
+ # Create minimal config structure
43
+ mock_config = MagicMock()
44
+ mock_config.agent.prompt_config.user_metadata = []
45
+
46
+ # Create minimal required dependencies
47
+ mock_chat_service = MagicMock()
48
+ mock_content_service = MagicMock()
49
+ mock_debug_info_manager = MagicMock()
50
+ mock_reference_manager = MagicMock()
51
+ mock_thinking_manager = MagicMock()
52
+ mock_tool_manager = MagicMock()
53
+ mock_history_manager = MagicMock()
54
+ mock_evaluation_manager = MagicMock()
55
+ mock_postprocessor_manager = MagicMock()
56
+ mock_streaming_handler = MagicMock()
57
+ mock_message_step_logger = MagicMock()
58
+
59
+ # Instantiate UniqueAI
60
+ ua = UniqueAI(
61
+ logger=mock_logger,
62
+ event=dummy_event,
63
+ config=mock_config,
64
+ chat_service=mock_chat_service,
65
+ content_service=mock_content_service,
66
+ debug_info_manager=mock_debug_info_manager,
67
+ streaming_handler=mock_streaming_handler,
68
+ reference_manager=mock_reference_manager,
69
+ thinking_manager=mock_thinking_manager,
70
+ tool_manager=mock_tool_manager,
71
+ history_manager=mock_history_manager,
72
+ evaluation_manager=mock_evaluation_manager,
73
+ postprocessor_manager=mock_postprocessor_manager,
74
+ message_step_logger=mock_message_step_logger,
75
+ mcp_servers=[],
76
+ )
77
+
78
+ return ua
79
+
80
+ @pytest.mark.ai
81
+ def test_log_tool_calls__creates_log_entry__with_single_tool_call(
82
+ self, mock_unique_ai: UniqueAI
83
+ ) -> None:
84
+ """
85
+ Purpose: Verify that _log_tool_calls creates a message log entry with the correct format when a single tool call is provided.
86
+
87
+ Why this matters: The logging of tool calls is critical for user visibility and debugging. The function must correctly format and log tool call information.
88
+
89
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with one available tool, and provide a single tool call.
90
+ """
91
+ # Arrange
92
+ mock_tool = MagicMock(spec=["name", "display_name"])
93
+ mock_tool.name = "search_tool"
94
+ mock_tool.display_name.return_value = "Search Tool"
95
+
96
+ mock_unique_ai._tool_manager.available_tools = [mock_tool]
97
+
98
+ mock_tool_call = MagicMock(spec=["name"])
99
+ mock_tool_call.name = "search_tool"
100
+
101
+ tool_calls = [mock_tool_call]
102
+
103
+ # Act
104
+ mock_unique_ai._log_tool_calls(tool_calls)
105
+
106
+ # Assert
107
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
108
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 1
109
+ mock_unique_ai._history_manager.add_tool_call.assert_called_once_with(
110
+ mock_tool_call
111
+ )
112
+
113
+ assert isinstance(
114
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
115
+ int,
116
+ )
117
+ assert (
118
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 1
119
+ )
120
+ mock_unique_ai._message_step_logger.create_message_log_entry.assert_called_once_with(
121
+ text="**Triggered Tool Calls:**\n \n• Search Tool", references=[]
122
+ )
123
+
124
+ @pytest.mark.ai
125
+ def test_log_tool_calls__creates_log_entry__with_multiple_tool_calls(
126
+ self, mock_unique_ai: UniqueAI
127
+ ) -> None:
128
+ """
129
+ Purpose: Verify that _log_tool_calls correctly formats and logs multiple tool calls in a single log entry.
130
+
131
+ Why this matters: Users may trigger multiple tools simultaneously, and all should be logged in a readable format.
132
+
133
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with multiple available tools, and provide multiple tool calls.
134
+ """
135
+ # Arrange
136
+ mock_tool_1 = MagicMock(spec=["name", "display_name"])
137
+ mock_tool_1.name = "search_tool"
138
+ mock_tool_1.display_name.return_value = "Search Tool"
139
+
140
+ mock_tool_2 = MagicMock(spec=["name", "display_name"])
141
+ mock_tool_2.name = "web_search"
142
+ mock_tool_2.display_name.return_value = "Web Search"
143
+
144
+ mock_unique_ai._tool_manager.available_tools = [mock_tool_1, mock_tool_2]
145
+
146
+ mock_tool_call_1 = MagicMock(spec=["name"])
147
+ mock_tool_call_1.name = "search_tool"
148
+
149
+ mock_tool_call_2 = MagicMock(spec=["name"])
150
+ mock_tool_call_2.name = "web_search"
151
+
152
+ tool_calls = [mock_tool_call_1, mock_tool_call_2]
153
+
154
+ # Act
155
+ mock_unique_ai._log_tool_calls(tool_calls)
156
+
157
+ # Assert
158
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
159
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 2
160
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_1)
161
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_2)
162
+
163
+ assert isinstance(
164
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
165
+ int,
166
+ )
167
+ assert (
168
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 1
169
+ )
170
+ mock_unique_ai._message_step_logger.create_message_log_entry.assert_called_once_with(
171
+ text="**Triggered Tool Calls:**\n \n• Search Tool\n• Web Search",
172
+ references=[],
173
+ )
174
+
175
+ @pytest.mark.ai
176
+ def test_log_tool_calls__uses_tool_name__when_tool_not_in_available_tools(
177
+ self, mock_unique_ai: UniqueAI
178
+ ) -> None:
179
+ """
180
+ Purpose: Verify that _log_tool_calls does not create a message log entry when the tool is not found in available_tools.
181
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with no matching tools, and provide a tool call with a name not in available_tools.
182
+ """
183
+ # Arrange
184
+ mock_tool = MagicMock(spec=["name", "display_name"])
185
+ mock_tool.name = "other_tool"
186
+ mock_tool.display_name.return_value = "Other Tool"
187
+
188
+ mock_unique_ai._tool_manager.available_tools = [mock_tool]
189
+
190
+ mock_tool_call = MagicMock(spec=["name"])
191
+ mock_tool_call.name = "unknown_tool"
192
+
193
+ tool_calls = [mock_tool_call]
194
+
195
+ # Act
196
+ mock_unique_ai._log_tool_calls(tool_calls)
197
+
198
+ # Assert
199
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
200
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 1
201
+ mock_unique_ai._history_manager.add_tool_call.assert_called_once_with(
202
+ mock_tool_call
203
+ )
204
+
205
+ assert isinstance(
206
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
207
+ int,
208
+ )
209
+ # Tool call not in available_tools should not create a log entry
210
+ assert (
211
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 0
212
+ )
213
+
214
+ @pytest.mark.ai
215
+ def test_log_tool_calls__uses_tool_name__when_display_name_is_falsy(
216
+ self, mock_unique_ai: UniqueAI
217
+ ) -> None:
218
+ """
219
+ Purpose: Verify that _log_tool_calls falls back to the tool call's name when display_name returns a falsy value.
220
+
221
+ Why this matters: The function must handle edge cases where display_name might return None or empty string, ensuring the log entry always contains meaningful information.
222
+
223
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with a tool that returns a falsy display_name, and provide a matching tool call.
224
+ """
225
+ # Arrange
226
+ mock_tool = MagicMock(spec=["name", "display_name"])
227
+ mock_tool.name = "search_tool"
228
+ mock_tool.display_name.return_value = None
229
+
230
+ mock_unique_ai._tool_manager.available_tools = [mock_tool]
231
+
232
+ mock_tool_call = MagicMock(spec=["name"])
233
+ mock_tool_call.name = "search_tool"
234
+
235
+ tool_calls = [mock_tool_call]
236
+
237
+ # Act
238
+ mock_unique_ai._log_tool_calls(tool_calls)
239
+
240
+ # Assert
241
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
242
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 1
243
+ mock_unique_ai._history_manager.add_tool_call.assert_called_once_with(
244
+ mock_tool_call
245
+ )
246
+
247
+ assert isinstance(
248
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
249
+ int,
250
+ )
251
+ assert (
252
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 1
253
+ )
254
+ # When display_name is falsy, should use tool_call.name
255
+ mock_unique_ai._message_step_logger.create_message_log_entry.assert_called_once_with(
256
+ text="**Triggered Tool Calls:**\n \n• search_tool", references=[]
257
+ )
258
+
259
+ @pytest.mark.ai
260
+ def test_log_tool_calls__handles_empty_list__without_error(
261
+ self, mock_unique_ai: UniqueAI
262
+ ) -> None:
263
+ """
264
+ Purpose: Verify that _log_tool_calls handles an empty tool_calls list without raising errors and does not create a log entry.
265
+
266
+ Why this matters: The function should be resilient to edge cases like empty input, ensuring the system continues to function even when no tool calls are present.
267
+
268
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with available tools, and provide an empty tool_calls list.
269
+ """
270
+ # Arrange
271
+ mock_tool = MagicMock(spec=["name", "display_name"])
272
+ mock_tool.name = "search_tool"
273
+ mock_tool.display_name.return_value = "Search Tool"
274
+
275
+ mock_unique_ai._tool_manager.available_tools = [mock_tool]
276
+
277
+ tool_calls: list = []
278
+
279
+ # Act
280
+ mock_unique_ai._log_tool_calls(tool_calls)
281
+
282
+ # Assert
283
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
284
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 0
285
+
286
+ assert isinstance(
287
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
288
+ int,
289
+ )
290
+ # Empty tool_calls list should not create a log entry
291
+ assert (
292
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 0
293
+ )
294
+
295
+ @pytest.mark.ai
296
+ def test_log_tool_calls__adds_all_tool_calls_to_history__regardless_of_availability(
297
+ self, mock_unique_ai: UniqueAI
298
+ ) -> None:
299
+ """
300
+ Purpose: Verify that _log_tool_calls adds all tool calls to history, even if they are not in the available_tools list.
301
+
302
+ Why this matters: History tracking should be complete and independent of whether tools are available for display name lookup.
303
+
304
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with some available tools, and provide tool calls including both available and unavailable tools.
305
+ """
306
+ # Arrange
307
+ mock_tool = MagicMock(spec=["name", "display_name"])
308
+ mock_tool.name = "search_tool"
309
+ mock_tool.display_name.return_value = "Search Tool"
310
+
311
+ mock_unique_ai._tool_manager.available_tools = [mock_tool]
312
+
313
+ mock_tool_call_1 = MagicMock(spec=["name"])
314
+ mock_tool_call_1.name = "search_tool"
315
+
316
+ mock_tool_call_2 = MagicMock(spec=["name"])
317
+ mock_tool_call_2.name = "unknown_tool"
318
+
319
+ tool_calls = [mock_tool_call_1, mock_tool_call_2]
320
+
321
+ # Act
322
+ mock_unique_ai._log_tool_calls(tool_calls)
323
+
324
+ # Assert
325
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
326
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 2
327
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_1)
328
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_2)
329
+
330
+ assert isinstance(
331
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
332
+ int,
333
+ )
334
+ assert (
335
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 1
336
+ )
337
+
338
+ @pytest.mark.ai
339
+ def test_log_tool_calls__excludes_deep_research_from_message_log__but_adds_to_history(
340
+ self, mock_unique_ai: UniqueAI
341
+ ) -> None:
342
+ """
343
+ Purpose: Verify that _log_tool_calls excludes "DeepResearch" from the message log entry but still adds it to history.
344
+
345
+ Why this matters: DeepResearch tool calls should be tracked in history for completeness but hidden from the user-facing message steps to avoid clutter or confusion.
346
+
347
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with "DeepResearch" and other tools, and provide tool calls including "DeepResearch".
348
+ """
349
+ # Arrange
350
+ mock_tool_1 = MagicMock(spec=["name", "display_name"])
351
+ mock_tool_1.name = "DeepResearch"
352
+ mock_tool_1.display_name.return_value = "Deep Research"
353
+
354
+ mock_tool_2 = MagicMock(spec=["name", "display_name"])
355
+ mock_tool_2.name = "search_tool"
356
+ mock_tool_2.display_name.return_value = "Search Tool"
357
+
358
+ mock_unique_ai._tool_manager.available_tools = [mock_tool_1, mock_tool_2]
359
+
360
+ mock_tool_call_1 = MagicMock(spec=["name"])
361
+ mock_tool_call_1.name = "DeepResearch"
362
+
363
+ mock_tool_call_2 = MagicMock(spec=["name"])
364
+ mock_tool_call_2.name = "search_tool"
365
+
366
+ tool_calls = [mock_tool_call_1, mock_tool_call_2]
367
+
368
+ # Act
369
+ mock_unique_ai._log_tool_calls(tool_calls)
370
+
371
+ # Assert
372
+ # Both tool calls should be added to history
373
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
374
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 2
375
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_1)
376
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_2)
377
+
378
+ # But only the non-DeepResearch tool should appear in the message log
379
+ assert isinstance(
380
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
381
+ int,
382
+ )
383
+ assert (
384
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 1
385
+ )
386
+ mock_unique_ai._message_step_logger.create_message_log_entry.assert_called_once_with(
387
+ text="**Triggered Tool Calls:**\n \n• Search Tool", references=[]
388
+ )
389
+
390
+ @pytest.mark.ai
391
+ def test_log_tool_calls__excludes_only_deep_research_when_present__with_multiple_tools(
392
+ self, mock_unique_ai: UniqueAI
393
+ ) -> None:
394
+ """
395
+ Purpose: Verify that _log_tool_calls only excludes "DeepResearch" and logs all other tools when multiple tools are called.
396
+
397
+ Why this matters: The exclusion logic should be precise, affecting only "DeepResearch" while allowing all other tools to be logged normally.
398
+
399
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with multiple tools including "DeepResearch", and provide multiple tool calls.
400
+ """
401
+ # Arrange
402
+ mock_tool_1 = MagicMock(spec=["name", "display_name"])
403
+ mock_tool_1.name = "DeepResearch"
404
+ mock_tool_1.display_name.return_value = "Deep Research"
405
+
406
+ mock_tool_2 = MagicMock(spec=["name", "display_name"])
407
+ mock_tool_2.name = "search_tool"
408
+ mock_tool_2.display_name.return_value = "Search Tool"
409
+
410
+ mock_tool_3 = MagicMock(spec=["name", "display_name"])
411
+ mock_tool_3.name = "web_search"
412
+ mock_tool_3.display_name.return_value = "Web Search"
413
+
414
+ mock_unique_ai._tool_manager.available_tools = [
415
+ mock_tool_1,
416
+ mock_tool_2,
417
+ mock_tool_3,
418
+ ]
419
+
420
+ mock_tool_call_1 = MagicMock(spec=["name"])
421
+ mock_tool_call_1.name = "search_tool"
422
+
423
+ mock_tool_call_2 = MagicMock(spec=["name"])
424
+ mock_tool_call_2.name = "DeepResearch"
425
+
426
+ mock_tool_call_3 = MagicMock(spec=["name"])
427
+ mock_tool_call_3.name = "web_search"
428
+
429
+ tool_calls = [mock_tool_call_1, mock_tool_call_2, mock_tool_call_3]
430
+
431
+ # Act
432
+ mock_unique_ai._log_tool_calls(tool_calls)
433
+
434
+ # Assert
435
+ # All tool calls should be added to history
436
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
437
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 3
438
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_1)
439
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_2)
440
+ mock_unique_ai._history_manager.add_tool_call.assert_any_call(mock_tool_call_3)
441
+
442
+ # But only the non-DeepResearch tools should appear in the message log
443
+ assert isinstance(
444
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
445
+ int,
446
+ )
447
+ assert (
448
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 1
449
+ )
450
+ mock_unique_ai._message_step_logger.create_message_log_entry.assert_called_once_with(
451
+ text="**Triggered Tool Calls:**\n \n• Search Tool\n• Web Search",
452
+ references=[],
453
+ )
454
+
455
+ @pytest.mark.ai
456
+ def test_log_tool_calls__creates_no_log_entry__when_only_deep_research_called(
457
+ self, mock_unique_ai: UniqueAI
458
+ ) -> None:
459
+ """
460
+ Purpose: Verify that _log_tool_calls does not create a message log entry when only "DeepResearch" is called.
461
+
462
+ Why this matters: When only excluded tools are called, no message log entry should be created since there's nothing to display to the user.
463
+
464
+ Setup summary: Create a UniqueAI instance with mocked dependencies, set up a tool manager with "DeepResearch", and provide only a DeepResearch tool call.
465
+ """
466
+ # Arrange
467
+ mock_tool = MagicMock(spec=["name", "display_name"])
468
+ mock_tool.name = "DeepResearch"
469
+ mock_tool.display_name.return_value = "Deep Research"
470
+
471
+ mock_unique_ai._tool_manager.available_tools = [mock_tool]
472
+
473
+ mock_tool_call = MagicMock(spec=["name"])
474
+ mock_tool_call.name = "DeepResearch"
475
+
476
+ tool_calls = [mock_tool_call]
477
+
478
+ # Act
479
+ mock_unique_ai._log_tool_calls(tool_calls)
480
+
481
+ # Assert
482
+ # DeepResearch should be added to history
483
+ assert isinstance(mock_unique_ai._history_manager.add_tool_call.call_count, int)
484
+ assert mock_unique_ai._history_manager.add_tool_call.call_count == 1
485
+ mock_unique_ai._history_manager.add_tool_call.assert_called_once_with(
486
+ mock_tool_call
487
+ )
488
+
489
+ # But no message log entry should be created since tool_string is empty
490
+ assert isinstance(
491
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count,
492
+ int,
493
+ )
494
+ assert (
495
+ mock_unique_ai._message_step_logger.create_message_log_entry.call_count == 0
496
+ )
@@ -74,6 +74,11 @@ async def test_history_updated_before_reference_extraction(monkeypatch):
74
74
  mock_history_manager.get_history_for_model_call = AsyncMock(
75
75
  return_value=MagicMock()
76
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()
77
82
 
78
83
  # Instantiate
79
84
  ua = UniqueAI(
@@ -83,12 +88,14 @@ async def test_history_updated_before_reference_extraction(monkeypatch):
83
88
  chat_service=mock_chat_service,
84
89
  content_service=mock_content_service,
85
90
  debug_info_manager=mock_debug_info_manager,
91
+ streaming_handler=mock_streaming_handler,
86
92
  reference_manager=mock_reference_manager,
87
93
  thinking_manager=mock_thinking_manager,
88
94
  tool_manager=mock_tool_manager,
89
95
  history_manager=mock_history_manager,
90
96
  evaluation_manager=MagicMock(),
91
97
  postprocessor_manager=MagicMock(),
98
+ message_step_logger=mock_message_step_logger,
92
99
  mcp_servers=[],
93
100
  )
94
101
 
@@ -107,7 +114,7 @@ async def test_history_updated_before_reference_extraction(monkeypatch):
107
114
  def record_reference_extract(results):
108
115
  call_sequence.append("reference_extract")
109
116
 
110
- def record_debug_extract(results):
117
+ def record_debug_extract(results, iteration_index):
111
118
  call_sequence.append("debug_extract")
112
119
 
113
120
  mock_history_manager.add_tool_call_results.side_effect = record_history_add