unique_orchestrator 2026.24.0.dev0__tar.gz → 2026.24.0.dev2__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 (32) hide show
  1. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/PKG-INFO +7 -7
  2. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/pyproject.toml +7 -7
  3. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/config.py +56 -2
  4. unique_orchestrator-2026.24.0.dev2/unique_orchestrator/tests/test_agent_config_rjsf_ui_schema.py +53 -0
  5. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_config_services_validators.py +39 -0
  6. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/unique_ai_builder.py +73 -2
  7. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/README.md +0 -0
  8. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/__init__.py +0 -0
  9. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/_builders/__init__.py +0 -0
  10. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
  11. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
  12. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/_builders/open_file_setup.py +0 -0
  13. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/_builders/skill_setup.py +0 -0
  14. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
  15. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
  16. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
  17. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/settings.py +0 -0
  18. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
  19. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
  20. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
  21. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
  22. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
  23. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
  24. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
  25. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py +0 -0
  26. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
  27. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
  28. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
  29. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
  30. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/tests/test_utils_resolve_other_options.py +0 -0
  31. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/unique_orchestrator/unique_ai.py +0 -0
  32. {unique_orchestrator-2026.24.0.dev0 → unique_orchestrator-2026.24.0.dev2}/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.dev2
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.dev11,<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.dev2,<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
- Requires-Dist: unique-web-search>=2026.24.0.dev0,<2026.24.0rc0
18
- Requires-Dist: unique-swot>=2026.24.0.dev0,<2026.24.0rc0
17
+ Requires-Dist: unique-web-search>=2026.24.0.dev3,<2026.24.0rc0
18
+ Requires-Dist: unique-swot>=2026.24.0.dev2,<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.dev2"
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.dev11,<2026.24.0rc0",
17
+ "unique-stock-ticker>=2026.24.0.dev1,<2026.24.0rc0",
18
+ "unique-follow-up-questions>=2026.24.0.dev2,<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
- "unique-web-search>=2026.24.0.dev0,<2026.24.0rc0",
22
- "unique-swot>=2026.24.0.dev0,<2026.24.0rc0",
21
+ "unique-web-search>=2026.24.0.dev3,<2026.24.0rc0",
22
+ "unique-swot>=2026.24.0.dev2,<2026.24.0rc0",
23
23
  "unique-skill-tool>=2026.24.0.dev0,<2026.24.0rc0",
24
24
  "openai>=1.109.1,<3",
25
25
  ]
@@ -14,6 +14,7 @@ from unique_internal_search.uploaded_search.config import (
14
14
  )
15
15
  from unique_stock_ticker.config import StockTickerConfig
16
16
  from unique_swot import SwotAnalysisTool, SwotAnalysisToolConfig
17
+ from unique_toolkit._common.pydantic.rjsf_tags import RJSFMetaTag
17
18
  from unique_toolkit._common.validators import (
18
19
  LMI,
19
20
  ClipInt,
@@ -76,6 +77,20 @@ class SpaceType(StrEnum):
76
77
  T = TypeVar("T", bound=SpaceType)
77
78
 
78
79
 
80
+ class SwitchableLanguageModelConfig(BaseToolConfig):
81
+ """A language model that chat users may select for a single message."""
82
+
83
+ display_name: str = Field(
84
+ description="Human-readable label shown to chat users in the model picker.",
85
+ )
86
+ language_model: LMI = Field(
87
+ description=(
88
+ "Language-model configuration accepted from the platform payload as "
89
+ "`languageModel`."
90
+ ),
91
+ )
92
+
93
+
79
94
  class SpaceConfigBase(BaseToolConfig, Generic[T]):
80
95
  """Base class for space configuration."""
81
96
 
@@ -88,6 +103,18 @@ class SpaceConfigBase(BaseToolConfig, Generic[T]):
88
103
 
89
104
  language_model: LMI = get_LMI_default_field(DEFAULT_GPT_4o)
90
105
 
106
+ allow_model_switching: bool = Field(
107
+ default=False,
108
+ description=(
109
+ "Whether chat users may override the language model for a single message"
110
+ ),
111
+ )
112
+
113
+ switchable_language_models: list[SwitchableLanguageModelConfig] = Field(
114
+ default_factory=list,
115
+ description=("Language models selectable by chat users for a single message."),
116
+ )
117
+
91
118
  custom_instructions: str = Field(
92
119
  default="",
93
120
  description="A custom instruction provided by the system admin.",
@@ -210,14 +237,20 @@ class EvaluationConfig(BaseToolConfig):
210
237
 
211
238
 
212
239
  class UniqueAIPromptConfig(BaseToolConfig):
213
- system_prompt_template: str = Field(
240
+ system_prompt_template: Annotated[
241
+ str,
242
+ RJSFMetaTag.StringWidget.textarea(rows=25),
243
+ ] = Field(
214
244
  default_factory=lambda: (
215
245
  Path(__file__).parent / "prompts" / "system_prompt.jinja2"
216
246
  ).read_text(),
217
247
  description="The system prompt template as a Jinja2 template string.",
218
248
  )
219
249
 
220
- user_message_prompt_template: str = Field(
250
+ user_message_prompt_template: Annotated[
251
+ str,
252
+ RJSFMetaTag.StringWidget.textarea(rows=4),
253
+ ] = Field(
221
254
  default_factory=lambda: (
222
255
  Path(__file__).parent / "prompts" / "user_message_prompt.jinja2"
223
256
  ).read_text(),
@@ -421,6 +454,27 @@ class UniqueAIConfig(BaseToolConfig):
421
454
  self.agent.experimental.sub_agents_config.referencing_config = None
422
455
  return self
423
456
 
457
+ @model_validator(mode="after")
458
+ def remove_tools_with_unsupported_model_capabilities(self) -> "UniqueAIConfig":
459
+ if ModelCapabilities.RESPONSES_API in self.space.language_model.capabilities:
460
+ return self
461
+
462
+ self.space.tools = [
463
+ tool
464
+ for tool in self.space.tools
465
+ if tool.name != OpenAIBuiltInToolName.CODE_INTERPRETER
466
+ ]
467
+ return self
468
+
469
+ @model_validator(mode="after")
470
+ def disable_responses_api_when_model_does_not_support_it(self) -> "UniqueAIConfig":
471
+ if ModelCapabilities.RESPONSES_API in self.space.language_model.capabilities:
472
+ return self
473
+
474
+ self.agent.experimental.responses_api_config.use_responses_api = False
475
+ self.agent.experimental.use_responses_api = False
476
+ return self
477
+
424
478
  @model_validator(mode="after")
425
479
  def enable_responses_api_for_code_interpreter_tool(self) -> "UniqueAIConfig":
426
480
  """Auto-enable the Responses API when Code Interpreter is added directly as a tool.
@@ -0,0 +1,53 @@
1
+ from humps import camelize
2
+ from unique_toolkit._common.pydantic.rjsf_tags import ui_schema_for_model
3
+
4
+ from unique_orchestrator.config import UniqueAIAgentConfig
5
+
6
+
7
+ def _nested_get(data: dict, path: str) -> object:
8
+ current: object = data
9
+ for part in path.split("."):
10
+ assert isinstance(current, dict)
11
+ current = current[part]
12
+ return current
13
+
14
+
15
+ def test_unique_ai_agent_config__prompt_fields__textarea_widgets() -> None:
16
+ ui_schema = ui_schema_for_model(UniqueAIAgentConfig, key_transform=camelize)
17
+ assert _nested_get(ui_schema, "promptConfig.systemPromptTemplate") == {
18
+ "ui:widget": "textarea",
19
+ "ui:disabled": False,
20
+ "ui:readonly": False,
21
+ "ui:options": {"rows": 25},
22
+ "ui:emptyValue": "",
23
+ }
24
+ assert _nested_get(ui_schema, "promptConfig.userMessagePromptTemplate") == {
25
+ "ui:widget": "textarea",
26
+ "ui:disabled": False,
27
+ "ui:readonly": False,
28
+ "ui:options": {"rows": 4},
29
+ "ui:emptyValue": "",
30
+ }
31
+ assert _nested_get(ui_schema, "services.followUpQuestionsConfig.userPrompt") == {
32
+ "ui:widget": "textarea",
33
+ "ui:disabled": False,
34
+ "ui:readonly": False,
35
+ "ui:options": {"rows": 10},
36
+ "ui:emptyValue": "",
37
+ }
38
+ assert _nested_get(ui_schema, "services.followUpQuestionsConfig.systemPrompt") == {
39
+ "ui:widget": "textarea",
40
+ "ui:disabled": False,
41
+ "ui:readonly": False,
42
+ "ui:options": {"rows": 15},
43
+ "ui:emptyValue": "",
44
+ }
45
+ assert _nested_get(
46
+ ui_schema, "services.followUpQuestionsConfig.suggestionsFormat"
47
+ ) == {
48
+ "ui:widget": "textarea",
49
+ "ui:disabled": False,
50
+ "ui:readonly": False,
51
+ "ui:options": {"rows": 8},
52
+ "ui:emptyValue": "",
53
+ }
@@ -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,