unique_orchestrator 2026.28.0.dev1__tar.gz → 2026.28.0.dev3__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.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/PKG-INFO +2 -2
  2. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/pyproject.toml +2 -2
  3. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/config.py +40 -0
  4. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_config_services_validators.py +69 -0
  5. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_loop_debug_params.py +1 -0
  6. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/unique_ai.py +37 -3
  7. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/README.md +0 -0
  8. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/__init__.py +0 -0
  9. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/_builders/__init__.py +0 -0
  10. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
  11. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
  12. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/_builders/open_file_setup.py +0 -0
  13. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/_builders/skill_setup.py +0 -0
  14. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
  15. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
  16. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
  17. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/settings.py +0 -0
  18. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_agent_config_rjsf_ui_schema.py +0 -0
  19. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
  20. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
  21. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
  22. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
  23. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
  24. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
  25. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
  26. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
  27. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
  28. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
  29. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
  30. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/tests/test_utils_resolve_other_options.py +0 -0
  31. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/unique_ai_builder.py +0 -0
  32. {unique_orchestrator-2026.28.0.dev1 → unique_orchestrator-2026.28.0.dev3}/unique_orchestrator/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-orchestrator
3
- Version: 2026.28.0.dev1
3
+ Version: 2026.28.0.dev3
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.28.0.dev0,<2026.28.0rc0
12
+ Requires-Dist: unique-toolkit>=2026.28.0.dev2,<2026.28.0rc0
13
13
  Requires-Dist: unique-stock-ticker>=2026.28.0.dev0,<2026.28.0rc0
14
14
  Requires-Dist: unique-follow-up-questions>=2026.28.0.dev0,<2026.28.0rc0
15
15
  Requires-Dist: unique-internal-search>=2026.28.0.dev0,<2026.28.0rc0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_orchestrator"
3
- version = "2026.28.0.dev1"
3
+ version = "2026.28.0.dev3"
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.28.0.dev0,<2026.28.0rc0",
16
+ "unique-toolkit>=2026.28.0.dev2,<2026.28.0rc0",
17
17
  "unique-stock-ticker>=2026.28.0.dev0,<2026.28.0rc0",
18
18
  "unique-follow-up-questions>=2026.28.0.dev0,<2026.28.0rc0",
19
19
  "unique-internal-search>=2026.28.0.dev0,<2026.28.0rc0",
@@ -37,6 +37,12 @@ from unique_toolkit.agentic.tools.a2a import (
37
37
  REFERENCING_INSTRUCTIONS_FOR_USER_PROMPT,
38
38
  )
39
39
  from unique_toolkit.agentic.tools.a2a.evaluation import SubAgentEvaluationServiceConfig
40
+ from unique_toolkit.agentic.tools.experimental.ask_user_tool import (
41
+ AskUserTool,
42
+ )
43
+ from unique_toolkit.agentic.tools.experimental.ask_user_tool import (
44
+ AskUserToolConfig as BaseAskUserToolConfig,
45
+ )
40
46
  from unique_toolkit.agentic.tools.experimental.open_file_tool.config import (
41
47
  OpenFileToolConfig,
42
48
  )
@@ -391,6 +397,13 @@ class UploadedSearchToolConfig(BaseToolConfig):
391
397
  return self
392
398
 
393
399
 
400
+ class AskUserToolConfig(BaseAskUserToolConfig):
401
+ enabled: bool = Field(
402
+ default=False,
403
+ description="Enable the AskUser elicitation tool.",
404
+ )
405
+
406
+
394
407
  class ExperimentalConfig(BaseToolConfig):
395
408
  """Experimental features this part of the configuration might evolve in the future continuously"""
396
409
 
@@ -431,6 +444,12 @@ class ExperimentalConfig(BaseToolConfig):
431
444
 
432
445
  uploaded_search_tool_config: UploadedSearchToolConfig = UploadedSearchToolConfig()
433
446
 
