unique_orchestrator 2026.22.0.dev3__tar.gz → 2026.22.0.dev5__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.dev3 → unique_orchestrator-2026.22.0.dev5}/PKG-INFO +3 -3
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/pyproject.toml +3 -3
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/_builders/skill_setup.py +80 -50
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/unique_ai.py +0 -1
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/unique_ai_builder.py +10 -1
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/README.md +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/__init__.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/_builders/__init__.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/_builders/inject_tool_reminders.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/_builders/loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/_builders/open_file_setup.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/config.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/prompts/generic_reference_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/prompts/system_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/prompts/user_message_prompt.jinja2 +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/settings.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_build_loop_iteration_runner.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_config_services_validators.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_qwen_max_loop_iterations.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_register_code_interpreter_postprocessors.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_async_modify.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_execution_timing.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_get_filtered_user_metadata.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_log_tool_calls.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_persist_tool_calls.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_plan_or_execute_include.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_reference_order.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/unique_orchestrator/tests/test_unique_ai_render_system_prompt_user_instructions.py +0 -0
- {unique_orchestrator-2026.22.0.dev3 → unique_orchestrator-2026.22.0.dev5}/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.dev5
|
|
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.
|
|
12
|
+
Requires-Dist: unique-toolkit>=2026.22.0.dev6,<2026.22.0rc0
|
|
13
13
|
Requires-Dist: unique-stock-ticker>=2026.22.0.dev2,<2026.22.0rc0
|
|
14
14
|
Requires-Dist: unique-follow-up-questions>=2026.22.0.dev2,<2026.22.0rc0
|
|
15
15
|
Requires-Dist: unique-internal-search>=2026.22.0.dev2,<2026.22.0rc0
|
|
16
16
|
Requires-Dist: unique-deep-research>=2026.22.0.dev2,<2026.22.0rc0
|
|
17
17
|
Requires-Dist: unique-web-search>=2026.22.0.dev4,<2026.22.0rc0
|
|
18
18
|
Requires-Dist: unique-swot>=2026.22.0.dev2,<2026.22.0rc0
|
|
19
|
-
Requires-Dist: unique-skill-tool>=2026.22.0.
|
|
19
|
+
Requires-Dist: unique-skill-tool>=2026.22.0.dev4,<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.dev5"
|
|
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.
|
|
16
|
+
"unique-toolkit>=2026.22.0.dev6,<2026.22.0rc0",
|
|
17
17
|
"unique-stock-ticker>=2026.22.0.dev2,<2026.22.0rc0",
|
|
18
18
|
"unique-follow-up-questions>=2026.22.0.dev2,<2026.22.0rc0",
|
|
19
19
|
"unique-internal-search>=2026.22.0.dev2,<2026.22.0rc0",
|
|
20
20
|
"unique-deep-research>=2026.22.0.dev2,<2026.22.0rc0",
|
|
21
21
|
"unique-web-search>=2026.22.0.dev4,<2026.22.0rc0",
|
|
22
22
|
"unique-swot>=2026.22.0.dev2,<2026.22.0rc0",
|
|
23
|
-
"unique-skill-tool>=2026.22.0.
|
|
23
|
+
"unique-skill-tool>=2026.22.0.dev4,<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,16 @@ 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 extract_invoked_skills, normalize_skill_name
|
|
52
53
|
from unique_toolkit.agentic.tools.config import ToolBuildConfig
|
|
53
54
|
from unique_toolkit.agentic.tools.schemas import ToolCallResponse
|
|
55
|
+
from unique_toolkit.app.schemas import SkillReference
|
|
54
56
|
from unique_toolkit.language_model.schemas import LanguageModelFunction
|
|
55
57
|
|
|
56
58
|
if TYPE_CHECKING:
|
|
@@ -97,6 +99,32 @@ def _parse_frontmatter(*, text: str) -> tuple[dict[str, Any], str]:
|
|
|
97
99
|
return dict(metadata), body
|
|
98
100
|
|
|
99
101
|
|
|
102
|
+
def normalize_available_skills_for_tool(
|
|
103
|
+
available_skills: list[SkillReference],
|
|
104
|
+
) -> list[SkillReference]:
|
|
105
|
+
"""Normalize ``ChatEventPayload.available_skills`` for the Skill tool.
|
|
106
|
+
|
|
107
|
+
Call this at the orchestration boundary (e.g. when building ``UniqueAI``)
|
|
108
|
+
before ``configure_skill_tool``. Deduplicates by ``content_id`` while
|
|
109
|
+
preserving first-seen order. Entries without a ``content_id`` are skipped.
|
|
110
|
+
"""
|
|
111
|
+
seen: set[str] = set()
|
|
112
|
+
out: list[SkillReference] = []
|
|
113
|
+
for choice in available_skills:
|
|
114
|
+
cid = choice.content_id
|
|
115
|
+
if not cid or cid in seen:
|
|
116
|
+
continue
|
|
117
|
+
seen.add(cid)
|
|
118
|
+
out.append(
|
|
119
|
+
SkillReference(
|
|
120
|
+
name=choice.name,
|
|
121
|
+
scope_id=choice.scope_id,
|
|
122
|
+
content_id=choice.content_id,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
100
128
|
def _build_skill(
|
|
101
129
|
*,
|
|
102
130
|
content_id: str,
|
|
@@ -111,8 +139,8 @@ def _build_skill(
|
|
|
111
139
|
is rejected by ``SkillDefinition`` rather than silently flowing into
|
|
112
140
|
the OpenAI tool enum where the model could never emit it verbatim.
|
|
113
141
|
|
|
114
|
-
``content_id``
|
|
115
|
-
|
|
142
|
+
``content_id`` is stored on the returned definition so it can be used
|
|
143
|
+
as a lookup key to load the SKILL.md.
|
|
116
144
|
"""
|
|
117
145
|
if not file_text.strip():
|
|
118
146
|
return None
|
|
@@ -135,6 +163,7 @@ def _build_skill(
|
|
|
135
163
|
name=name,
|
|
136
164
|
description=description,
|
|
137
165
|
content=body,
|
|
166
|
+
content_id=content_id,
|
|
138
167
|
)
|
|
139
168
|
except ValidationError as exc:
|
|
140
169
|
logger.warning(
|
|
@@ -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,50 @@ 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
|
-
|
|
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 ``content_id``. Entries without a
|
|
329
|
+
``content_id`` are skipped — every skill choice must carry one.
|
|
296
330
|
2. For each matched skill, synthesizes a ``LanguageModelFunction``
|
|
297
331
|
and runs it through the already-registered ``SkillTool`` — the
|
|
298
332
|
exact same code path ``UniqueAI._handle_tool_calls`` uses.
|
|
299
333
|
3. Appends the synthetic assistant tool-call message plus the
|
|
300
334
|
resulting tool-result messages to history, so the first model
|
|
301
335
|
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.
|
|
336
|
+
|
|
337
|
+
Duplicate choices that resolve to the same registered skill are
|
|
338
|
+
ignored after the first.
|
|
307
339
|
"""
|
|
308
340
|
skill_tool = tool_manager.get_tool_by_name(SkillTool.name)
|
|
309
341
|
if not isinstance(skill_tool, SkillTool):
|
|
310
342
|
return
|
|
311
343
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if text:
|
|
330
|
-
forced_skills = extract_invoked_skills(text, skill_tool.skill_registry)
|
|
344
|
+
# content_id → skill.
|
|
345
|
+
content_id_index: dict[str, SkillDefinition] = {
|
|
346
|
+
s.content_id: s for s in skill_tool.skill_registry.values() if s.content_id
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
forced_skills: list[SkillDefinition] = []
|
|
350
|
+
seen_forced: set[str] = set()
|
|
351
|
+
for choice in skill_choices:
|
|
352
|
+
forced_skill: SkillDefinition | None = None
|
|
353
|
+
if choice.content_id:
|
|
354
|
+
forced_skill = content_id_index.get(choice.content_id)
|
|
355
|
+
if forced_skill is None:
|
|
356
|
+
continue
|
|
357
|
+
if forced_skill.name in seen_forced:
|
|
358
|
+
continue
|
|
359
|
+
seen_forced.add(forced_skill.name)
|
|
360
|
+
forced_skills.append(forced_skill)
|
|
331
361
|
|
|
332
362
|
if not forced_skills:
|
|
333
363
|
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
|