unique_orchestrator 2026.30.0.dev5__tar.gz → 2026.30.0.dev7__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.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/PKG-INFO +2 -2
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/pyproject.toml +2 -2
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/history_manager.py +5 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_async_modify.py +24 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py +28 -0
- unique_orchestrator-2026.30.0.dev7/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +76 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/unique_ai.py +28 -3
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/unique_ai_builder.py +5 -18
- unique_orchestrator-2026.30.0.dev5/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -120
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/README.md +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/__init__.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/__init__.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/open_file_setup.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/skill_setup.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/config.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/settings.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_agent_config_rjsf_ui_schema.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_utils_resolve_other_options.py +0 -0
- {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-orchestrator
|
|
3
|
-
Version: 2026.30.0.
|
|
3
|
+
Version: 2026.30.0.dev7
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Andreas Hauri
|
|
6
6
|
Author-email: Andreas Hauri <andreas.hauri@unique.ai>
|
|
@@ -9,7 +9,7 @@ Requires-Dist: pydantic>=2.8.2,<3
|
|
|
9
9
|
Requires-Dist: pydantic-settings>=2.10.1,<3
|
|
10
10
|
Requires-Dist: typing-extensions>=4.9.0,<5
|
|
11
11
|
Requires-Dist: jinja2>=3.1.0,<4
|
|
12
|
-
Requires-Dist: unique-toolkit>=2026.30.0.
|
|
12
|
+
Requires-Dist: unique-toolkit>=2026.30.0.dev19,<2026.30.0rc0
|
|
13
13
|
Requires-Dist: unique-stock-ticker>=2026.30.0.dev0,<2026.30.0rc0
|
|
14
14
|
Requires-Dist: unique-follow-up-questions>=2026.30.0.dev0,<2026.30.0rc0
|
|
15
15
|
Requires-Dist: unique-internal-search>=2026.30.0.dev0,<2026.30.0rc0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_orchestrator"
|
|
3
|
-
version = "2026.30.0.
|
|
3
|
+
version = "2026.30.0.dev7"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Proprietary" }
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
"pydantic-settings>=2.10.1,<3",
|
|
14
14
|
"typing-extensions>=4.9.0,<5",
|
|
15
15
|
"jinja2>=3.1.0,<4",
|
|
16
|
-
"unique-toolkit>=2026.30.0.
|
|
16
|
+
"unique-toolkit>=2026.30.0.dev19,<2026.30.0rc0",
|
|
17
17
|
"unique-stock-ticker>=2026.30.0.dev0,<2026.30.0rc0",
|
|
18
18
|
"unique-follow-up-questions>=2026.30.0.dev0,<2026.30.0rc0",
|
|
19
19
|
"unique-internal-search>=2026.30.0.dev0,<2026.30.0rc0",
|
|
@@ -14,6 +14,9 @@ from unique_toolkit.agentic.history_manager.history_manager import (
|
|
|
14
14
|
)
|
|
15
15
|
from unique_toolkit.agentic.reference_manager.reference_manager import ReferenceManager
|
|
16
16
|
from unique_toolkit.agentic.tools.openai_builtin.base import OpenAIBuiltInToolName
|
|
17
|
+
from unique_toolkit.agentic.tools.openai_builtin.code_interpreter import (
|
|
18
|
+
CodeInterpreterActivatorTool,
|
|
19
|
+
)
|
|
17
20
|
from unique_toolkit.agentic.tools.openai_builtin.code_interpreter.postprocessors.artifacts import (
|
|
18
21
|
load_code_execution_metadata,
|
|
19
22
|
)
|
|
@@ -64,6 +67,8 @@ def _get_file_content_serializer(
|
|
|
64
67
|
code_interpreter_available=(
|
|
65
68
|
tool_manager.get_tool_by_name(OpenAIBuiltInToolName.CODE_INTERPRETER)
|
|
66
69
|
is not None
|
|
70
|
+
or tool_manager.get_tool_by_name(CodeInterpreterActivatorTool.NAME)
|
|
71
|
+
is not None
|
|
67
72
|
),
|
|
68
73
|
)
|
|
69
74
|
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
from unittest.mock import AsyncMock, MagicMock
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
from unique_toolkit.agentic.tools.openai_builtin.code_interpreter import (
|
|
7
|
+
DisplayCodeInterpreterFilesPostProcessor,
|
|
8
|
+
)
|
|
6
9
|
|
|
7
10
|
from unique_orchestrator.unique_ai import UniqueAI
|
|
8
11
|
|
|
@@ -85,6 +88,7 @@ def _make_ua(monkeypatch, *, feature_flag_enabled: bool = False):
|
|
|
85
88
|
ua._mcp_servers = []
|
|
86
89
|
ua._execution_times = []
|
|
87
90
|
ua._current_loop_timing = {}
|
|
91
|
+
ua._generated_files_info = None
|
|
88
92
|
ua.current_iteration_index = 0
|
|
89
93
|
ua._skill_choices = []
|
|
90
94
|
|
|
@@ -126,3 +130,23 @@ async def test_process_plan_calls_modify_async_on_empty_response(monkeypatch):
|
|
|
126
130
|
|
|
127
131
|
assert result is True
|
|
128
132
|
ua._chat_service.modify_assistant_message_async.assert_called()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.ai
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
async def test_handle_no_tool_calls_stores_returned_artifacts_for_analytics(
|
|
138
|
+
monkeypatch,
|
|
139
|
+
):
|
|
140
|
+
ua = _make_ua(monkeypatch)
|
|
141
|
+
artifacts = {"count": 1, "filetypes": ["csv"]}
|
|
142
|
+
ua._postprocessor_manager.run_postprocessors.return_value = {
|
|
143
|
+
DisplayCodeInterpreterFilesPostProcessor.__name__: artifacts
|
|
144
|
+
}
|
|
145
|
+
ua._current_loop_timing = {"post_processing": {}, "evaluation": {}}
|
|
146
|
+
|
|
147
|
+
response = MagicMock()
|
|
148
|
+
response.model_copy.return_value = response
|
|
149
|
+
|
|
150
|
+
await ua._handle_no_tool_calls(response)
|
|
151
|
+
|
|
152
|
+
assert ua._generated_files_info == artifacts
|
|
@@ -478,6 +478,34 @@ class TestRunLoopDebugParams:
|
|
|
478
478
|
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
479
479
|
assert analytics_call.kwargs["total_time_to_answer_ms"] is None
|
|
480
480
|
|
|
481
|
+
@pytest.mark.ai
|
|
482
|
+
@pytest.mark.asyncio
|
|
483
|
+
async def test_run__generated_files_info__reset_at_start_prevents_stale_artifacts(
|
|
484
|
+
self, monkeypatch
|
|
485
|
+
) -> None:
|
|
486
|
+
"""
|
|
487
|
+
Purpose: Verify run() resets _generated_files_info at the start, so a rerun
|
|
488
|
+
that exits without reaching _handle_no_tool_calls does not report the previous
|
|
489
|
+
run's artifacts.
|
|
490
|
+
Why this matters: _generated_files_info persists on the UniqueAI instance;
|
|
491
|
+
without a reset it leaks a prior run's artifact count/filetypes into the
|
|
492
|
+
current answer's analytics (Cursor bot, UN-22110).
|
|
493
|
+
Setup summary: Seed a stale value, force an early loop exit via _process_plan
|
|
494
|
+
returning True (a tool took control — _handle_no_tool_calls never runs), and
|
|
495
|
+
assert add_analytics received artifacts=None.
|
|
496
|
+
"""
|
|
497
|
+
ua = self._build_run_ua(monkeypatch)
|
|
498
|
+
# Leftover from a hypothetical previous run on the same instance.
|
|
499
|
+
ua._generated_files_info = {"count": 99, "filetypes": ["stale"]}
|
|
500
|
+
# Exit the loop as if a tool took control: _handle_no_tool_calls (which sets
|
|
501
|
+
# _generated_files_info) is skipped, but finalization still calls add_analytics.
|
|
502
|
+
ua._process_plan = AsyncMock(return_value=True) # type: ignore[method-assign]
|
|
503
|
+
|
|
504
|
+
await ua.run()
|
|
505
|
+
|
|
506
|
+
analytics_call = ua._debug_info_manager.add_analytics.call_args
|
|
507
|
+
assert analytics_call.kwargs["artifacts"] is None
|
|
508
|
+
|
|
481
509
|
@pytest.mark.ai
|
|
482
510
|
@pytest.mark.asyncio
|
|
483
511
|
async def test_run__loop_params__contains_one_entry_per_iteration(
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Tests for ResponsesStreamingHandler include injection (UN-17972).
|
|
2
|
+
|
|
3
|
+
The handler always requests code_interpreter_call.outputs so UniqueAI stays
|
|
4
|
+
generic (no isinstance checks) and code interpreter execution logs attach to
|
|
5
|
+
tool calls whenever code interpreter runs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from unique_orchestrator.unique_ai_builder import (
|
|
13
|
+
ResponsesStreamingHandler,
|
|
14
|
+
_ChatServiceResponsesStreaming,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_handler() -> ResponsesStreamingHandler:
|
|
19
|
+
inner = MagicMock()
|
|
20
|
+
inner.complete_with_references_async = AsyncMock(return_value=MagicMock())
|
|
21
|
+
inner.complete_with_references = MagicMock(return_value=MagicMock())
|
|
22
|
+
return ResponsesStreamingHandler(inner=inner)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.ai
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_responses_streaming_handler__always_injects_include__async_path() -> (
|
|
28
|
+
None
|
|
29
|
+
):
|
|
30
|
+
"""complete_with_references_async always sets include=code_interpreter_call.outputs."""
|
|
31
|
+
handler = _make_handler()
|
|
32
|
+
|
|
33
|
+
await handler.complete_with_references_async(messages=[], model_name="model")
|
|
34
|
+
|
|
35
|
+
call_kwargs = handler._inner.complete_with_references_async.await_args.kwargs
|
|
36
|
+
assert call_kwargs["include"] == ["code_interpreter_call.outputs"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.ai
|
|
40
|
+
def test_responses_streaming_handler__always_injects_include__sync_path() -> None:
|
|
41
|
+
"""The synchronous complete_with_references also always sets include."""
|
|
42
|
+
handler = _make_handler()
|
|
43
|
+
|
|
44
|
+
handler.complete_with_references(messages=[], model_name="model")
|
|
45
|
+
|
|
46
|
+
call_kwargs = handler._inner.complete_with_references.call_args.kwargs
|
|
47
|
+
assert call_kwargs["include"] == ["code_interpreter_call.outputs"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.ai
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
async def test_chat_service_responses_streaming__forwards_to_responses_methods() -> (
|
|
53
|
+
None
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Purpose: The legacy adapter must route the protocol's
|
|
57
|
+
`complete_with_references[_async]` to ChatService's Responses-specific
|
|
58
|
+
methods, not the Chat Completions ones.
|
|
59
|
+
Why this matters: ChatService exposes both; routing to the wrong method
|
|
60
|
+
would silently stream through the Chat Completions API.
|
|
61
|
+
"""
|
|
62
|
+
chat_service = MagicMock()
|
|
63
|
+
chat_service.complete_responses_with_references = MagicMock(
|
|
64
|
+
return_value=MagicMock()
|
|
65
|
+
)
|
|
66
|
+
chat_service.complete_responses_with_references_async = AsyncMock(
|
|
67
|
+
return_value=MagicMock()
|
|
68
|
+
)
|
|
69
|
+
adapter = _ChatServiceResponsesStreaming(chat_service)
|
|
70
|
+
|
|
71
|
+
adapter.complete_with_references(messages=[], model_name="model")
|
|
72
|
+
await adapter.complete_with_references_async(messages=[], model_name="model")
|
|
73
|
+
|
|
74
|
+
chat_service.complete_responses_with_references.assert_called_once()
|
|
75
|
+
chat_service.complete_responses_with_references_async.assert_awaited_once()
|
|
76
|
+
chat_service.complete_with_references.assert_not_called()
|
|
@@ -32,6 +32,12 @@ from unique_toolkit.agentic.tools.experimental.open_file_tool import (
|
|
|
32
32
|
OpenFileToolRuntime,
|
|
33
33
|
OpenFileToolRuntimeConfig,
|
|
34
34
|
)
|
|
35
|
+
from unique_toolkit.agentic.tools.openai_builtin.code_interpreter import (
|
|
36
|
+
DisplayCodeInterpreterFilesPostProcessor,
|
|
37
|
+
)
|
|
38
|
+
from unique_toolkit.agentic.tools.openai_builtin.code_interpreter.postprocessors.generated_files import (
|
|
39
|
+
ArtifactsDebugInfo,
|
|
40
|
+
)
|
|
35
41
|
from unique_toolkit.agentic.tools.tool_manager import (
|
|
36
42
|
ResponsesApiToolManager,
|
|
37
43
|
SafeTaskExecutor,
|
|
@@ -197,6 +203,7 @@ class UniqueAI:
|
|
|
197
203
|
self._execution_times: list[dict[str, Any]] = []
|
|
198
204
|
self._current_loop_timing: dict[str, Any] = {}
|
|
199
205
|
self._loop_debug_params: list[dict[str, Any]] = []
|
|
206
|
+
self._generated_files_info: ArtifactsDebugInfo | None = None
|
|
200
207
|
|
|
201
208
|
async def _on_cancellation(self, _event: CancellationEvent) -> None:
|
|
202
209
|
"""Subscriber called by the cancellation event bus."""
|
|
@@ -222,6 +229,10 @@ class UniqueAI:
|
|
|
222
229
|
|
|
223
230
|
self._execution_times = []
|
|
224
231
|
self._loop_debug_params = []
|
|
232
|
+
# Reset per-run artifact analytics too: a later run that exits without
|
|
233
|
+
# reaching _handle_no_tool_calls (tool takes control / empty response /
|
|
234
|
+
# cancellation) must not report the previous run's artifacts.
|
|
235
|
+
self._generated_files_info = None
|
|
225
236
|
run_start = time.perf_counter()
|
|
226
237
|
|
|
227
238
|
await preload_invoked_skills(
|
|
@@ -363,6 +374,7 @@ class UniqueAI:
|
|
|
363
374
|
answer_length=len(self._last_assistant_text or ""),
|
|
364
375
|
loop_iteration_count=len(self._execution_times),
|
|
365
376
|
total_time_to_answer_ms=total_time_to_answer_ms,
|
|
377
|
+
artifacts=self._generated_files_info,
|
|
366
378
|
)
|
|
367
379
|
|
|
368
380
|
# Get current debug info from chat service and add debug info from run. Do not update if DeepResearch is in the tool names.
|
|
@@ -696,11 +708,19 @@ class UniqueAI:
|
|
|
696
708
|
loop_response.model_copy(deep=True),
|
|
697
709
|
)
|
|
698
710
|
|
|
699
|
-
|
|
711
|
+
postprocessor_result, evaluation_results = await asyncio.gather(
|
|
700
712
|
postprocessor_result,
|
|
701
713
|
evaluation_results,
|
|
702
714
|
)
|
|
703
|
-
|
|
715
|
+
postprocessor_outputs = postprocessor_result.unpack() or {}
|
|
716
|
+
# run_postprocessors erases each postprocessor's return type to
|
|
717
|
+
# `object | None` (generic channel), so re-narrow the one we own.
|
|
718
|
+
self._generated_files_info = cast(
|
|
719
|
+
"ArtifactsDebugInfo | None",
|
|
720
|
+
postprocessor_outputs.get(
|
|
721
|
+
DisplayCodeInterpreterFilesPostProcessor.__name__
|
|
722
|
+
),
|
|
723
|
+
)
|
|
704
724
|
self._current_loop_timing["post_processing"].update(
|
|
705
725
|
self._postprocessor_manager.get_execution_times()
|
|
706
726
|
)
|
|
@@ -755,7 +775,12 @@ class UniqueAI:
|
|
|
755
775
|
# (see ``unique_skill_tool.SkillTool._log_skill_loaded``), so it is
|
|
756
776
|
# redundant and noisy to also list it here.
|
|
757
777
|
|
|
758
|
-
tool_names_not_to_log: set[str] = {
|
|
778
|
+
tool_names_not_to_log: set[str] = {
|
|
779
|
+
"DeepResearch",
|
|
780
|
+
"Skill",
|
|
781
|
+
"AskUser",
|
|
782
|
+
"ActivatePython",
|
|
783
|
+
}
|
|
759
784
|
|
|
760
785
|
used_tools: dict[str, int] = {}
|
|
761
786
|
for tool_call in tool_calls:
|
|
@@ -2,6 +2,7 @@ from logging import Logger
|
|
|
2
2
|
from typing import Any, NamedTuple, cast
|
|
3
3
|
|
|
4
4
|
from openai import AsyncOpenAI
|
|
5
|
+
from openai.types.responses import ResponseIncludable
|
|
5
6
|
from typing_extensions import override
|
|
6
7
|
from unique_follow_up_questions.follow_up_postprocessor import (
|
|
7
8
|
FollowUpPostprocessor,
|
|
@@ -115,37 +116,24 @@ def _configure_follow_up_questions(
|
|
|
115
116
|
|
|
116
117
|
|
|
117
118
|
class ResponsesStreamingHandler(ResponsesSupportCompleteWithReferences):
|
|
118
|
-
"""Streaming handler for Responses API runs.
|
|
119
|
+
"""Streaming handler for Responses API runs."""
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
orchestrator loop stays generic (no isinstance checks in UniqueAI).
|
|
122
|
-
|
|
123
|
-
Delegates the actual streaming to an injected
|
|
124
|
-
`ResponsesSupportCompleteWithReferences` (the event-routing
|
|
125
|
-
`ResponsesCompleteWithReferences` orchestrator), so this class only owns
|
|
126
|
-
the `include` injection.
|
|
127
|
-
"""
|
|
121
|
+
_INCLUDE: list[ResponseIncludable] = ["code_interpreter_call.outputs"]
|
|
128
122
|
|
|
129
123
|
def __init__(
|
|
130
124
|
self,
|
|
131
125
|
inner: ResponsesSupportCompleteWithReferences,
|
|
132
|
-
tool_manager: ResponsesApiToolManager,
|
|
133
126
|
) -> None:
|
|
134
127
|
self._inner: ResponsesSupportCompleteWithReferences = inner
|
|
135
|
-
self._tool_manager: ResponsesApiToolManager = tool_manager
|
|
136
128
|
|
|
137
129
|
@override
|
|
138
130
|
def complete_with_references(self, *args: Any, **kwargs: Any) -> Any:
|
|
139
|
-
include = self.
|
|
140
|
-
if include:
|
|
141
|
-
kwargs["include"] = include
|
|
131
|
+
kwargs["include"] = self._INCLUDE
|
|
142
132
|
return self._inner.complete_with_references(*args, **kwargs)
|
|
143
133
|
|
|
144
134
|
@override
|
|
145
135
|
async def complete_with_references_async(self, *args: Any, **kwargs: Any) -> Any:
|
|
146
|
-
include = self.
|
|
147
|
-
if include:
|
|
148
|
-
kwargs["include"] = include
|
|
136
|
+
kwargs["include"] = self._INCLUDE
|
|
149
137
|
return await self._inner.complete_with_references_async(*args, **kwargs)
|
|
150
138
|
|
|
151
139
|
|
|
@@ -602,7 +590,6 @@ async def _build_responses(
|
|
|
602
590
|
|
|
603
591
|
streaming_handler = ResponsesStreamingHandler(
|
|
604
592
|
inner=inner,
|
|
605
|
-
tool_manager=tool_manager,
|
|
606
593
|
)
|
|
607
594
|
|
|
608
595
|
_add_sub_agents_postprocessor(
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"""Tests for ResponsesStreamingHandler include injection (UN-17972).
|
|
2
|
-
|
|
3
|
-
The include forwarding logic lives in ResponsesStreamingHandler in
|
|
4
|
-
unique_ai_builder.py so that UniqueAI stays generic (no isinstance checks).
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from unittest.mock import AsyncMock, MagicMock
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
from unique_toolkit.agentic.tools.tool_manager import ResponsesApiToolManager
|
|
11
|
-
|
|
12
|
-
from unique_orchestrator.unique_ai_builder import (
|
|
13
|
-
ResponsesStreamingHandler,
|
|
14
|
-
_ChatServiceResponsesStreaming,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _make_handler(
|
|
19
|
-
include_params: list[str],
|
|
20
|
-
) -> ResponsesStreamingHandler:
|
|
21
|
-
tool_manager = MagicMock(spec=ResponsesApiToolManager)
|
|
22
|
-
tool_manager.get_required_include_params.return_value = include_params
|
|
23
|
-
inner = MagicMock()
|
|
24
|
-
inner.complete_with_references_async = AsyncMock(return_value=MagicMock())
|
|
25
|
-
inner.complete_with_references = MagicMock(return_value=MagicMock())
|
|
26
|
-
return ResponsesStreamingHandler(
|
|
27
|
-
inner=inner,
|
|
28
|
-
tool_manager=tool_manager,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.mark.ai
|
|
33
|
-
@pytest.mark.asyncio
|
|
34
|
-
async def test_responses_streaming_handler__injects_include__when_tool_manager_returns_params() -> (
|
|
35
|
-
None
|
|
36
|
-
):
|
|
37
|
-
"""
|
|
38
|
-
Purpose: Verify complete_with_references_async injects include when the tool
|
|
39
|
-
manager returns a non-empty list.
|
|
40
|
-
Why this matters: UN-17972 requires code_interpreter_call.outputs on the
|
|
41
|
-
Responses API so execution logs attach to tool calls.
|
|
42
|
-
"""
|
|
43
|
-
handler = _make_handler(include_params=["code_interpreter_call.outputs"])
|
|
44
|
-
|
|
45
|
-
await handler.complete_with_references_async(messages=[], model_name="model")
|
|
46
|
-
|
|
47
|
-
call_kwargs = handler._inner.complete_with_references_async.await_args.kwargs
|
|
48
|
-
assert call_kwargs["include"] == ["code_interpreter_call.outputs"]
|
|
49
|
-
handler._tool_manager.get_required_include_params.assert_called_once()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@pytest.mark.ai
|
|
53
|
-
@pytest.mark.asyncio
|
|
54
|
-
async def test_responses_streaming_handler__omits_include__when_tool_manager_returns_empty() -> (
|
|
55
|
-
None
|
|
56
|
-
):
|
|
57
|
-
"""
|
|
58
|
-
Purpose: Verify include is not added when get_required_include_params returns [].
|
|
59
|
-
Why this matters: No extra include should reach the Responses API when no tool
|
|
60
|
-
requests it — preserving legacy behaviour exactly.
|
|
61
|
-
"""
|
|
62
|
-
handler = _make_handler(include_params=[])
|
|
63
|
-
|
|
64
|
-
await handler.complete_with_references_async(messages=[], model_name="model")
|
|
65
|
-
|
|
66
|
-
call_kwargs = handler._inner.complete_with_references_async.await_args.kwargs
|
|
67
|
-
assert "include" not in call_kwargs
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@pytest.mark.ai
|
|
71
|
-
def test_responses_streaming_handler__injects_include__sync_path() -> None:
|
|
72
|
-
"""
|
|
73
|
-
Purpose: Verify the synchronous complete_with_references also injects include.
|
|
74
|
-
"""
|
|
75
|
-
handler = _make_handler(include_params=["code_interpreter_call.outputs"])
|
|
76
|
-
|
|
77
|
-
handler.complete_with_references(messages=[], model_name="model")
|
|
78
|
-
|
|
79
|
-
call_kwargs = handler._inner.complete_with_references.call_args.kwargs
|
|
80
|
-
assert call_kwargs["include"] == ["code_interpreter_call.outputs"]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@pytest.mark.ai
|
|
84
|
-
def test_responses_streaming_handler__omits_include__sync_path_when_empty() -> None:
|
|
85
|
-
"""Purpose: No include injected via sync path when tool manager returns []."""
|
|
86
|
-
handler = _make_handler(include_params=[])
|
|
87
|
-
|
|
88
|
-
handler.complete_with_references(messages=[], model_name="model")
|
|
89
|
-
|
|
90
|
-
call_kwargs = handler._inner.complete_with_references.call_args.kwargs
|
|
91
|
-
assert "include" not in call_kwargs
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@pytest.mark.ai
|
|
95
|
-
@pytest.mark.asyncio
|
|
96
|
-
async def test_chat_service_responses_streaming__forwards_to_responses_methods() -> (
|
|
97
|
-
None
|
|
98
|
-
):
|
|
99
|
-
"""
|
|
100
|
-
Purpose: The legacy adapter must route the protocol's
|
|
101
|
-
`complete_with_references[_async]` to ChatService's Responses-specific
|
|
102
|
-
methods, not the Chat Completions ones.
|
|
103
|
-
Why this matters: ChatService exposes both; routing to the wrong method
|
|
104
|
-
would silently stream through the Chat Completions API.
|
|
105
|
-
"""
|
|
106
|
-
chat_service = MagicMock()
|
|
107
|
-
chat_service.complete_responses_with_references = MagicMock(
|
|
108
|
-
return_value=MagicMock()
|
|
109
|
-
)
|
|
110
|
-
chat_service.complete_responses_with_references_async = AsyncMock(
|
|
111
|
-
return_value=MagicMock()
|
|
112
|
-
)
|
|
113
|
-
adapter = _ChatServiceResponsesStreaming(chat_service)
|
|
114
|
-
|
|
115
|
-
adapter.complete_with_references(messages=[], model_name="model")
|
|
116
|
-
await adapter.complete_with_references_async(messages=[], model_name="model")
|
|
117
|
-
|
|
118
|
-
chat_service.complete_responses_with_references.assert_called_once()
|
|
119
|
-
chat_service.complete_responses_with_references_async.assert_awaited_once()
|
|
120
|
-
chat_service.complete_with_references.assert_not_called()
|
|
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
|