unique_orchestrator 2026.22.0.dev7__tar.gz → 2026.22.0.dev8__tar.gz
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.
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/PKG-INFO +1 -1
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/pyproject.toml +1 -1
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +5 -3
- unique_orchestrator-2026.22.0.dev8/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py +424 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/unique_ai.py +23 -3
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/README.md +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/__init__.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/_builders/__init__.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/_builders/open_file_setup.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/_builders/skill_setup.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/config.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/settings.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/tests/test_utils_resolve_other_options.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/unique_ai_builder.py +0 -0
- {unique_orchestrator-2026.22.0.dev7 → unique_orchestrator-2026.22.0.dev8}/unique_orchestrator/utils.py +0 -0
|
@@ -727,9 +727,11 @@ class TestRunExecutionTimingIntegration:
|
|
|
727
727
|
|
|
728
728
|
await ua.run()
|
|
729
729
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
730
|
+
calls_by_key = {
|
|
731
|
+
args[0][0]: args[0][1] for args in ua._debug_info_manager.add.call_args_list
|
|
732
|
+
}
|
|
733
|
+
assert "execution_time" in calls_by_key
|
|
734
|
+
payload = calls_by_key["execution_time"]
|
|
733
735
|
assert "loop_iterations" in payload
|
|
734
736
|
assert "total_time" in payload
|
|
735
737
|
|
unique_orchestrator-2026.22.0.dev8/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""Tests for _loop_debug_params — per-iteration debug info tracking.
|
|
2
|
+
|
|
3
|
+
Covers initialisation, per-call accumulation in _plan_or_execute (both
|
|
4
|
+
completions-API and responses-API thinking-level extraction), and the final
|
|
5
|
+
write of ``loop_params`` to the debug info manager in run().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Shared helpers
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_unique_ai(**overrides):
|
|
18
|
+
"""Return a minimal UniqueAI with all external dependencies mocked."""
|
|
19
|
+
from unique_orchestrator.unique_ai import UniqueAI
|
|
20
|
+
|
|
21
|
+
mock_tool_manager = MagicMock()
|
|
22
|
+
mock_tool_manager.available_tools = []
|
|
23
|
+
mock_tool_manager.get_forced_tools.return_value = []
|
|
24
|
+
mock_tool_manager.get_tool_definitions.return_value = []
|
|
25
|
+
mock_tool_manager.filter_duplicate_tool_calls.side_effect = lambda x: x
|
|
26
|
+
mock_tool_manager.filter_tool_calls_by_max_tool_calls_allowed.side_effect = (
|
|
27
|
+
lambda x: x
|
|
28
|
+
)
|
|
29
|
+
mock_tool_manager.does_a_tool_take_control.return_value = False
|
|
30
|
+
mock_tool_manager.should_stop_after_tool_calls.return_value = False
|
|
31
|
+
mock_tool_manager.get_evaluation_check_list.return_value = []
|
|
32
|
+
|
|
33
|
+
mock_chat_service = MagicMock()
|
|
34
|
+
mock_chat_service.update_debug_info_async = AsyncMock(return_value=None)
|
|
35
|
+
mock_chat_service.get_debug_info_async = AsyncMock(return_value={})
|
|
36
|
+
|
|
37
|
+
dummy_event = MagicMock()
|
|
38
|
+
dummy_event.payload.assistant_message.id = "assist_1"
|
|
39
|
+
dummy_event.payload.user_message.text = "query"
|
|
40
|
+
dummy_event.payload.assistant_id = "assistant_123"
|
|
41
|
+
dummy_event.payload.name = "TestAssistant"
|
|
42
|
+
dummy_event.payload.user_metadata = {}
|
|
43
|
+
dummy_event.payload.tool_parameters = {}
|
|
44
|
+
|
|
45
|
+
mock_config = MagicMock()
|
|
46
|
+
mock_config.agent.prompt_config.user_metadata = []
|
|
47
|
+
|
|
48
|
+
mock_debug_info_manager = MagicMock()
|
|
49
|
+
mock_debug_info_manager.get.return_value = {}
|
|
50
|
+
|
|
51
|
+
defaults = dict(
|
|
52
|
+
logger=MagicMock(),
|
|
53
|
+
event=dummy_event,
|
|
54
|
+
config=mock_config,
|
|
55
|
+
chat_service=mock_chat_service,
|
|
56
|
+
content_service=MagicMock(),
|
|
57
|
+
debug_info_manager=mock_debug_info_manager,
|
|
58
|
+
streaming_handler=MagicMock(),
|
|
59
|
+
reference_manager=MagicMock(),
|
|
60
|
+
thinking_manager=MagicMock(),
|
|
61
|
+
tool_manager=mock_tool_manager,
|
|
62
|
+
history_manager=MagicMock(),
|
|
63
|
+
evaluation_manager=MagicMock(),
|
|
64
|
+
postprocessor_manager=MagicMock(),
|
|
65
|
+
message_step_logger=MagicMock(),
|
|
66
|
+
mcp_servers=[],
|
|
67
|
+
loop_iteration_runner=MagicMock(),
|
|
68
|
+
)
|
|
69
|
+
defaults.update(overrides)
|
|
70
|
+
return UniqueAI(**defaults)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _make_loop_response() -> MagicMock:
|
|
74
|
+
response = MagicMock()
|
|
75
|
+
response.tool_calls = None
|
|
76
|
+
response.message = MagicMock()
|
|
77
|
+
response.message.references = []
|
|
78
|
+
response.message.text = "response text"
|
|
79
|
+
response.is_empty.return_value = False
|
|
80
|
+
return response
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _call_plan_or_execute(ua, monkeypatch, other_options: dict) -> None:
|
|
84
|
+
"""Call _plan_or_execute with a patched resolve_other_options."""
|
|
85
|
+
monkeypatch.setattr(
|
|
86
|
+
"unique_orchestrator.unique_ai.resolve_other_options",
|
|
87
|
+
lambda *_args, **_kwargs: other_options,
|
|
88
|
+
)
|
|
89
|
+
ua._compose_message_plan_execution = AsyncMock(return_value=[]) # type: ignore[method-assign]
|
|
90
|
+
ua._loop_iteration_runner = AsyncMock(return_value=_make_loop_response())
|
|
91
|
+
await ua._plan_or_execute()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Initialisation
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestLoopDebugParamsInit:
|
|
100
|
+
@pytest.mark.ai
|
|
101
|
+
def test_init__loop_debug_params__is_empty_list(self) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Purpose: Verify that _loop_debug_params is initialised as an empty list.
|
|
104
|
+
Why this matters: An uninitialised attribute causes AttributeError on the
|
|
105
|
+
first append inside _plan_or_execute.
|
|
106
|
+
Setup summary: Freshly constructed UniqueAI; assert _loop_debug_params == [].
|
|
107
|
+
"""
|
|
108
|
+
ua = _build_unique_ai()
|
|
109
|
+
assert ua._loop_debug_params == []
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
# _plan_or_execute — thinking-level extraction
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class TestPlanOrExecuteLoopDebugParams:
|
|
118
|
+
@pytest.mark.ai
|
|
119
|
+
@pytest.mark.asyncio
|
|
120
|
+
async def test_plan_or_execute__completions_api__reasoning_effort_stored(
|
|
121
|
+
self, monkeypatch
|
|
122
|
+
) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Purpose: Verify that a ``reasoning_effort`` value from the completions API
|
|
125
|
+
is stored as ``thinking_level`` in the appended entry.
|
|
126
|
+
Why this matters: The completions API uses a flat ``reasoning_effort`` key;
|
|
127
|
+
the extractor must read from there, not from ``reasoning.effort``.
|
|
128
|
+
Setup summary: Patch resolve_other_options to return
|
|
129
|
+
``{'reasoning_effort': 'high'}``; call _plan_or_execute; assert
|
|
130
|
+
_loop_debug_params[0]['thinking_level'] == 'high'.
|
|
131
|
+
"""
|
|
132
|
+
ua = _build_unique_ai()
|
|
133
|
+
await _call_plan_or_execute(ua, monkeypatch, {"reasoning_effort": "high"})
|
|
134
|
+
|
|
135
|
+
assert len(ua._loop_debug_params) == 1
|
|
136
|
+
assert ua._loop_debug_params[0]["thinking_level"] == "high"
|
|
137
|
+
|
|
138
|
+
@pytest.mark.ai
|
|
139
|
+
@pytest.mark.asyncio
|
|
140
|
+
async def test_plan_or_execute__responses_api__reasoning_effort_stored(
|
|
141
|
+
self, monkeypatch
|
|
142
|
+
) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Purpose: Verify that ``reasoning.effort`` from the responses API is stored
|
|
145
|
+
as ``thinking_level``.
|
|
146
|
+
Why this matters: The responses API nests the effort inside a ``reasoning``
|
|
147
|
+
dict; failing to look there would always produce None for responses-API calls.
|
|
148
|
+
Setup summary: Patch resolve_other_options to return
|
|
149
|
+
``{'reasoning': {'effort': 'low'}}``; assert thinking_level == 'low'.
|
|
150
|
+
"""
|
|
151
|
+
ua = _build_unique_ai()
|
|
152
|
+
await _call_plan_or_execute(ua, monkeypatch, {"reasoning": {"effort": "low"}})
|
|
153
|
+
|
|
154
|
+
assert len(ua._loop_debug_params) == 1
|
|
155
|
+
assert ua._loop_debug_params[0]["thinking_level"] == "low"
|
|
156
|
+
|
|
157
|
+
@pytest.mark.ai
|
|
158
|
+
@pytest.mark.asyncio
|
|
159
|
+
async def test_plan_or_execute__no_thinking_level__none_string_stored(
|
|
160
|
+
self, monkeypatch
|
|
161
|
+
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Purpose: Verify that when resolve_other_options returns no reasoning-related
|
|
164
|
+
keys, thinking_level is stored as the string "None".
|
|
165
|
+
Why this matters: Using the string "None" instead of Python None keeps the
|
|
166
|
+
debug payload consistently typed as str, which serialises cleanly to JSON
|
|
167
|
+
and is unambiguous in log output.
|
|
168
|
+
Setup summary: Patch resolve_other_options to return {}; assert
|
|
169
|
+
thinking_level == "None".
|
|
170
|
+
"""
|
|
171
|
+
ua = _build_unique_ai()
|
|
172
|
+
await _call_plan_or_execute(ua, monkeypatch, {})
|
|
173
|
+
|
|
174
|
+
assert len(ua._loop_debug_params) == 1
|
|
175
|
+
assert ua._loop_debug_params[0]["thinking_level"] == "None"
|
|
176
|
+
|
|
177
|
+
@pytest.mark.ai
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_plan_or_execute__loop_number__matches_current_iteration_index(
|
|
180
|
+
self, monkeypatch
|
|
181
|
+
) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Purpose: Verify that loop_number in the appended entry equals
|
|
184
|
+
current_iteration_index at call time.
|
|
185
|
+
Why this matters: Consumers use loop_number to correlate debug params with
|
|
186
|
+
execution timing entries; a wrong value breaks that correlation.
|
|
187
|
+
Setup summary: Set current_iteration_index to 3; call _plan_or_execute;
|
|
188
|
+
assert loop_number == 3.
|
|
189
|
+
"""
|
|
190
|
+
ua = _build_unique_ai()
|
|
191
|
+
ua.current_iteration_index = 3
|
|
192
|
+
await _call_plan_or_execute(ua, monkeypatch, {"reasoning_effort": "medium"})
|
|
193
|
+
|
|
194
|
+
assert ua._loop_debug_params[0]["loop_number"] == 3
|
|
195
|
+
|
|
196
|
+
@pytest.mark.ai
|
|
197
|
+
@pytest.mark.asyncio
|
|
198
|
+
async def test_plan_or_execute__multiple_calls__accumulates_entries_in_order(
|
|
199
|
+
self, monkeypatch
|
|
200
|
+
) -> None:
|
|
201
|
+
"""
|
|
202
|
+
Purpose: Verify that repeated calls across iterations append independent
|
|
203
|
+
entries in the correct order.
|
|
204
|
+
Why this matters: A multi-iteration run must produce one entry per iteration
|
|
205
|
+
with no overwrites or entry merges.
|
|
206
|
+
Setup summary: Call _plan_or_execute twice with different iteration indices
|
|
207
|
+
and thinking levels; assert len == 2 and each entry has the correct values.
|
|
208
|
+
"""
|
|
209
|
+
ua = _build_unique_ai()
|
|
210
|
+
|
|
211
|
+
monkeypatch.setattr(
|
|
212
|
+
"unique_orchestrator.unique_ai.resolve_other_options",
|
|
213
|
+
lambda *_args, **_kwargs: {"reasoning_effort": "high"},
|
|
214
|
+
)
|
|
215
|
+
ua._compose_message_plan_execution = AsyncMock(return_value=[]) # type: ignore[method-assign]
|
|
216
|
+
ua._loop_iteration_runner = AsyncMock(return_value=_make_loop_response())
|
|
217
|
+
ua.current_iteration_index = 0
|
|
218
|
+
await ua._plan_or_execute()
|
|
219
|
+
|
|
220
|
+
monkeypatch.setattr(
|
|
221
|
+
"unique_orchestrator.unique_ai.resolve_other_options",
|
|
222
|
+
lambda *_args, **_kwargs: {"reasoning": {"effort": "low"}},
|
|
223
|
+
)
|
|
224
|
+
ua.current_iteration_index = 1
|
|
225
|
+
await ua._plan_or_execute()
|
|
226
|
+
|
|
227
|
+
assert len(ua._loop_debug_params) == 2
|
|
228
|
+
assert ua._loop_debug_params[0] == {"loop_number": 0, "thinking_level": "high"}
|
|
229
|
+
assert ua._loop_debug_params[1] == {"loop_number": 1, "thinking_level": "low"}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# run() integration — loop_params written to debug info manager
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class TestRunLoopDebugParams:
|
|
238
|
+
def _build_run_ua(self, monkeypatch):
|
|
239
|
+
mock_feature_flags = MagicMock()
|
|
240
|
+
monkeypatch.setattr(
|
|
241
|
+
"unique_orchestrator.unique_ai.feature_flags", mock_feature_flags
|
|
242
|
+
)
|
|
243
|
+
monkeypatch.setattr(
|
|
244
|
+
"unique_orchestrator.unique_ai.resolve_other_options",
|
|
245
|
+
lambda *_args, **_kwargs: {"reasoning_effort": "high"},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
mock_cancellation = MagicMock()
|
|
249
|
+
mock_cancellation.is_cancelled = False
|
|
250
|
+
mock_cancellation.on_cancellation.subscribe = MagicMock(
|
|
251
|
+
return_value=MagicMock()
|
|
252
|
+
)
|
|
253
|
+
mock_cancellation.check_cancellation_async = AsyncMock(return_value=False)
|
|
254
|
+
|
|
255
|
+
mock_chat_service = MagicMock()
|
|
256
|
+
mock_chat_service.cancellation = mock_cancellation
|
|
257
|
+
mock_chat_service.get_debug_info_async = AsyncMock(return_value={})
|
|
258
|
+
mock_chat_service.update_debug_info_async = AsyncMock(return_value=None)
|
|
259
|
+
mock_chat_service.modify_assistant_message_async = AsyncMock(return_value=None)
|
|
260
|
+
mock_chat_service.create_assistant_message_async = AsyncMock(
|
|
261
|
+
return_value=MagicMock(id="assist_new")
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
mock_tool_manager = MagicMock()
|
|
265
|
+
mock_tool_manager.available_tools = []
|
|
266
|
+
mock_tool_manager.get_forced_tools.return_value = []
|
|
267
|
+
mock_tool_manager.get_tool_definitions.return_value = []
|
|
268
|
+
mock_tool_manager.filter_duplicate_tool_calls.side_effect = lambda x: x
|
|
269
|
+
mock_tool_manager.filter_tool_calls_by_max_tool_calls_allowed.side_effect = (
|
|
270
|
+
lambda x: x
|
|
271
|
+
)
|
|
272
|
+
mock_tool_manager.does_a_tool_take_control.return_value = False
|
|
273
|
+
mock_tool_manager.should_stop_after_tool_calls.return_value = False
|
|
274
|
+
mock_tool_manager.get_evaluation_check_list.return_value = []
|
|
275
|
+
|
|
276
|
+
mock_debug_info_manager = MagicMock()
|
|
277
|
+
mock_debug_info_manager.get.return_value = {"tools": []}
|
|
278
|
+
|
|
279
|
+
mock_config = MagicMock()
|
|
280
|
+
mock_config.effective_max_loop_iterations = 1
|
|
281
|
+
mock_config.agent.prompt_config.user_metadata = []
|
|
282
|
+
|
|
283
|
+
mock_history_manager = MagicMock()
|
|
284
|
+
mock_history_manager.get_history_for_model_call = AsyncMock(
|
|
285
|
+
return_value=MagicMock()
|
|
286
|
+
)
|
|
287
|
+
mock_history_manager.add_tool_call_results = MagicMock()
|
|
288
|
+
mock_history_manager.extract_message_tools.return_value = []
|
|
289
|
+
|
|
290
|
+
mock_postprocessor_manager = MagicMock()
|
|
291
|
+
mock_postprocessor_manager.run_postprocessors = AsyncMock(return_value=None)
|
|
292
|
+
mock_postprocessor_manager.get_execution_times.return_value = {}
|
|
293
|
+
|
|
294
|
+
mock_evaluation_manager = MagicMock()
|
|
295
|
+
mock_evaluation_manager.run_evaluations = AsyncMock(return_value=[])
|
|
296
|
+
mock_evaluation_manager.get_execution_times.return_value = {}
|
|
297
|
+
|
|
298
|
+
dummy_event = MagicMock()
|
|
299
|
+
dummy_event.payload.assistant_message.id = "assist_1"
|
|
300
|
+
dummy_event.payload.user_message.text = "query"
|
|
301
|
+
dummy_event.payload.assistant_id = "assistant_123"
|
|
302
|
+
dummy_event.payload.name = "TestAssistant"
|
|
303
|
+
dummy_event.payload.user_metadata = {}
|
|
304
|
+
dummy_event.payload.tool_parameters = {}
|
|
305
|
+
dummy_event.company_id = "company_1"
|
|
306
|
+
|
|
307
|
+
loop_response = _make_loop_response()
|
|
308
|
+
|
|
309
|
+
from unique_orchestrator.unique_ai import UniqueAI
|
|
310
|
+
|
|
311
|
+
ua = UniqueAI(
|
|
312
|
+
logger=MagicMock(),
|
|
313
|
+
event=dummy_event,
|
|
314
|
+
config=mock_config,
|
|
315
|
+
chat_service=mock_chat_service,
|
|
316
|
+
content_service=MagicMock(),
|
|
317
|
+
debug_info_manager=mock_debug_info_manager,
|
|
318
|
+
streaming_handler=MagicMock(),
|
|
319
|
+
reference_manager=MagicMock(),
|
|
320
|
+
thinking_manager=MagicMock(),
|
|
321
|
+
tool_manager=mock_tool_manager,
|
|
322
|
+
history_manager=mock_history_manager,
|
|
323
|
+
evaluation_manager=mock_evaluation_manager,
|
|
324
|
+
postprocessor_manager=mock_postprocessor_manager,
|
|
325
|
+
message_step_logger=MagicMock(),
|
|
326
|
+
mcp_servers=[],
|
|
327
|
+
loop_iteration_runner=AsyncMock(return_value=loop_response),
|
|
328
|
+
)
|
|
329
|
+
ua._render_user_prompt = AsyncMock(return_value="user") # type: ignore[method-assign]
|
|
330
|
+
ua._render_system_prompt = AsyncMock(return_value="system") # type: ignore[method-assign]
|
|
331
|
+
ua._thinking_manager.thinking_is_displayed = MagicMock(return_value=True)
|
|
332
|
+
return ua
|
|
333
|
+
|
|
334
|
+
@pytest.mark.ai
|
|
335
|
+
@pytest.mark.asyncio
|
|
336
|
+
async def test_run__debug_info_manager__receives_loop_params_key(
|
|
337
|
+
self, monkeypatch
|
|
338
|
+
) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Purpose: Verify that run() calls debug_info_manager.add with 'loop_params'
|
|
341
|
+
after the agentic loop completes.
|
|
342
|
+
Why this matters: Without this call, loop-level debug params are never
|
|
343
|
+
persisted and the feature has no observable effect.
|
|
344
|
+
Setup summary: Run one iteration; assert add was called with 'loop_params'
|
|
345
|
+
as the first argument.
|
|
346
|
+
"""
|
|
347
|
+
ua = self._build_run_ua(monkeypatch)
|
|
348
|
+
|
|
349
|
+
await ua.run()
|
|
350
|
+
|
|
351
|
+
keys_written = [
|
|
352
|
+
call.args[0] for call in ua._debug_info_manager.add.call_args_list
|
|
353
|
+
]
|
|
354
|
+
assert "loop_params" in keys_written
|
|
355
|
+
|
|
356
|
+
@pytest.mark.ai
|
|
357
|
+
@pytest.mark.asyncio
|
|
358
|
+
async def test_run__loop_params__contains_one_entry_per_iteration(
|
|
359
|
+
self, monkeypatch
|
|
360
|
+
) -> None:
|
|
361
|
+
"""
|
|
362
|
+
Purpose: Verify that the loop_params value written to the debug info manager
|
|
363
|
+
contains exactly one entry for a single-iteration run.
|
|
364
|
+
Why this matters: The entry count must match the iteration count so that
|
|
365
|
+
loop_params and execution_time.loop_iterations stay in sync.
|
|
366
|
+
Setup summary: Run with effective_max_loop_iterations=1; find the loop_params
|
|
367
|
+
add call; assert the value is a list of length 1.
|
|
368
|
+
"""
|
|
369
|
+
ua = self._build_run_ua(monkeypatch)
|
|
370
|
+
|
|
371
|
+
await ua.run()
|
|
372
|
+
|
|
373
|
+
loop_params_call = next(
|
|
374
|
+
call
|
|
375
|
+
for call in ua._debug_info_manager.add.call_args_list
|
|
376
|
+
if call.args[0] == "loop_params"
|
|
377
|
+
)
|
|
378
|
+
payload = loop_params_call.args[1]
|
|
379
|
+
assert isinstance(payload, list)
|
|
380
|
+
assert len(payload) == 1
|
|
381
|
+
|
|
382
|
+
@pytest.mark.ai
|
|
383
|
+
@pytest.mark.asyncio
|
|
384
|
+
async def test_run__loop_params_entry__contains_loop_number_and_thinking_level(
|
|
385
|
+
self, monkeypatch
|
|
386
|
+
) -> None:
|
|
387
|
+
"""
|
|
388
|
+
Purpose: Verify that each entry in loop_params has the expected keys.
|
|
389
|
+
Why this matters: Consumers read these keys by name; missing keys cause
|
|
390
|
+
KeyErrors or require defensive coding across all consumers.
|
|
391
|
+
Setup summary: Run one iteration; assert the single entry has both
|
|
392
|
+
'loop_number' and 'thinking_level'.
|
|
393
|
+
"""
|
|
394
|
+
ua = self._build_run_ua(monkeypatch)
|
|
395
|
+
|
|
396
|
+
await ua.run()
|
|
397
|
+
|
|
398
|
+
loop_params_call = next(
|
|
399
|
+
call
|
|
400
|
+
for call in ua._debug_info_manager.add.call_args_list
|
|
401
|
+
if call.args[0] == "loop_params"
|
|
402
|
+
)
|
|
403
|
+
entry = loop_params_call.args[1][0]
|
|
404
|
+
assert "loop_number" in entry
|
|
405
|
+
assert "thinking_level" in entry
|
|
406
|
+
|
|
407
|
+
@pytest.mark.ai
|
|
408
|
+
@pytest.mark.asyncio
|
|
409
|
+
async def test_run__loop_debug_params__reset_on_each_run(self, monkeypatch) -> None:
|
|
410
|
+
"""
|
|
411
|
+
Purpose: Verify that _loop_debug_params is cleared at the start of each
|
|
412
|
+
run() call.
|
|
413
|
+
Why this matters: Without a reset, consecutive run() calls on the same
|
|
414
|
+
instance accumulate stale entries from previous runs, inflating the
|
|
415
|
+
loop_params list.
|
|
416
|
+
Setup summary: Run twice; assert _loop_debug_params has exactly one entry
|
|
417
|
+
after the second run (not two).
|
|
418
|
+
"""
|
|
419
|
+
ua = self._build_run_ua(monkeypatch)
|
|
420
|
+
|
|
421
|
+
await ua.run()
|
|
422
|
+
await ua.run()
|
|
423
|
+
|
|
424
|
+
assert len(ua._loop_debug_params) == 1
|
|
@@ -188,6 +188,7 @@ class UniqueAI:
|
|
|
188
188
|
|
|
189
189
|
self._execution_times: list[dict[str, Any]] = []
|
|
190
190
|
self._current_loop_timing: dict[str, Any] = {}
|
|
191
|
+
self._loop_debug_params: list[dict[str, Any]] = []
|
|
191
192
|
|
|
192
193
|
async def _on_cancellation(self, _event: CancellationEvent) -> None:
|
|
193
194
|
"""Subscriber called by the cancellation event bus."""
|
|
@@ -212,6 +213,7 @@ class UniqueAI:
|
|
|
212
213
|
)
|
|
213
214
|
|
|
214
215
|
self._execution_times = []
|
|
216
|
+
self._loop_debug_params = []
|
|
215
217
|
run_start = time.perf_counter()
|
|
216
218
|
|
|
217
219
|
await preload_invoked_skills(
|
|
@@ -306,6 +308,7 @@ class UniqueAI:
|
|
|
306
308
|
"total_time": round(time.perf_counter() - run_start, 3),
|
|
307
309
|
},
|
|
308
310
|
)
|
|
311
|
+
self._debug_info_manager.add("loop_params", self._loop_debug_params)
|
|
309
312
|
|
|
310
313
|
tool_names = [
|
|
311
314
|
tool["name"] for tool in self._debug_info_manager.get()["tools"]
|
|
@@ -328,6 +331,20 @@ class UniqueAI:
|
|
|
328
331
|
finally:
|
|
329
332
|
sub.cancel()
|
|
330
333
|
|
|
334
|
+
def _record_loop_debug_params(self, other_options: dict) -> None:
|
|
335
|
+
reasoning = other_options.get("reasoning")
|
|
336
|
+
thinking_level: str = (
|
|
337
|
+
other_options.get("reasoning_effort")
|
|
338
|
+
if not isinstance(reasoning, dict)
|
|
339
|
+
else reasoning.get("effort")
|
|
340
|
+
) or "None"
|
|
341
|
+
self._loop_debug_params.append(
|
|
342
|
+
{
|
|
343
|
+
"loop_number": self.current_iteration_index,
|
|
344
|
+
"thinking_level": thinking_level,
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
|
|
331
348
|
# @track()
|
|
332
349
|
async def _plan_or_execute(self) -> LanguageModelStreamResponse:
|
|
333
350
|
self._logger.info("Planning or executing the loop.")
|
|
@@ -335,6 +352,11 @@ class UniqueAI:
|
|
|
335
352
|
|
|
336
353
|
self._logger.info("Done composing message plan execution.")
|
|
337
354
|
|
|
355
|
+
other_options = resolve_other_options(
|
|
356
|
+
self._config, self._tool_manager, self._logger
|
|
357
|
+
)
|
|
358
|
+
self._record_loop_debug_params(other_options)
|
|
359
|
+
|
|
338
360
|
kwargs: dict = dict(
|
|
339
361
|
messages=messages,
|
|
340
362
|
iteration_index=self.current_iteration_index,
|
|
@@ -346,9 +368,7 @@ class UniqueAI:
|
|
|
346
368
|
debug_info=self._debug_info_manager.get(),
|
|
347
369
|
temperature=self._config.agent.experimental.temperature,
|
|
348
370
|
tool_choices=self._tool_manager.get_forced_tools(), # type: ignore (as above)
|
|
349
|
-
other_options=
|
|
350
|
-
self._config, self._tool_manager, self._logger
|
|
351
|
-
),
|
|
371
|
+
other_options=other_options,
|
|
352
372
|
)
|
|
353
373
|
|
|
354
374
|
# Experimental Feature UN-17905
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|