447
+ ask_user_tool_config: AskUserToolConfig = Field(
448
+ title="Ask User Tool (Elicitation)",
449
+ description="Configuration for the AskUser tool",
450
+ default_factory=AskUserToolConfig,
451
+ )
452
+
434
453
  use_responses_api: bool = Field(
435
454
  default=False,
436
455
  description="If set, the main agent will use the Responses API from OpenAI",
@@ -593,6 +612,27 @@ class UniqueAIConfig(BaseToolConfig):
593
612
 
594
613
  return self
595
614
 
615
+ @model_validator(mode="after")
616
+ def inject_ask_user_tool(self) -> "UniqueAIConfig":
617
+ tool_names = [t.name for t in self.space.tools]
618
+ has_tool = AskUserTool.name in tool_names
619
+ config = self.agent.experimental.ask_user_tool_config
620
+
621
+ if config.enabled and not has_tool:
622
+ self.space.tools.append(
623
+ ToolBuildConfig(
624
+ name=AskUserTool.name,
625
+ display_name=AskUserTool.DISPLAY_NAME,
626
+ configuration=config,
627
+ )
628
+ )
629
+ elif not config.enabled and has_tool:
630
+ self.space.tools = [
631
+ t for t in self.space.tools if t.name != AskUserTool.name
632
+ ]
633
+
634
+ return self
635
+
596
636
  @property
597
637
  def effective_max_loop_iterations(self) -> int:
598
638
  """Effective max loop iterations, accounting for model-specific overrides."""
@@ -1,6 +1,9 @@
1
1
  import pytest
2
2
  from unique_follow_up_questions.config import FollowUpQuestionsConfig
3
3
  from unique_stock_ticker.config import StockTickerConfig
4
+ from unique_toolkit.agentic.tools.experimental.ask_user_tool import (
5
+ AskUserTool,
6
+ )
4
7
  from unique_toolkit.agentic.tools.experimental.open_file_tool.config import (
5
8
  OpenFileToolConfig,
6
9
  )
@@ -23,6 +26,7 @@ from unique_toolkit.language_model.schemas import LanguageModelTokenLimits
23
26
  from unique_user_memory.config import UserMemoryConfig
24
27
 
25
28
  from unique_orchestrator.config import (
29
+ AskUserToolConfig,
26
30
  EvaluationConfig,
27
31
  UniqueAIConfig,
28
32
  UniqueAIServices,
@@ -496,3 +500,68 @@ class TestUniqueAIConfigUserMemory:
496
500
  config = UniqueAIConfig(space={"allowUserMemory": True})
497
501
 
498
502
  assert config.space.allow_user_memory is True
503
+
504
+
505
+ class TestUniqueAIConfigInjectAskUserToolValidator:
506
+ """Tests for UniqueAIConfig.inject_ask_user_tool (AskUser)."""
507
+
508
+ def _entries(self, config: UniqueAIConfig) -> list[ToolBuildConfig]:
509
+ return [t for t in config.space.tools if t.name == AskUserTool.name]
510
+
511
+ def test_appends_when_enabled(self) -> None:
512
+ config = UniqueAIConfig(
513
+ space=UniqueAISpaceConfig(
514
+ language_model=_make_model(supports_responses_api=True),
515
+ tools=[],
516
+ ),
517
+ agent={
518
+ "experimental": {
519
+ "ask_user_tool_config": AskUserToolConfig(enabled=True)
520
+ }
521
+ },
522
+ )
523
+
524
+ entries = self._entries(config)
525
+ assert len(entries) == 1
526
+ assert entries[0].name == AskUserTool.name
527
+ assert isinstance(entries[0].configuration, AskUserToolConfig)
528
+ assert entries[0].configuration.enabled is True
529
+
530
+ def test_removes_when_disabled(self) -> None:
531
+ config = UniqueAIConfig(
532
+ space=UniqueAISpaceConfig(
533
+ language_model=_make_model(supports_responses_api=True),
534
+ tools=[
535
+ ToolBuildConfig(
536
+ name=AskUserTool.name,
537
+ configuration=AskUserToolConfig(enabled=False),
538
+ )
539
+ ],
540
+ ),
541
+ agent={
542
+ "experimental": {
543
+ "ask_user_tool_config": AskUserToolConfig(enabled=False)
544
+ }
545
+ },
546
+ )
547
+
548
+ assert self._entries(config) == []
549
+
550
+ def test_idempotent_when_already_present(self) -> None:
551
+ existing = ToolBuildConfig(
552
+ name=AskUserTool.name,
553
+ configuration=AskUserToolConfig(enabled=True),
554
+ )
555
+ config = UniqueAIConfig(
556
+ space=UniqueAISpaceConfig(
557
+ language_model=_make_model(supports_responses_api=True),
558
+ tools=[existing],
559
+ ),
560
+ agent={
561
+ "experimental": {
562
+ "ask_user_tool_config": AskUserToolConfig(enabled=True)
563
+ }
564
+ },
565
+ )
566
+
567
+ assert len(self._entries(config)) == 1
@@ -352,6 +352,7 @@ class TestRunLoopDebugParams:
352
352
  call.args[0] for call in ua._debug_info_manager.add.call_args_list
353
353
  ]
354
354
  assert "loop_params" in keys_written
355
+ assert "skills" in keys_written
355
356
 
356
357
  @pytest.mark.ai
357
358
  @pytest.mark.asyncio
@@ -6,6 +6,7 @@ from typing import Any, cast, overload
6
6
 
7
7
  import jinja2
8
8
  from typing_extensions import deprecated
9
+ from unique_skill_tool.service import SkillTool
9
10
  from unique_toolkit.agentic.debug_info_manager.debug_info_manager import (
10
11
  DebugInfoManager,
11
12
  )
@@ -35,7 +36,7 @@ from unique_toolkit.agentic.tools.tool_manager import (
35
36
  SafeTaskExecutor,
36
37
  ToolManager,
37
38
  )
38
- from unique_toolkit.app.schemas import ChatEvent, McpServer
39
+ from unique_toolkit.app.schemas import ChatEvent, McpServer, SkillReference
39
40
  from unique_toolkit.chat.cancellation import CancellationEvent
40
41
  from unique_toolkit.chat.service import ChatService
41
42
  from unique_toolkit.content import Content
@@ -143,7 +144,9 @@ class UniqueAI:
143
144
  self._content_service = content_service
144
145
  self._uploaded_documents = uploaded_documents or []
145
146
  self._user_memory_text = user_memory_text
146
- self._skill_choices = getattr(event.payload, "skill_choices", [])
147
+ self._skill_choices: list[SkillReference] = getattr(
148
+ event.payload, "skill_choices", []
149
+ )
147
150
 
148
151
  self._debug_info_manager = debug_info_manager
149
152
  self._reference_manager = reference_manager
@@ -313,6 +316,10 @@ class UniqueAI:
313
316
  },
