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.
Files changed (34) hide show
  1. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/PKG-INFO +2 -2
  2. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/pyproject.toml +2 -2
  3. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/history_manager.py +5 -0
  4. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_async_modify.py +24 -0
  5. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py +28 -0
  6. unique_orchestrator-2026.30.0.dev7/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +76 -0
  7. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/unique_ai.py +28 -3
  8. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/unique_ai_builder.py +5 -18
  9. unique_orchestrator-2026.30.0.dev5/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -120
  10. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/README.md +0 -0
  11. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/__init__.py +0 -0
  12. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/__init__.py +0 -0
  13. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
  14. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
  15. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/open_file_setup.py +0 -0
  16. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/_builders/skill_setup.py +0 -0
  17. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/config.py +0 -0
  18. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
  19. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
  20. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
  21. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/settings.py +0 -0
  22. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_agent_config_rjsf_ui_schema.py +0 -0
  23. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
  24. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
  25. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
  26. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
  27. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
  28. {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
  29. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
  30. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
  31. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
  32. {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
  33. {unique_orchestrator-2026.30.0.dev5 → unique_orchestrator-2026.30.0.dev7}/unique_orchestrator/tests/test_utils_resolve_other_options.py +0 -0
  34. {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.dev5
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.dev15,<2026.30.0rc0
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.dev5"
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.dev15,<2026.30.0rc0",
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
- _, evaluation_results = await asyncio.gather(
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] = {"DeepResearch", "Skill", "AskUser"}
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
- Injects `include` params from the tool manager on every call so the
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._tool_manager.get_required_include_params()
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._tool_manager.get_required_include_params()
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()