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.
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/PKG-INFO +9 -9
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/pyproject.toml +9 -9
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/skill_setup.py +71 -46
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/unique_ai.py +0 -1
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/unique_ai_builder.py +10 -1
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/README.md +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/__init__.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/__init__.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/_builders/open_file_setup.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/config.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/settings.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
- {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
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
- {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
- {unique_orchestrator-2026.22.0.dev2 → unique_orchestrator-2026.22.0.dev4}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
- {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
- {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.
|
|
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.
|
|
13
|
-
Requires-Dist: unique-stock-ticker>=2026.22.0.
|
|
14
|
-
Requires-Dist: unique-follow-up-questions>=2026.22.0.
|
|
15
|
-
Requires-Dist: unique-internal-search>=2026.22.0.
|
|
16
|
-
Requires-Dist: unique-deep-research>=2026.22.0.
|
|
17
|
-
Requires-Dist: unique-web-search>=2026.22.0.
|
|
18
|
-
Requires-Dist: unique-swot>=2026.22.0.
|
|
19
|
-
Requires-Dist: unique-skill-tool>=2026.22.0.
|
|
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.
|
|
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.
|
|
17
|
-
"unique-stock-ticker>=2026.22.0.
|
|
18
|
-
"unique-follow-up-questions>=2026.22.0.
|
|
19
|
-
"unique-internal-search>=2026.22.0.
|
|
20
|
-
"unique-deep-research>=2026.22.0.
|
|
21
|
-
"unique-web-search>=2026.22.0.
|
|
22
|
-
"unique-swot>=2026.22.0.
|
|
23
|
-
"unique-skill-tool>=2026.22.0.
|
|
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
|
|
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
|
|
51
|
+
from unique_skill_tool.schemas import SkillDefinition
|
|
50
52
|
from unique_skill_tool.service import SkillTool
|
|
51
|
-
from unique_skill_tool.utils import
|
|
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[
|
|
181
|
+
selectable_skills: list[SkillReference],
|
|
153
182
|
logger: Logger,
|
|
154
183
|
) -> dict[str, SkillDefinition]:
|
|
155
|
-
"""Load skills from an explicit list of ``
|
|
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
|
-
|
|
271
|
+
to_load = list(selectable_skills or [])
|
|
246
272
|
|
|
247
|
-
if not
|
|
273
|
+
if not isinstance(skill_tool_build_config.configuration, SkillToolConfig):
|
|
248
274
|
logger.warning(
|
|
249
|
-
"SkillTool
|
|
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=
|
|
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[
|
|
320
|
+
skill_choices: list[SkillReference],
|
|
284
321
|
) -> None:
|
|
285
|
-
"""Preload
|
|
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
|
|
291
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|