unique_orchestrator 2026.22.0.dev2__tar.gz → 2026.22.0.dev4__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 (29) hide show
  1. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/PKG-INFO +9 -9
  2. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/pyproject.toml +9 -9
  3. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/skill_setup.py +71 -46
  4. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/unique_ai.py +0 -1
  5. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/unique_ai_builder.py +10 -1
  6. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/README.md +0 -0
  7. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/__init__.py +0 -0
  8. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/__init__.py +0 -0
  9. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
  10. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
  11. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/open_file_setup.py +0 -0
  12. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/config.py +0 -0
  13. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
  14. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
  15. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
  16. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/settings.py +0 -0
  17. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
  18. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
  19. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
  20. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
  21. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
  22. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
  23. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
  24. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
  25. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
  26. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
  27. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
  28. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
  29. {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-orchestrator
3
- Version: 2026.22.0.dev2
3
+ Version: 2026.22.0.dev4
4
4
  Summary:
5
5
  Author: Andreas Hauri
6
6
  Author-email: Andreas Hauri <andreas.hauri@unique.ai>
@@ -9,14 +9,14 @@ 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.22.0.dev2,<2026.22.0rc0
13
- Requires-Dist: unique-stock-ticker>=2026.22.0.dev1,<2026.22.0rc0
14
- Requires-Dist: unique-follow-up-questions>=2026.22.0.dev1,<2026.22.0rc0
15
- Requires-Dist: unique-internal-search>=2026.22.0.dev1,<2026.22.0rc0
16
- Requires-Dist: unique-deep-research>=2026.22.0.dev1,<2026.22.0rc0
17
- Requires-Dist: unique-web-search>=2026.22.0.dev2,<2026.22.0rc0
18
- Requires-Dist: unique-swot>=2026.22.0.dev1,<2026.22.0rc0
19
- Requires-Dist: unique-skill-tool>=2026.22.0.dev1,<2026.22.0rc0
12
+ Requires-Dist: unique-toolkit>=2026.22.0.dev6,<2026.22.0rc0
13
+ Requires-Dist: unique-stock-ticker>=2026.22.0.dev2,<2026.22.0rc0
14
+ Requires-Dist: unique-follow-up-questions>=2026.22.0.dev2,<2026.22.0rc0
15
+ Requires-Dist: unique-internal-search>=2026.22.0.dev2,<2026.22.0rc0
16
+ Requires-Dist: unique-deep-research>=2026.22.0.dev2,<2026.22.0rc0
17
+ Requires-Dist: unique-web-search>=2026.22.0.dev4,<2026.22.0rc0
18
+ Requires-Dist: unique-swot>=2026.22.0.dev2,<2026.22.0rc0
19
+ Requires-Dist: unique-skill-tool>=2026.22.0.dev3,<2026.22.0rc0
20
20
  Requires-Dist: openai>=1.109.1,<3
21
21
  Requires-Dist: python-frontmatter>=1.1.0,<2
22
22
  Requires-Python: >=3.12, <4
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_orchestrator"
3
- version = "2026.22.0.dev2"
3
+ version = "2026.22.0.dev4"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "Proprietary" }
@@ -13,14 +13,14 @@ 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.22.0.dev2,<2026.22.0rc0",
17
- "unique-stock-ticker>=2026.22.0.dev1,<2026.22.0rc0",
18
- "unique-follow-up-questions>=2026.22.0.dev1,<2026.22.0rc0",
19
- "unique-internal-search>=2026.22.0.dev1,<2026.22.0rc0",
20
- "unique-deep-research>=2026.22.0.dev1,<2026.22.0rc0",
21
- "unique-web-search>=2026.22.0.dev2,<2026.22.0rc0",
22
- "unique-swot>=2026.22.0.dev1,<2026.22.0rc0",
23
- "unique-skill-tool>=2026.22.0.dev1,<2026.22.0rc0",
16
+ "unique-toolkit>=2026.22.0.dev6,<2026.22.0rc0",
17
+ "unique-stock-ticker>=2026.22.0.dev2,<2026.22.0rc0",
18
+ "unique-follow-up-questions>=2026.22.0.dev2,<2026.22.0rc0",
19
+ "unique-internal-search>=2026.22.0.dev2,<2026.22.0rc0",
20
+ "unique-deep-research>=2026.22.0.dev2,<2026.22.0rc0",
21
+ "unique-web-search>=2026.22.0.dev4,<2026.22.0rc0",
22
+ "unique-swot>=2026.22.0.dev2,<2026.22.0rc0",
23
+ "unique-skill-tool>=2026.22.0.dev3,<2026.22.0rc0",
24
24
  "openai>=1.109.1,<3",
25
25
  "python-frontmatter>=1.1.0,<2",
26
26
  ]
@@ -1,7 +1,9 @@
1
1
  """Helpers for the Skill tool.
2
2
 
3
3
  Loads skill definitions from the knowledge base and registers the
4
- SkillTool with the tool manager.
4
+ SkillTool with the tool manager. Which files to load is determined only
5
+ by the per-message skill list: callers map ``ChatEventPayload.available_skills``
6
+ through ``normalize_available_skills_for_tool`` before ``configure_skill_tool``.
5
7
 
6
8
  Skill discovery follows the official Agent Skills protocol — see
7
9
  https://agentskills.io/home. Each skill is a **folder**
@@ -41,16 +43,17 @@ from __future__ import annotations
41
43
 
42
44
  import asyncio
43
45
  from logging import Logger
44
- from typing import TYPE_CHECKING, Any, cast
46
+ from typing import TYPE_CHECKING, Any
45
47
 
46
48
  import frontmatter
47
49
  from pydantic import ValidationError
48
50
  from unique_skill_tool.config import SkillToolConfig
49
- from unique_skill_tool.schemas import SelectableSkill, SkillDefinition
51
+ from unique_skill_tool.schemas import SkillDefinition
50
52
  from unique_skill_tool.service import SkillTool
51
- from unique_skill_tool.utils import extract_invoked_skills, normalize_skill_name
53
+ from unique_skill_tool.utils import normalize_skill_name
52
54
  from unique_toolkit.agentic.tools.config import ToolBuildConfig
53
55
  from unique_toolkit.agentic.tools.schemas import ToolCallResponse
56
+ from unique_toolkit.app.schemas import SkillReference
54
57
  from unique_toolkit.language_model.schemas import LanguageModelFunction
55
58
 
56
59
  if TYPE_CHECKING:
@@ -97,6 +100,32 @@ def _parse_frontmatter(*, text: str) -> tuple[dict[str, Any], str]:
97
100
  return dict(metadata), body
98
101
 
99
102
 
103
+ def normalize_available_skills_for_tool(
104
+ available_skills: list[SkillReference],
105
+ ) -> list[SkillReference]:
106
+ """Normalize ``ChatEventPayload.available_skills`` for the Skill tool.
107
+
108
+ Call this at the orchestration boundary (e.g. when building ``UniqueAI``)
109
+ before ``configure_skill_tool``. Deduplicates by ``content_id`` while
110
+ preserving first-seen order. Entries without a ``content_id`` are skipped.
111
+ """
112
+ seen: set[str] = set()
113
+ out: list[SkillReference] = []
114
+ for choice in available_skills:
115
+ cid = choice.content_id
116
+ if not cid or cid in seen:
117
+ continue
118
+ seen.add(cid)
119
+ out.append(
120
+ SkillReference(
121
+ name=choice.name,
122
+ scope_id=choice.scope_id,
123
+ content_id=choice.content_id,
124
+ )
125
+ )
126
+ return out
127
+
128
+
100
129
  def _build_skill(
101
130
  *,
102
131
  content_id: str,
@@ -149,10 +178,10 @@ def _build_skill(
149
178
  async def load_selectable_skills(
150
179
  *,
151
180
  content_service: ContentService,
152
- selectable_skills: list[SelectableSkill],
181
+ selectable_skills: list[SkillReference],
153
182
  logger: Logger,
154
183
  ) -> dict[str, SkillDefinition]:
155
- """Load skills from an explicit list of ``SelectableSkill`` references.
184
+ """Load skills from an explicit list of ``SkillReference`` references.
156
185
 
157
186
  Entries with an empty ``content_id`` are skipped (admin UIs commonly
158
187
  leave a blank row as a placeholder). Failures are logged and do not
@@ -232,28 +261,37 @@ async def configure_skill_tool(
232
261
  logger: Logger,
233
262
  content_service: ContentService,
234
263
  tool_manager: ToolManager | ResponsesApiToolManager,
264
+ selectable_skills: list[SkillReference] | None = None,
235
265
  ) -> None:
236
- """Populate the SkillTool's skill registry when it is enabled in ``space.tools``.
237
-
238
- Skills are loaded from ``selectable_skills`` only. Each entry should
239
- reference a ``SKILL.md`` document via ``content_id``.
240
- """
266
+ """Populate the SkillTool's skill registry when it is enabled in ``space.tools``."""
241
267
  skill_tool_build_config = _find_skill_tool_build_config(config.space.tools)
242
268
  if skill_tool_build_config is None or not skill_tool_build_config.is_enabled:
243
269
  return
244
270
 
245
- skill_config = cast(SkillToolConfig, skill_tool_build_config.configuration)
271
+ to_load = list(selectable_skills or [])
246
272
 
247
- if not skill_config.selectable_skills.selected:
273
+ if not isinstance(skill_tool_build_config.configuration, SkillToolConfig):
248
274
  logger.warning(
249
- "SkillTool is enabled but no skills are configured — no skills will be loaded."
275
+ "SkillTool build config has unexpected configuration type '%s' — "
276
+ "skills will not be loaded and the tool will be excluded.",
277
+ type(skill_tool_build_config.configuration).__name__,
278
+ )
279
+ tool_manager.exclude_tool(SkillTool.name)
280
+ return
281
+
282
+ skill_tool_build_config.configuration.selectable_skills.selected = to_load
283
+
284
+ if not to_load:
285
+ logger.warning(
286
+ "SkillTool is enabled but selectable_skills is empty — "
287
+ "no skills will be loaded."
250
288
  )
251
289
  tool_manager.exclude_tool(SkillTool.name)
252
290
  return
253
291
 
254
292
  skill_registry = await load_selectable_skills(
255
293
  content_service=content_service,
256
- selectable_skills=skill_config.selectable_skills.selected,
294
+ selectable_skills=to_load,
257
295
  logger=logger,
258
296
  )
259
297
 
@@ -276,58 +314,45 @@ async def configure_skill_tool(
276
314
 
277
315
  async def preload_invoked_skills(
278
316
  *,
279
- text: str,
280
317
  tool_manager: ToolManager | ResponsesApiToolManager,
281
318
  history_manager: HistoryManager,
282
319
  logger: Logger,
283
- skill_choices: list[SelectableSkill],
320
+ skill_choices: list[SkillReference],
284
321
  ) -> None:
285
- """Preload forced and slash-invoked skills before the first model turn.
322
+ """Preload skills selected in ``skill_choices`` before the first model turn.
286
323
 
287
324
  Mirrors the normal mid-loop activation path so preloaded skills are
288
325
  indistinguishable from skills the model activates itself:
289
326
 
290
- 1. Resolves forced ``skill_choices`` first.
291
- 2. If no forced skills are found, pulls every ``/skill-name`` token
292
- out of the user message — whether at the start, between words, or
293
- at the end — as long as it is properly word-boundaried so URLs and
294
- file paths are not mistaken for invocations. Unknown tokens are
295
- left as ordinary text.
327
+ 1. Resolves each ``skill_choices`` entry against the registered
328
+ ``SkillTool`` registry (by normalized name or ``content_id``).
296
329
  2. For each matched skill, synthesizes a ``LanguageModelFunction``
297
330
  and runs it through the already-registered ``SkillTool`` — the
298
331
  exact same code path ``UniqueAI._handle_tool_calls`` uses.
299
332
  3. Appends the synthetic assistant tool-call message plus the
300
333
  resulting tool-result messages to history, so the first model
301
334
  turn sees the skills as already-activated tool calls.
302
- ``skill_choices`` are treated as forced skills and loaded even when
303
- the user message does not contain ``/skill-name`` tokens. Duplicate
304
- choices that resolve to the same registered skill are ignored after
305
- the first (same deduplication idea as ``extract_invoked_skills``).
306
- User message text is never modified in this preload step.
335
+
336
+ Duplicate choices that resolve to the same registered skill are
337
+ ignored after the first.
307
338
  """
308
339
  skill_tool = tool_manager.get_tool_by_name(SkillTool.name)
309
340
  if not isinstance(skill_tool, SkillTool):
310
341
  return
311
342
 
312
- forced_skills = []
313
-
314
- if skill_choices:
315
- seen_forced: set[str] = set()
316
- for choice in skill_choices:
317
- if not choice.name:
318
- continue
343
+ forced_skills: list[SkillDefinition] = []
344
+ seen_forced: set[str] = set()
345
+ for choice in skill_choices:
346
+ forced_skill: SkillDefinition | None = None
347
+ if choice.name:
319
348
  normalized_name = normalize_skill_name(choice.name)
320
349
  forced_skill = skill_tool.skill_registry.get(normalized_name)
321
- if forced_skill is None:
322
- continue
323
- if forced_skill.name in seen_forced:
324
- continue
325
- seen_forced.add(forced_skill.name)
326
- forced_skills.append(forced_skill)
327
-
328
- else:
329
- if text:
330
- forced_skills = extract_invoked_skills(text, skill_tool.skill_registry)
350
+ if forced_skill is None:
351
+ continue
352
+ if forced_skill.name in seen_forced:
353
+ continue
354
+ seen_forced.add(forced_skill.name)
355
+ forced_skills.append(forced_skill)
331
356
 
332
357
  if not forced_skills:
333
358
  return
@@ -214,7 +214,6 @@ class UniqueAI:
214
214
  run_start = time.perf_counter()
215
215
 
216
216
  await preload_invoked_skills(
217
- text=self._event.payload.user_message.text,
218
217
  tool_manager=self._tool_manager,
219
218
  history_manager=self._history_manager,
220
219
  logger=self._logger,
@@ -82,7 +82,10 @@ from unique_orchestrator._builders.open_file_setup import (
82
82
  configure_file_payload,
83
83
  handle_uploaded_file_tool_choices,
84
84
  )
85
- from unique_orchestrator._builders.skill_setup import configure_skill_tool
85
+ from unique_orchestrator._builders.skill_setup import (
86
+ configure_skill_tool,
87
+ normalize_available_skills_for_tool,
88
+ )
86
89
  from unique_orchestrator.config import UniqueAIConfig
87
90
  from unique_orchestrator.unique_ai import UniqueAI
88
91
  from unique_orchestrator.utils import filter_uploaded_documents_by_selection
@@ -450,6 +453,9 @@ async def _build_responses(
450
453
  logger=logger,
451
454
  content_service=common_components.content_service,
452
455
  tool_manager=tool_manager,
456
+ selectable_skills=normalize_available_skills_for_tool(
457
+ event.payload.available_skills
458
+ ),
453
459
  )
454
460
 
455
461
  loop_iteration_runner = build_responses_loop_iteration_runner(
@@ -528,6 +534,9 @@ async def _build_completions(
528
534
  logger=logger,
529
535
  content_service=common_components.content_service,
530
536
  tool_manager=tool_manager,
537
+ selectable_skills=normalize_available_skills_for_tool(
538
+ event.payload.available_skills
539
+ ),
531
540
  )
532
541
 
533
542
  postprocessor_manager = common_components.postprocessor_manager