unique_orchestrator 2026.24.0.dev0__tar.gz → 2026.24.0.dev1__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 (31) hide show
  1. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/PKG-INFO +6 -6
  2. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/pyproject.toml +6 -6
  3. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/config.py +47 -0
  4. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_config_services_validators.py +39 -0
  5. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/unique_ai_builder.py +73 -2
  6. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/README.md +0 -0
  7. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/__init__.py +0 -0
  8. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/_builders/__init__.py +0 -0
  9. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
  10. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
  11. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/_builders/open_file_setup.py +0 -0
  12. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/_builders/skill_setup.py +0 -0
  13. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
  14. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
  15. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
  16. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/settings.py +0 -0
  17. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
  18. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
  19. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
  20. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
  21. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
  22. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
  23. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
  24. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py +0 -0
  25. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
  26. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
  27. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
  28. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
  29. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/tests/test_utils_resolve_other_options.py +0 -0
  30. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/unique_ai.py +0 -0
  31. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev1}/unique_orchestrator/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-orchestrator
3
- Version: 2026.24.0.dev0
3
+ Version: 2026.24.0.dev1
4
4
  Summary:
5
5
  Author: Andreas Hauri
6
6
  Author-email: Andreas Hauri <andreas.hauri@unique.ai>
@@ -9,13 +9,13 @@ 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.24.0.dev0,<2026.24.0rc0
13
- Requires-Dist: unique-stock-ticker>=2026.24.0.dev0,<2026.24.0rc0
14
- Requires-Dist: unique-follow-up-questions>=2026.24.0.dev0,<2026.24.0rc0
15
- Requires-Dist: unique-internal-search>=2026.24.0.dev0,<2026.24.0rc0
12
+ Requires-Dist: unique-toolkit>=2026.24.0.dev6,<2026.24.0rc0
13
+ Requires-Dist: unique-stock-ticker>=2026.24.0.dev1,<2026.24.0rc0
14
+ Requires-Dist: unique-follow-up-questions>=2026.24.0.dev1,<2026.24.0rc0
15
+ Requires-Dist: unique-internal-search>=2026.24.0.dev1,<2026.24.0rc0
16
16
  Requires-Dist: unique-deep-research>=2026.24.0.dev0,<2026.24.0rc0
17
17
  Requires-Dist: unique-web-search>=2026.24.0.dev0,<2026.24.0rc0
18
- Requires-Dist: unique-swot>=2026.24.0.dev0,<2026.24.0rc0
18
+ Requires-Dist: unique-swot>=2026.24.0.dev1,<2026.24.0rc0
19
19
  Requires-Dist: unique-skill-tool>=2026.24.0.dev0,<2026.24.0rc0
20
20
  Requires-Dist: openai>=1.109.1,<3
21
21
  Requires-Python: >=3.12, <4
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_orchestrator"
3
- version = "2026.24.0.dev0"
3
+ version = "2026.24.0.dev1"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "Proprietary" }
@@ -13,13 +13,13 @@ 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.24.0.dev0,<2026.24.0rc0",
17
- "unique-stock-ticker>=2026.24.0.dev0,<2026.24.0rc0",
18
- "unique-follow-up-questions>=2026.24.0.dev0,<2026.24.0rc0",
19
- "unique-internal-search>=2026.24.0.dev0,<2026.24.0rc0",
16
+ "unique-toolkit>=2026.24.0.dev6,<2026.24.0rc0",
17
+ "unique-stock-ticker>=2026.24.0.dev1,<2026.24.0rc0",
18
+ "unique-follow-up-questions>=2026.24.0.dev1,<2026.24.0rc0",
19
+ "unique-internal-search>=2026.24.0.dev1,<2026.24.0rc0",
20
20
  "unique-deep-research>=2026.24.0.dev0,<2026.24.0rc0",
21
21
  "unique-web-search>=2026.24.0.dev0,<2026.24.0rc0",
22
- "unique-swot>=2026.24.0.dev0,<2026.24.0rc0",
22
+ "unique-swot>=2026.24.0.dev1,<2026.24.0rc0",
23
23
  "unique-skill-tool>=2026.24.0.dev0,<2026.24.0rc0",
24
24
  "openai>=1.109.1,<3",
25
25
  ]
@@ -76,6 +76,20 @@ class SpaceType(StrEnum):
76
76
  T = TypeVar("T", bound=SpaceType)
77
77
 
78
78
 
79
+ class SwitchableLanguageModelConfig(BaseToolConfig):
80
+ """A language model that chat users may select for a single message."""
81
+
82
+ display_name: str = Field(
83
+ description="Human-readable label shown to chat users in the model picker.",
84
+ )
85
+ language_model: LMI = Field(
86
+ description=(
87
+ "Language-model configuration accepted from the platform payload as "
88
+ "`languageModel`."
89
+ ),
90
+ )
91
+
92
+
79
93
  class SpaceConfigBase(BaseToolConfig, Generic[T]):
80
94
  """Base class for space configuration."""
81
95
 
@@ -88,6 +102,18 @@ class SpaceConfigBase(BaseToolConfig, Generic[T]):
88
102
 
89
103
  language_model: LMI = get_LMI_default_field(DEFAULT_GPT_4o)
90
104
 
105
+ allow_model_switching: bool = Field(
106
+ default=False,
107
+ description=(
108
+ "Whether chat users may override the language model for a single message"
109
+ ),
110
+ )
111
+
112
+ switchable_language_models: list[SwitchableLanguageModelConfig] = Field(
113
+ default_factory=list,
114
+ description=("Language models selectable by chat users for a single message."),
115
+ )
116
+
91
117
  custom_instructions: str = Field(
92
118
  default="",
93
119
  description="A custom instruction provided by the system admin.",
@@ -421,6 +447,27 @@ class UniqueAIConfig(BaseToolConfig):
421
447
  self.agent.experimental.sub_agents_config.referencing_config = None
422
448
  return self
423
449
 
450
+ @model_validator(mode="after")
451
+ def remove_tools_with_unsupported_model_capabilities(self) -> "UniqueAIConfig":
452
+ if ModelCapabilities.RESPONSES_API in self.space.language_model.capabilities:
453
+ return self
454
+
455
+ self.space.tools = [
456
+ tool
457
+ for tool in self.space.tools
458
+ if tool.name != OpenAIBuiltInToolName.CODE_INTERPRETER
459
+ ]
460
+ return self
461
+
462
+ @model_validator(mode="after")
463
+ def disable_responses_api_when_model_does_not_support_it(self) -> "UniqueAIConfig":
464
+ if ModelCapabilities.RESPONSES_API in self.space.language_model.capabilities:
465
+ return self
466
+
467
+ self.agent.experimental.responses_api_config.use_responses_api = False
468
+ self.agent.experimental.use_responses_api = False
469
+ return self
470
+
424
471
  @model_validator(mode="after")
425
472
  def enable_responses_api_for_code_interpreter_tool(self) -> "UniqueAIConfig":
426
473
  """Auto-enable the Responses API when Code Interpreter is added directly as a tool.
@@ -29,6 +29,45 @@ from unique_orchestrator.config import (
29
29
  )
30
30
 
31
31
 
32
+ class TestUniqueAISpaceConfigModelSwitching:
33
+ def test_accepts_camel_case_model_switching_fields(self):
34
+ config = UniqueAISpaceConfig.model_validate(
35
+ {
36
+ "allowModelSwitching": True,
37
+ "switchableLanguageModels": [
38
+ {
39
+ "displayName": "GPT-4o",
40
+ "languageModel": LanguageModelName.AZURE_GPT_4o_2024_1120,
41
+ }
42
+ ],
43
+ }
44
+ )
45
+
46
+ assert config.allow_model_switching is True
47
+ assert len(config.switchable_language_models) == 1
48
+ assert config.switchable_language_models[0].display_name == "GPT-4o"
49
+ assert (
50
+ config.switchable_language_models[0].language_model.name
51
+ == LanguageModelName.AZURE_GPT_4o_2024_1120
52
+ )
53
+
54
+ @pytest.mark.ai
55
+ def test_defaults_absent_switchable_language_models_from_node_backend(self):
56
+ """Purpose: Verify omitted node-backend model-switching payloads parse.
57
+ Why this matters: Existing spaces can omit the new JSON field before it is configured.
58
+ Setup summary: Validate a payload without switchableLanguageModels and assert it defaults to [].
59
+ """
60
+ config = UniqueAIConfig.model_validate(
61
+ {
62
+ "space": {
63
+ "allowModelSwitching": False,
64
+ },
65
+ }
66
+ )
67
+
68
+ assert config.space.switchable_language_models == []
69
+
70
+
32
71
  class TestUniqueAIServicesStockTickerConfigValidator:
33
72
  """Test suite for UniqueAIServices.check_if_stock_ticker_config_is_none validator"""
34
73
 
@@ -68,7 +68,7 @@ from unique_toolkit.app.schemas import ChatEvent, McpServer
68
68
  from unique_toolkit.chat.service import ChatService
69
69
  from unique_toolkit.content import Content
70
70
  from unique_toolkit.content.service import ContentService
71
- from unique_toolkit.language_model.infos import ModelCapabilities
71
+ from unique_toolkit.language_model.infos import LanguageModelInfo, ModelCapabilities
72
72
  from unique_toolkit.protocols.support import ResponsesSupportCompleteWithReferences
73
73
 
74
74
  from unique_orchestrator._builders import (
@@ -83,7 +83,11 @@ from unique_orchestrator._builders.skill_setup import (
83
83
  configure_skill_tool,
84
84
  normalize_available_skills_for_tool,
85
85
  )
86
- from unique_orchestrator.config import UniqueAIConfig, UploadedSearchToolConfig
86
+ from unique_orchestrator.config import (
87
+ SwitchableLanguageModelConfig,
88
+ UniqueAIConfig,
89
+ UploadedSearchToolConfig,
90
+ )
87
91
  from unique_orchestrator.unique_ai import UniqueAI
88
92
  from unique_orchestrator.utils import filter_uploaded_documents_by_selection
89
93
 
@@ -126,6 +130,11 @@ async def build_unique_ai(
126
130
  config: UniqueAIConfig,
127
131
  debug_info_manager: DebugInfoManager,
128
132
  ) -> UniqueAI:
133
+ config = _apply_model_choice_override(event=event, logger=logger, config=config)
134
+ _record_language_model_debug_info(
135
+ debug_info_manager=debug_info_manager,
136
+ config=config,
137
+ )
129
138
  common_components = await _build_common(event, logger, config)
130
139
 
131
140
  if (
@@ -170,6 +179,68 @@ class _CommonComponents(NamedTuple):
170
179
  mcp_servers: list[McpServer]
171
180
 
172
181
 
182
+ def _apply_model_choice_override(
183
+ *,
184
+ event: ChatEvent,
185
+ logger: Logger,
186
+ config: UniqueAIConfig,
187
+ ) -> UniqueAIConfig:
188
+ if (
189
+ not config.space.allow_model_switching
190
+ or not event.payload.has_model_choice_override
191
+ ):
192
+ return config
193
+
194
+ selected_model = event.payload.model_choice
195
+ if (
196
+ config.space.switchable_language_models
197
+ and not _is_switchable_language_model_choice(
198
+ selected_model=selected_model,
199
+ switchable_language_models=config.space.switchable_language_models,
200
+ )
201
+ ):
202
+ raise ValueError(
203
+ f"User model choice {selected_model.display_name!r} is not "
204
+ "allowed for this space."
205
+ )
206
+
207
+ config_data = config.model_dump()
208
+ config_data["space"]["language_model"] = selected_model
209
+ validated_config = UniqueAIConfig.model_validate(config_data)
210
+
211
+ logger.info(
212
+ "Using user model choice %s.",
213
+ selected_model.display_name,
214
+ )
215
+
216
+ return validated_config
217
+
218
+
219
+ def _record_language_model_debug_info(
220
+ *,
221
+ debug_info_manager: DebugInfoManager,
222
+ config: UniqueAIConfig,
223
+ ) -> None:
224
+ debug_info_manager.add(
225
+ "language_model",
226
+ config.space.language_model.model_dump(
227
+ mode="json",
228
+ include={"name", "provider", "family"},
229
+ ),
230
+ )
231
+
232
+
233
+ def _is_switchable_language_model_choice(
234
+ *,
235
+ selected_model: LanguageModelInfo,
236
+ switchable_language_models: list[SwitchableLanguageModelConfig],
237
+ ) -> bool:
238
+ return any(
239
+ selected_model == switchable_model.language_model
240
+ for switchable_model in switchable_language_models
241
+ )
242
+
243
+
173
244
  async def _build_common(
174
245
  event: ChatEvent,
175
246
  logger: Logger,