314
317
  )
315
318
  self._debug_info_manager.add("loop_params", self._loop_debug_params)
319
+ self._debug_info_manager.add(
320
+ "skills",
321
+ self._get_activated_skills_debug_info(),
322
+ )
316
323
 
317
324
  tool_names = [
318
325
  tool["name"] for tool in self._debug_info_manager.get()["tools"]
@@ -349,6 +356,33 @@ class UniqueAI:
349
356
  }
350
357
  )
351
358
 
359
+ def _get_activated_skills_debug_info(self) -> list[dict[str, str | bool]]:
360
+ skill_tool = self._tool_manager.get_tool_by_name(SkillTool.name)
361
+ if not isinstance(skill_tool, SkillTool):
362
+ return []
363
+
364
+ forced_content_ids = {
365
+ choice.content_id for choice in self._skill_choices if choice.content_id
366
+ }
367
+ forced_names = {choice.name for choice in self._skill_choices if choice.name}
368
+
369
+ skills_debug_info: dict[str, dict[str, str | bool]] = {}
370
+ for skill in skill_tool.activated_skills:
371
+ skills_debug_info.setdefault(
372
+ skill.name,
373
+ {
374
+ "name": skill.name,
375
+ "content_id": skill.content_id,
376
+ "is_forced": (
377
+ skill.content_id in forced_content_ids
378
+ if skill.content_id
379
+ else skill.name in forced_names
380
+ ),
381
+ },
382
+ )
383
+
384
+ return list(skills_debug_info.values())
385
+
352
386
  # @track()
353
387
  async def _plan_or_execute(self) -> LanguageModelStreamResponse:
354
388
  self._logger.info("Planning or executing the loop.")
@@ -667,7 +701,7 @@ class UniqueAI:
667
701
  # (see ``unique_skill_tool.SkillTool._log_skill_loaded``), so it is
668
702
  # redundant and noisy to also list it here.
669
703
 
670
- tool_names_not_to_log: set[str] = {"DeepResearch", "Skill"}
704
+ tool_names_not_to_log: set[str] = {"DeepResearch", "Skill", "AskUser"}
671
705
 
672
706
  used_tools: dict[str, int] = {}
673
707
  for tool_call in tool_calls: