unique-skill-tool 2026.22.0.dev5__tar.gz → 2026.22.0.dev6__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_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/PKG-INFO +3 -2
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/pyproject.toml +3 -2
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/__init__.py +2 -0
- unique_skill_tool-2026.22.0.dev6/unique_skill_tool/loader.py +137 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/schemas.py +20 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/service.py +27 -4
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/README.md +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/config.py +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/examples/README.md +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/examples/analyze-data/SKILL.md +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/examples/analyze-factsheet/SKILL.md +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/examples/draft-email/SKILL.md +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/examples/review-contract/SKILL.md +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/prompts.py +0 -0
- {unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/utils.py +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-skill-tool
|
|
3
|
-
Version: 2026.22.0.
|
|
3
|
+
Version: 2026.22.0.dev6
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Fabian Schläpfer
|
|
6
6
|
Author-email: Fabian Schläpfer <fabian@unique.ch>
|
|
7
7
|
License: Proprietary
|
|
8
8
|
Requires-Dist: jinja2>=3.1.0,<4
|
|
9
9
|
Requires-Dist: pydantic>=2.8.2,<3
|
|
10
|
-
Requires-Dist:
|
|
10
|
+
Requires-Dist: python-frontmatter>=1.1.0,<2
|
|
11
|
+
Requires-Dist: unique-toolkit>=2026.22.0.dev11,<2026.22.0rc0
|
|
11
12
|
Requires-Python: >=3.12, <4
|
|
12
13
|
Description-Content-Type: text/markdown
|
|
13
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "unique_skill_tool"
|
|
3
|
-
version = "2026.22.0.
|
|
3
|
+
version = "2026.22.0.dev6"
|
|
4
4
|
description = ""
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Proprietary" }
|
|
@@ -11,7 +11,8 @@ requires-python = ">=3.12,<4"
|
|
|
11
11
|
dependencies = [
|
|
12
12
|
"jinja2>=3.1.0,<4",
|
|
13
13
|
"pydantic>=2.8.2,<3",
|
|
14
|
-
"
|
|
14
|
+
"python-frontmatter>=1.1.0,<2",
|
|
15
|
+
"unique-toolkit>=2026.22.0.dev11,<2026.22.0rc0",
|
|
15
16
|
]
|
|
16
17
|
|
|
17
18
|
[build-system]
|
{unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/__init__.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from unique_skill_tool.config import SkillToolConfig
|
|
2
|
+
from unique_skill_tool.loader import parse_skill_file
|
|
2
3
|
from unique_skill_tool.schemas import SkillDefinition
|
|
3
4
|
from unique_skill_tool.service import SkillTool
|
|
4
5
|
|
|
@@ -6,4 +7,5 @@ __all__ = [
|
|
|
6
7
|
"SkillTool",
|
|
7
8
|
"SkillToolConfig",
|
|
8
9
|
"SkillDefinition",
|
|
10
|
+
"parse_skill_file",
|
|
9
11
|
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Parse SKILL.md files into ``SkillDefinition`` objects.
|
|
2
|
+
|
|
3
|
+
Every skill is a folder with a ``SKILL.md`` entrypoint that carries YAML
|
|
4
|
+
frontmatter::
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
name: summarize-report
|
|
8
|
+
description: >-
|
|
9
|
+
Summarize a document into key findings.
|
|
10
|
+
metadata: # optional
|
|
11
|
+
thinking_level: high
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Summarize Report
|
|
15
|
+
...instructions...
|
|
16
|
+
|
|
17
|
+
:func:`parse_skill_file` is the single public entry point: it accepts the raw
|
|
18
|
+
text of one ``SKILL.md`` file and returns a ``SkillDefinition``, or ``None``
|
|
19
|
+
when the file is empty or malformed.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from logging import Logger
|
|
25
|
+
|
|
26
|
+
import frontmatter
|
|
27
|
+
from pydantic import ValidationError
|
|
28
|
+
from unique_toolkit.language_model.schemas import to_reasoning_effort
|
|
29
|
+
|
|
30
|
+
from unique_skill_tool.schemas import SkillDefinition, SkillMetadata
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_frontmatter(*, text: str) -> tuple[dict[str, object], str]:
|
|
34
|
+
"""Split YAML frontmatter from the markdown body.
|
|
35
|
+
|
|
36
|
+
Thin wrapper around ``python-frontmatter``. On a YAML parse error, or when
|
|
37
|
+
the frontmatter parses to a non-mapping (e.g. a top-level YAML list), falls
|
|
38
|
+
back to ``({}, <original text>)`` so a broken skill file never leaks raw
|
|
39
|
+
``---\\nname: ...\\n---`` delimiters into the LLM prompt and never raises.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
post = frontmatter.loads(text)
|
|
43
|
+
body = post.content
|
|
44
|
+
return dict(post.metadata), body
|
|
45
|
+
except Exception:
|
|
46
|
+
return {}, text
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_skill_file(
|
|
50
|
+
*,
|
|
51
|
+
file_text: str,
|
|
52
|
+
content_id: str,
|
|
53
|
+
source_label: str = "",
|
|
54
|
+
logger: Logger | None = None,
|
|
55
|
+
) -> SkillDefinition | None:
|
|
56
|
+
"""Build a ``SkillDefinition`` from the raw text of a SKILL.md file.
|
|
57
|
+
|
|
58
|
+
Parses YAML frontmatter for ``name`` and ``description``. A malformed
|
|
59
|
+
``name`` (non-kebab-case, contains whitespace/punctuation, too long) is
|
|
60
|
+
rejected by ``SkillDefinition`` validation rather than silently flowing into
|
|
61
|
+
the OpenAI tool enum where the model could never emit it verbatim.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
file_text: Raw UTF-8 text of the SKILL.md file.
|
|
65
|
+
content_id: Knowledge-base content ID this skill was loaded from.
|
|
66
|
+
source_label: Human-readable identifier used only in warning messages
|
|
67
|
+
so callers can locate the offending file (e.g. a knowledge-base
|
|
68
|
+
content key or file path).
|
|
69
|
+
logger: Optional logger for diagnostic warnings. Pass ``None`` to
|
|
70
|
+
suppress all log output (useful in tests).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A validated ``SkillDefinition``, or ``None`` when the file is empty,
|
|
74
|
+
missing required frontmatter fields, or fails schema validation.
|
|
75
|
+
"""
|
|
76
|
+
if not file_text.strip():
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
metadata, body = _parse_frontmatter(text=file_text)
|
|
80
|
+
|
|
81
|
+
name = metadata.get("name")
|
|
82
|
+
description = metadata.get("description")
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
not isinstance(name, str)
|
|
86
|
+
or not isinstance(description, str)
|
|
87
|
+
or not name
|
|
88
|
+
or not description
|
|
89
|
+
):
|
|
90
|
+
if logger is not None:
|
|
91
|
+
logger.warning(
|
|
92
|
+
"Skipping '%s': wrong skill format.",
|
|
93
|
+
source_label,
|
|
94
|
+
)
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
skill_meta: SkillMetadata | None = None
|
|
98
|
+
raw_meta = metadata.get("metadata")
|
|
99
|
+
if raw_meta is not None and not isinstance(raw_meta, dict):
|
|
100
|
+
if logger is not None:
|
|
101
|
+
logger.warning(
|
|
102
|
+
"Skill '%s': 'metadata' must be a key-value mapping, got %r — ignoring.",
|
|
103
|
+
source_label,
|
|
104
|
+
type(raw_meta).__name__,
|
|
105
|
+
)
|
|
106
|
+
raw_meta = None
|
|
107
|
+
if isinstance(raw_meta, dict):
|
|
108
|
+
raw_thinking = raw_meta.get("thinking_level")
|
|
109
|
+
thinking_level = None
|
|
110
|
+
if raw_thinking is not None:
|
|
111
|
+
try:
|
|
112
|
+
thinking_level = to_reasoning_effort(str(raw_thinking))
|
|
113
|
+
except ValueError:
|
|
114
|
+
if logger is not None:
|
|
115
|
+
logger.warning(
|
|
116
|
+
"Skill '%s': unknown thinking_level %r — ignoring.",
|
|
117
|
+
source_label,
|
|
118
|
+
raw_thinking,
|
|
119
|
+
)
|
|
120
|
+
skill_meta = SkillMetadata(thinking_level=thinking_level)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return SkillDefinition(
|
|
124
|
+
name=name,
|
|
125
|
+
description=description,
|
|
126
|
+
content=body,
|
|
127
|
+
content_id=content_id,
|
|
128
|
+
metadata=skill_meta,
|
|
129
|
+
)
|
|
130
|
+
except ValidationError as exc:
|
|
131
|
+
if logger is not None:
|
|
132
|
+
logger.warning(
|
|
133
|
+
"Skipping '%s': invalid skill definition: %s",
|
|
134
|
+
source_label,
|
|
135
|
+
exc.errors(include_url=False),
|
|
136
|
+
)
|
|
137
|
+
return None
|
{unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/schemas.py
RENAMED
|
@@ -2,11 +2,27 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
from unique_toolkit._common.pydantic_helpers import get_configuration_dict
|
|
5
|
+
from unique_toolkit.language_model.schemas import ReasoningEffort
|
|
5
6
|
|
|
6
7
|
SKILL_NAME_PATTERN = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
7
8
|
SKILL_NAME_MAX_LENGTH = 64
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
class SkillMetadata(BaseModel):
|
|
12
|
+
"""Typed representation of a skill's SKILL.md ``metadata`` frontmatter block."""
|
|
13
|
+
|
|
14
|
+
model_config = get_configuration_dict()
|
|
15
|
+
|
|
16
|
+
thinking_level: ReasoningEffort | None = Field(
|
|
17
|
+
default=None,
|
|
18
|
+
description=(
|
|
19
|
+
"Optional reasoning effort hint. The orchestrator uses the highest "
|
|
20
|
+
"level across all activated skills in a run to set "
|
|
21
|
+
"``reasoning_effort`` on the LLM call."
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
10
26
|
class SkillDefinition(BaseModel):
|
|
11
27
|
"""A skill that the agent can activate via the SkillTool.
|
|
12
28
|
|
|
@@ -36,3 +52,7 @@ class SkillDefinition(BaseModel):
|
|
|
36
52
|
content_id: str = Field(
|
|
37
53
|
description="Knowledge-base content ID this skill was loaded from.",
|
|
38
54
|
)
|
|
55
|
+
metadata: SkillMetadata | None = Field(
|
|
56
|
+
default=None,
|
|
57
|
+
description="Parsed ``metadata`` block from the skill's SKILL.md frontmatter.",
|
|
58
|
+
)
|
{unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/service.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import override
|
|
3
|
+
from typing import cast, override
|
|
4
4
|
|
|
5
5
|
from jinja2.sandbox import SandboxedEnvironment
|
|
6
6
|
from unique_toolkit.agentic.evaluation.schemas import EvaluationMetricName
|
|
@@ -11,14 +11,14 @@ from unique_toolkit.agentic.tools.tool_progress_reporter import ToolProgressRepo
|
|
|
11
11
|
from unique_toolkit.app.schemas import ChatEvent
|
|
12
12
|
from unique_toolkit.chat.schemas import MessageLog, MessageLogStatus
|
|
13
13
|
from unique_toolkit.language_model.schemas import (
|
|
14
|
+
REASONING_EFFORT_ORDER,
|
|
14
15
|
LanguageModelFunction,
|
|
15
16
|
LanguageModelToolDescription,
|
|
17
|
+
ReasoningEffort,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
from unique_skill_tool.config import SkillToolConfig
|
|
19
|
-
from unique_skill_tool.schemas import
|
|
20
|
-
SkillDefinition,
|
|
21
|
-
)
|
|
21
|
+
from unique_skill_tool.schemas import SkillDefinition
|
|
22
22
|
from unique_skill_tool.utils import (
|
|
23
23
|
format_skill_listing,
|
|
24
24
|
normalize_skill_name,
|
|
@@ -46,6 +46,7 @@ class SkillTool(Tool[SkillToolConfig]):
|
|
|
46
46
|
) -> None:
|
|
47
47
|
super().__init__(config, event, tool_progress_reporter)
|
|
48
48
|
self._skill_registry: dict[str, SkillDefinition] = {}
|
|
49
|
+
self._activated_skills: list[SkillDefinition] = []
|
|
49
50
|
|
|
50
51
|
@property
|
|
51
52
|
def skill_registry(self) -> dict[str, SkillDefinition]:
|
|
@@ -55,6 +56,27 @@ class SkillTool(Tool[SkillToolConfig]):
|
|
|
55
56
|
def skill_registry(self, value: dict[str, SkillDefinition]) -> None:
|
|
56
57
|
self._skill_registry = value
|
|
57
58
|
|
|
59
|
+
@property
|
|
60
|
+
def activated_skills(self) -> list[SkillDefinition]:
|
|
61
|
+
"""All skills successfully activated in this run, in activation order."""
|
|
62
|
+
return self._activated_skills
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def max_thinking_level(self) -> ReasoningEffort | None:
|
|
66
|
+
"""Highest ``thinking_level`` across all skills activated in this run.
|
|
67
|
+
|
|
68
|
+
Returns ``None`` when no activated skill declares a ``thinking_level``.
|
|
69
|
+
The ordering is ``none < minimal < low < medium < high < xhigh``.
|
|
70
|
+
"""
|
|
71
|
+
levels = [
|
|
72
|
+
s.metadata.thinking_level
|
|
73
|
+
for s in self._activated_skills
|
|
74
|
+
if s.metadata is not None and s.metadata.thinking_level is not None
|
|
75
|
+
]
|
|
76
|
+
if not levels:
|
|
77
|
+
return None
|
|
78
|
+
return cast(ReasoningEffort, max(levels, key=REASONING_EFFORT_ORDER.index))
|
|
79
|
+
|
|
58
80
|
@override
|
|
59
81
|
def display_name(self) -> str:
|
|
60
82
|
return "Skill"
|
|
@@ -153,6 +175,7 @@ class SkillTool(Tool[SkillToolConfig]):
|
|
|
153
175
|
),
|
|
154
176
|
)
|
|
155
177
|
|
|
178
|
+
self._activated_skills.append(skill)
|
|
156
179
|
self._active_message_log = await self._log_skill_loaded(skill_name=skill_name)
|
|
157
180
|
|
|
158
181
|
content_parts = [
|
|
File without changes
|
{unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/prompts.py
RENAMED
|
File without changes
|
{unique_skill_tool-2026.22.0.dev5 → unique_skill_tool-2026.22.0.dev6}/unique_skill_tool/utils.py
RENAMED
|
File without changes
|