plm-skill-kernel 1.0.0__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.
- plm_skill_kernel-1.0.0/PKG-INFO +135 -0
- plm_skill_kernel-1.0.0/README.md +115 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/__init__.py +33 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/__main__.py +13 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/dispatcher.py +141 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/registry.py +80 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/__init__.py +29 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/_registry.py +111 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/context.py +66 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/decorator.py +147 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/types.py +86 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/server.py +173 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/__init__.py +16 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/agents/__init__.py +13 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/agents/suggest.py +191 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/bpmn/__init__.py +9 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/bpmn/generate.py +147 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/cleansing/__init__.py +10 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/cleansing/dedupe.py +162 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/cleansing/normalise.py +178 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/documents/__init__.py +12 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/documents/parse.py +202 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/__init__.py +32 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/_v1_corpus.py +153 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/get_record.py +102 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/resolve.py +143 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/retrieve.py +143 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/search.py +161 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/validate.py +222 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/__init__.py +0 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/knowledge_pack_loader.py +17 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/knowledge_search_engine.py +25 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/plm_skill_registry.py +573 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/plm_tool_definitions.py +44 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/skill_orchestrator.py +163 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/PKG-INFO +135 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/SOURCES.txt +51 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/dependency_links.txt +1 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/entry_points.txt +2 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/requires.txt +12 -0
- plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/top_level.txt +1 -0
- plm_skill_kernel-1.0.0/pyproject.toml +113 -0
- plm_skill_kernel-1.0.0/setup.cfg +4 -0
- plm_skill_kernel-1.0.0/tests/test_agents_suggest_asgi.py +208 -0
- plm_skill_kernel-1.0.0/tests/test_agents_suggest_unit.py +72 -0
- plm_skill_kernel-1.0.0/tests/test_dispatcher.py +98 -0
- plm_skill_kernel-1.0.0/tests/test_documents_parse_asgi.py +197 -0
- plm_skill_kernel-1.0.0/tests/test_documents_parse_unit.py +99 -0
- plm_skill_kernel-1.0.0/tests/test_sdk_decorator.py +113 -0
- plm_skill_kernel-1.0.0/tests/test_server.py +202 -0
- plm_skill_kernel-1.0.0/tests/test_skills_cleansing.py +203 -0
- plm_skill_kernel-1.0.0/tests/test_skills_knowledge.py +376 -0
- plm_skill_kernel-1.0.0/tests/test_skills_knowledge_registry.py +81 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plm-skill-kernel
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: TracePulse PLM Skill Kernel — Wave 2 Conv A. Decorator-based Skill SDK + V1 in-process dispatcher + Kernel HTTP server stub matching CR.10 §7.bis (POST /v1/skills/{id}/invoke). Ships 3 starter skills (cleansing.normalise, cleansing.dedupe, bpmn.generate). Sibling of plm-engine-core; the two communicate over the V1.1 HTTP loopback (SKILL_KERNEL_LOOPBACK=on) — no Python-level coupling at the dispatch boundary.
|
|
5
|
+
Author: TracePulse
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: plm-shared
|
|
10
|
+
Requires-Dist: pydantic>=2.5.0
|
|
11
|
+
Requires-Dist: fastapi>=0.110
|
|
12
|
+
Requires-Dist: uvicorn>=0.27
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Requires-Dist: plm-engine-core
|
|
15
|
+
Requires-Dist: plm-knowledge
|
|
16
|
+
Provides-Extra: test
|
|
17
|
+
Requires-Dist: pytest; extra == "test"
|
|
18
|
+
Requires-Dist: pytest-asyncio; extra == "test"
|
|
19
|
+
Requires-Dist: httpx<1,>=0.27; extra == "test"
|
|
20
|
+
|
|
21
|
+
# plm-skill-kernel
|
|
22
|
+
|
|
23
|
+
Wave 2 Conv A (FTR-604). The Skill Kernel package — sibling of
|
|
24
|
+
`plm-engine-core` — that hosts the decorator-based Skill SDK, the V1
|
|
25
|
+
in-process dispatcher, and the Kernel HTTP server stub matching CR.10
|
|
26
|
+
§7.bis verbatim.
|
|
27
|
+
|
|
28
|
+
## Layout
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
02_App/plm-skill-kernel/
|
|
32
|
+
├── pyproject.toml # editable install + import-linter contracts
|
|
33
|
+
├── plm_skill_kernel/
|
|
34
|
+
│ ├── __init__.py # re-exports `skill`, `SkillContext`, etc.
|
|
35
|
+
│ ├── __main__.py # `python -m plm_skill_kernel` → server
|
|
36
|
+
│ ├── server.py # FastAPI app at POST /v1/skills/{id}/invoke
|
|
37
|
+
│ ├── dispatcher.py # V1 in-process invoker
|
|
38
|
+
│ ├── registry.py # boot wiring — discovers + registers skills
|
|
39
|
+
│ ├── sdk/
|
|
40
|
+
│ │ ├── decorator.py # `@skill(id=, version=)`
|
|
41
|
+
│ │ ├── context.py # SkillContext Pydantic
|
|
42
|
+
│ │ ├── types.py # SkillInvokeRequest / SkillInvokeResult
|
|
43
|
+
│ │ └── _registry.py # in-process decorator registry
|
|
44
|
+
│ └── skills/ # 3 starter skills (Q-W2A-1 nested layout)
|
|
45
|
+
│ ├── cleansing/
|
|
46
|
+
│ │ ├── normalise.py # cleansing.normalise / 1.0.0 / L2
|
|
47
|
+
│ │ └── dedupe.py # cleansing.dedupe / 1.0.0 / L2
|
|
48
|
+
│ └── bpmn/
|
|
49
|
+
│ └── generate.py # bpmn.generate / 1.0.0 / L1 (thin proxy)
|
|
50
|
+
└── tests/ # unit + ASGI in-process integration
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Editable install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd 02_App/backend
|
|
57
|
+
.\venv\Scripts\Activate.ps1 # Windows
|
|
58
|
+
pip install -e ../plm-skill-kernel
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Mirrors the plm-engine-core editable-install pattern.
|
|
62
|
+
|
|
63
|
+
## Dev launch (Q-W2A-3)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m plm_skill_kernel # uvicorn on port 8100
|
|
67
|
+
plm-skill-kernel --port 8100 # equivalent CLI
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The backend at port 8000 is unaffected. The V1.1 loopback round-trip
|
|
71
|
+
(D-CONV-M-4 closure) sends HTTP from the Engine Core dispatcher to
|
|
72
|
+
this Kernel server when `SKILL_KERNEL_LOOPBACK=on`.
|
|
73
|
+
|
|
74
|
+
## V1 wire contract
|
|
75
|
+
|
|
76
|
+
`POST /v1/skills/{id}/invoke` — frozen at CR.10 §7.bis.
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Headers:
|
|
80
|
+
X-Core-Caller: <UUID> (required when caller is Core)
|
|
81
|
+
Idempotency-Key: <uuid> (optional, V1)
|
|
82
|
+
traceparent: <W3C> (optional)
|
|
83
|
+
|
|
84
|
+
Request body (SkillInvokeRequest):
|
|
85
|
+
payload: Dict[str, Any]
|
|
86
|
+
version: str ("1.0.0", etc.)
|
|
87
|
+
autonomy_hint: Optional[str] ("L1" / "L2" / "L3")
|
|
88
|
+
|
|
89
|
+
Response body (SkillInvokeResult):
|
|
90
|
+
ok: bool
|
|
91
|
+
result: Optional[Dict[str, Any]]
|
|
92
|
+
error: Optional[SkillErrorEnvelope]
|
|
93
|
+
deferred_to: Optional[str]
|
|
94
|
+
cancelled: Optional[bool]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Skill authoring (Decision #59 — decorator)
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from plm_skill_kernel import skill, SkillContext
|
|
101
|
+
|
|
102
|
+
@skill(id="cleansing.normalise", version="1.0.0")
|
|
103
|
+
async def normalise(payload: dict, ctx: SkillContext) -> dict:
|
|
104
|
+
return {"normalised": _normalise_records(payload["records"])}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The decorator auto-registers into the in-process registry at module
|
|
108
|
+
import time + carries `version` through to the federated manifest
|
|
109
|
+
builder (CR.9).
|
|
110
|
+
|
|
111
|
+
## Import-linter contracts
|
|
112
|
+
|
|
113
|
+
Two Forbidden contracts live in `pyproject.toml`:
|
|
114
|
+
|
|
115
|
+
1. `plm_skill_kernel` MUST NOT import `plm_engine_core` — federation
|
|
116
|
+
contract preservation. The two communicate over HTTP, not Python.
|
|
117
|
+
2. `plm_skill_kernel` MUST NOT import `plm_accelerators` (Workbench)
|
|
118
|
+
— mirror of the plm-engine-core constraint.
|
|
119
|
+
|
|
120
|
+
Run:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
lint-imports --config 02_App/plm-skill-kernel/pyproject.toml
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Tests
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cd 02_App/plm-skill-kernel
|
|
130
|
+
python -m pytest -q
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The V1.1 HTTP round-trip integration test mounts the FastAPI app via
|
|
134
|
+
httpx.AsyncClient(ASGITransport) — no subprocess, no real port
|
|
135
|
+
(Q-W2A-4).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# plm-skill-kernel
|
|
2
|
+
|
|
3
|
+
Wave 2 Conv A (FTR-604). The Skill Kernel package — sibling of
|
|
4
|
+
`plm-engine-core` — that hosts the decorator-based Skill SDK, the V1
|
|
5
|
+
in-process dispatcher, and the Kernel HTTP server stub matching CR.10
|
|
6
|
+
§7.bis verbatim.
|
|
7
|
+
|
|
8
|
+
## Layout
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
02_App/plm-skill-kernel/
|
|
12
|
+
├── pyproject.toml # editable install + import-linter contracts
|
|
13
|
+
├── plm_skill_kernel/
|
|
14
|
+
│ ├── __init__.py # re-exports `skill`, `SkillContext`, etc.
|
|
15
|
+
│ ├── __main__.py # `python -m plm_skill_kernel` → server
|
|
16
|
+
│ ├── server.py # FastAPI app at POST /v1/skills/{id}/invoke
|
|
17
|
+
│ ├── dispatcher.py # V1 in-process invoker
|
|
18
|
+
│ ├── registry.py # boot wiring — discovers + registers skills
|
|
19
|
+
│ ├── sdk/
|
|
20
|
+
│ │ ├── decorator.py # `@skill(id=, version=)`
|
|
21
|
+
│ │ ├── context.py # SkillContext Pydantic
|
|
22
|
+
│ │ ├── types.py # SkillInvokeRequest / SkillInvokeResult
|
|
23
|
+
│ │ └── _registry.py # in-process decorator registry
|
|
24
|
+
│ └── skills/ # 3 starter skills (Q-W2A-1 nested layout)
|
|
25
|
+
│ ├── cleansing/
|
|
26
|
+
│ │ ├── normalise.py # cleansing.normalise / 1.0.0 / L2
|
|
27
|
+
│ │ └── dedupe.py # cleansing.dedupe / 1.0.0 / L2
|
|
28
|
+
│ └── bpmn/
|
|
29
|
+
│ └── generate.py # bpmn.generate / 1.0.0 / L1 (thin proxy)
|
|
30
|
+
└── tests/ # unit + ASGI in-process integration
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Editable install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
cd 02_App/backend
|
|
37
|
+
.\venv\Scripts\Activate.ps1 # Windows
|
|
38
|
+
pip install -e ../plm-skill-kernel
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Mirrors the plm-engine-core editable-install pattern.
|
|
42
|
+
|
|
43
|
+
## Dev launch (Q-W2A-3)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m plm_skill_kernel # uvicorn on port 8100
|
|
47
|
+
plm-skill-kernel --port 8100 # equivalent CLI
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The backend at port 8000 is unaffected. The V1.1 loopback round-trip
|
|
51
|
+
(D-CONV-M-4 closure) sends HTTP from the Engine Core dispatcher to
|
|
52
|
+
this Kernel server when `SKILL_KERNEL_LOOPBACK=on`.
|
|
53
|
+
|
|
54
|
+
## V1 wire contract
|
|
55
|
+
|
|
56
|
+
`POST /v1/skills/{id}/invoke` — frozen at CR.10 §7.bis.
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Headers:
|
|
60
|
+
X-Core-Caller: <UUID> (required when caller is Core)
|
|
61
|
+
Idempotency-Key: <uuid> (optional, V1)
|
|
62
|
+
traceparent: <W3C> (optional)
|
|
63
|
+
|
|
64
|
+
Request body (SkillInvokeRequest):
|
|
65
|
+
payload: Dict[str, Any]
|
|
66
|
+
version: str ("1.0.0", etc.)
|
|
67
|
+
autonomy_hint: Optional[str] ("L1" / "L2" / "L3")
|
|
68
|
+
|
|
69
|
+
Response body (SkillInvokeResult):
|
|
70
|
+
ok: bool
|
|
71
|
+
result: Optional[Dict[str, Any]]
|
|
72
|
+
error: Optional[SkillErrorEnvelope]
|
|
73
|
+
deferred_to: Optional[str]
|
|
74
|
+
cancelled: Optional[bool]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Skill authoring (Decision #59 — decorator)
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from plm_skill_kernel import skill, SkillContext
|
|
81
|
+
|
|
82
|
+
@skill(id="cleansing.normalise", version="1.0.0")
|
|
83
|
+
async def normalise(payload: dict, ctx: SkillContext) -> dict:
|
|
84
|
+
return {"normalised": _normalise_records(payload["records"])}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The decorator auto-registers into the in-process registry at module
|
|
88
|
+
import time + carries `version` through to the federated manifest
|
|
89
|
+
builder (CR.9).
|
|
90
|
+
|
|
91
|
+
## Import-linter contracts
|
|
92
|
+
|
|
93
|
+
Two Forbidden contracts live in `pyproject.toml`:
|
|
94
|
+
|
|
95
|
+
1. `plm_skill_kernel` MUST NOT import `plm_engine_core` — federation
|
|
96
|
+
contract preservation. The two communicate over HTTP, not Python.
|
|
97
|
+
2. `plm_skill_kernel` MUST NOT import `plm_accelerators` (Workbench)
|
|
98
|
+
— mirror of the plm-engine-core constraint.
|
|
99
|
+
|
|
100
|
+
Run:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
lint-imports --config 02_App/plm-skill-kernel/pyproject.toml
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Tests
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
cd 02_App/plm-skill-kernel
|
|
110
|
+
python -m pytest -q
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The V1.1 HTTP round-trip integration test mounts the FastAPI app via
|
|
114
|
+
httpx.AsyncClient(ASGITransport) — no subprocess, no real port
|
|
115
|
+
(Q-W2A-4).
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""TracePulse PLM Skill Kernel — Wave 2 Conv A (FTR-604).
|
|
2
|
+
|
|
3
|
+
Sibling of ``plm-engine-core``. Owns:
|
|
4
|
+
|
|
5
|
+
* The decorator-based Skill SDK (Decision #59 — Q-W2A-1 nested layout).
|
|
6
|
+
* The V1 in-process dispatcher.
|
|
7
|
+
* The Kernel HTTP server stub at ``POST /v1/skills/{id}/invoke``
|
|
8
|
+
matching CR.10 §7.bis verbatim.
|
|
9
|
+
* 3 starter skills harvested per Decision #60 — ``cleansing.normalise``,
|
|
10
|
+
``cleansing.dedupe``, ``bpmn.generate``.
|
|
11
|
+
|
|
12
|
+
The Kernel and the Engine Core are deployed-separable. Their only
|
|
13
|
+
sanctioned interaction is the V1.1 HTTP loopback (env-flagged on
|
|
14
|
+
``SKILL_KERNEL_LOOPBACK``); any Python-level reach-in is forbidden by
|
|
15
|
+
the import-linter contract in ``pyproject.toml``.
|
|
16
|
+
"""
|
|
17
|
+
__version__ = "0.1.0-alpha"
|
|
18
|
+
|
|
19
|
+
# The decorator + sdk surface is the public authoring API. Re-exported
|
|
20
|
+
# here so callers write ``from plm_skill_kernel import skill`` rather
|
|
21
|
+
# than the deeper path. Skill author imports stay short + stable.
|
|
22
|
+
from .sdk.decorator import skill
|
|
23
|
+
from .sdk.context import SkillContext
|
|
24
|
+
from .sdk.types import SkillInvokeRequest, SkillInvokeResult, SkillErrorEnvelope
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"SkillContext",
|
|
28
|
+
"SkillErrorEnvelope",
|
|
29
|
+
"SkillInvokeRequest",
|
|
30
|
+
"SkillInvokeResult",
|
|
31
|
+
"__version__",
|
|
32
|
+
"skill",
|
|
33
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""``python -m plm_skill_kernel`` entry — delegates to :func:`server._cli_entry`.
|
|
2
|
+
|
|
3
|
+
Q-W2A-3 picked the standalone launch shape. The ``__main__`` module is
|
|
4
|
+
the canonical Python idiom; the ``plm-skill-kernel`` console script
|
|
5
|
+
points at the same function via ``pyproject.toml [project.scripts]``.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .server import _cli_entry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
_cli_entry()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""V1 in-process Kernel dispatcher.
|
|
2
|
+
|
|
3
|
+
Resolves ``(skill_id, version)`` against the SDK registry, validates the
|
|
4
|
+
payload + context, and invokes the registered callable. Exceptions are
|
|
5
|
+
caught + converted to a typed :class:`SkillInvokeResult` carrying a
|
|
6
|
+
:class:`SkillErrorEnvelope` — bare exceptions never escape the dispatch
|
|
7
|
+
boundary so the HTTP server can rely on always getting back a
|
|
8
|
+
serialisable result.
|
|
9
|
+
|
|
10
|
+
Decision #51 (Conv M / Q-M2 = (c) "Both"): the dispatcher lives in
|
|
11
|
+
plm-skill-kernel; the plm-engine-core dispatcher (under
|
|
12
|
+
``plm_engine_core.agent_runtime.dispatcher``) becomes a thin transport
|
|
13
|
+
proxy when ``SKILL_KERNEL_LOOPBACK=on`` (Wave 2 Conv A item 8 — the
|
|
14
|
+
V1.1 transport closure for D-CONV-M-4).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any, Dict, Optional
|
|
20
|
+
|
|
21
|
+
from .sdk._registry import RegisteredSkill, _Registry, get_registry
|
|
22
|
+
from .sdk.context import SkillContext
|
|
23
|
+
from .sdk.types import (
|
|
24
|
+
SkillErrorEnvelope,
|
|
25
|
+
SkillInvokeRequest,
|
|
26
|
+
SkillInvokeResult,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ── Error codes (D-AUDIT-7 entries owned by the Skill Kernel) ────────────
|
|
34
|
+
#
|
|
35
|
+
# These mirror string-values declared on
|
|
36
|
+
# ``plm_engine_core.control_plane.capability_registry.errors`` +
|
|
37
|
+
# ``services/llm_errors.py``. Wave 2 Conv A re-uses the existing
|
|
38
|
+
# ``SKILL_NOT_FOUND`` / ``SKILL_VERSION_NOT_FOUND`` /
|
|
39
|
+
# ``KERNEL_DISPATCH_DISABLED`` strings (no new codes — the close
|
|
40
|
+
# audit at Conv N tallied 57 codes; this conv keeps that total).
|
|
41
|
+
SKILL_NOT_FOUND = "SKILL_NOT_FOUND"
|
|
42
|
+
SKILL_VERSION_NOT_FOUND = "SKILL_VERSION_NOT_FOUND"
|
|
43
|
+
KERNEL_DISPATCH_FAILED = "KERNEL_DISPATCH_FAILED"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InProcessDispatcher:
|
|
47
|
+
"""V1 in-process invoker.
|
|
48
|
+
|
|
49
|
+
Constructor accepts an explicit registry so tests can isolate
|
|
50
|
+
registration state. Production callers receive the module-level
|
|
51
|
+
singleton via :func:`get_registry`.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, registry: Optional[_Registry] = None) -> None:
|
|
55
|
+
self._registry = registry if registry is not None else get_registry()
|
|
56
|
+
|
|
57
|
+
async def dispatch(
|
|
58
|
+
self,
|
|
59
|
+
skill_id: str,
|
|
60
|
+
request: SkillInvokeRequest,
|
|
61
|
+
*,
|
|
62
|
+
context: SkillContext,
|
|
63
|
+
) -> SkillInvokeResult:
|
|
64
|
+
"""Resolve + invoke a skill.
|
|
65
|
+
|
|
66
|
+
Returns a :class:`SkillInvokeResult` always — exceptions raised
|
|
67
|
+
by the skill body are caught and surfaced via the
|
|
68
|
+
``KERNEL_DISPATCH_FAILED`` envelope.
|
|
69
|
+
"""
|
|
70
|
+
entry = self._registry.resolve(skill_id, request.version)
|
|
71
|
+
if entry is None:
|
|
72
|
+
return self._not_found(skill_id, request.version)
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
result = await entry.fn(dict(request.payload), context)
|
|
76
|
+
except Exception as exc: # noqa: BLE001 — boundary; we re-package
|
|
77
|
+
logger.exception(
|
|
78
|
+
"skill dispatch failed skill_id=%s version=%s",
|
|
79
|
+
skill_id,
|
|
80
|
+
request.version,
|
|
81
|
+
)
|
|
82
|
+
return SkillInvokeResult(
|
|
83
|
+
ok=False,
|
|
84
|
+
error=SkillErrorEnvelope(
|
|
85
|
+
error_code=KERNEL_DISPATCH_FAILED,
|
|
86
|
+
message=f"skill {skill_id!r} raised {type(exc).__name__}",
|
|
87
|
+
detail={"exception_message": str(exc)},
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# V1 contract: skills return a JSON-serialisable dict.
|
|
92
|
+
if not isinstance(result, dict):
|
|
93
|
+
return SkillInvokeResult(
|
|
94
|
+
ok=False,
|
|
95
|
+
error=SkillErrorEnvelope(
|
|
96
|
+
error_code=KERNEL_DISPATCH_FAILED,
|
|
97
|
+
message=(
|
|
98
|
+
f"skill {skill_id!r} returned a non-dict result; "
|
|
99
|
+
"V1 contract requires `dict[str, Any]`"
|
|
100
|
+
),
|
|
101
|
+
detail={"actual_type": type(result).__name__},
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return SkillInvokeResult(ok=True, result=result)
|
|
106
|
+
|
|
107
|
+
def _not_found(self, skill_id: str, version: str) -> SkillInvokeResult:
|
|
108
|
+
# Differentiate "skill_id has no entries" from
|
|
109
|
+
# "skill_id exists but the requested version is missing" by
|
|
110
|
+
# walking the registry once.
|
|
111
|
+
any_version_for_id = any(
|
|
112
|
+
e.skill_id == skill_id for e in self._registry.list_all()
|
|
113
|
+
)
|
|
114
|
+
if any_version_for_id:
|
|
115
|
+
return SkillInvokeResult(
|
|
116
|
+
ok=False,
|
|
117
|
+
error=SkillErrorEnvelope(
|
|
118
|
+
error_code=SKILL_VERSION_NOT_FOUND,
|
|
119
|
+
message=(
|
|
120
|
+
f"skill {skill_id!r} version {version!r} is not "
|
|
121
|
+
"registered"
|
|
122
|
+
),
|
|
123
|
+
detail={"skill_id": skill_id, "version": version},
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
return SkillInvokeResult(
|
|
127
|
+
ok=False,
|
|
128
|
+
error=SkillErrorEnvelope(
|
|
129
|
+
error_code=SKILL_NOT_FOUND,
|
|
130
|
+
message=f"no skill registered for id={skill_id!r}",
|
|
131
|
+
detail={"skill_id": skill_id, "version": version},
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = [
|
|
137
|
+
"InProcessDispatcher",
|
|
138
|
+
"KERNEL_DISPATCH_FAILED",
|
|
139
|
+
"SKILL_NOT_FOUND",
|
|
140
|
+
"SKILL_VERSION_NOT_FOUND",
|
|
141
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Boot-time skill discovery + registration.
|
|
2
|
+
|
|
3
|
+
Importing :func:`load_starter_skills` triggers the import of each
|
|
4
|
+
starter-skill module. Each module's top-level ``@skill(...)``
|
|
5
|
+
decorator runs as a side-effect of import, so by the time
|
|
6
|
+
:func:`load_starter_skills` returns the SDK registry contains every
|
|
7
|
+
starter skill.
|
|
8
|
+
|
|
9
|
+
This module is the V1 wiring point — production ``server.py`` calls
|
|
10
|
+
:func:`load_starter_skills` at startup. Tests that need a clean
|
|
11
|
+
registry call ``get_registry().clear()`` first.
|
|
12
|
+
|
|
13
|
+
Wave 3 Conv A (per Decision #60) added ``documents.parse`` +
|
|
14
|
+
``agents.suggest`` to the fan-out — bringing the starter set to 5
|
|
15
|
+
skills (3 Wave 2 + 2 Wave 3).
|
|
16
|
+
|
|
17
|
+
Wave 5 Conv C (FTR-603 / Decision #122 + #124) adds the 5 active V1
|
|
18
|
+
``knowledge.*`` primitives — bringing the boot-loaded set to 10
|
|
19
|
+
skills (3 Wave 2 + 2 Wave 3 + 5 Wave 5). The 2 deferred primitives
|
|
20
|
+
(``explain`` / ``context_pack``) stay YAML ``draft`` capabilities
|
|
21
|
+
only and do NOT register a kernel skill in V1; their dispatch is
|
|
22
|
+
short-circuited by the engine-core envelope returning
|
|
23
|
+
``KS_PRIMITIVE_DEFERRED_V1_1``.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import List, Tuple
|
|
29
|
+
|
|
30
|
+
from .sdk._registry import RegisteredSkill, get_registry
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_starter_skills() -> List[RegisteredSkill]:
|
|
37
|
+
"""Import the 5 starter skills + return the resulting list.
|
|
38
|
+
|
|
39
|
+
The imports themselves run the decorator side-effects; this
|
|
40
|
+
function returns the registry's view post-load so callers can
|
|
41
|
+
log or assert on the exact skills available.
|
|
42
|
+
|
|
43
|
+
Idempotent — re-importing a module is a no-op (Python caches),
|
|
44
|
+
and the SDK registry tolerates same-callable re-registration.
|
|
45
|
+
"""
|
|
46
|
+
# Local imports — module-load side effect is the registration. The
|
|
47
|
+
# imports MUST be inside the function so test fixtures that clear
|
|
48
|
+
# the registry can re-trigger registration by re-calling.
|
|
49
|
+
# NOTE: `noqa: F401` because the bound names are unused; the
|
|
50
|
+
# imports' side effect (decorator registration) is the point.
|
|
51
|
+
from .skills.cleansing import dedupe as _dedupe # noqa: F401
|
|
52
|
+
from .skills.cleansing import normalise as _normalise # noqa: F401
|
|
53
|
+
from .skills.bpmn import generate as _generate # noqa: F401
|
|
54
|
+
from .skills.documents import parse as _parse # noqa: F401
|
|
55
|
+
from .skills.agents import suggest as _suggest # noqa: F401
|
|
56
|
+
# Wave 5 Conv C — FTR-603 Knowledge Services V1 active primitives.
|
|
57
|
+
from .skills.knowledge import search as _ks_search # noqa: F401
|
|
58
|
+
from .skills.knowledge import get_record as _ks_get_record # noqa: F401
|
|
59
|
+
from .skills.knowledge import retrieve as _ks_retrieve # noqa: F401
|
|
60
|
+
from .skills.knowledge import resolve as _ks_resolve # noqa: F401
|
|
61
|
+
from .skills.knowledge import validate as _ks_validate # noqa: F401
|
|
62
|
+
|
|
63
|
+
entries = get_registry().list_all()
|
|
64
|
+
logger.info(
|
|
65
|
+
"skill_kernel boot_loaded count=%s ids=%s",
|
|
66
|
+
len(entries),
|
|
67
|
+
",".join(sorted(f"{e.skill_id}@{e.version}" for e in entries)),
|
|
68
|
+
)
|
|
69
|
+
return entries
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def list_registered_skill_ids() -> List[Tuple[str, str]]:
|
|
73
|
+
"""Return ``(skill_id, version)`` pairs for every registered entry.
|
|
74
|
+
|
|
75
|
+
Used by the federation manifest builder + introspection endpoints.
|
|
76
|
+
"""
|
|
77
|
+
return [(e.skill_id, e.version) for e in get_registry().list_all()]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["list_registered_skill_ids", "load_starter_skills"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Public Skill SDK surface (Wave 2 Conv A / Decision #59).
|
|
2
|
+
|
|
3
|
+
Skill authors import from here:
|
|
4
|
+
|
|
5
|
+
from plm_skill_kernel import skill, SkillContext
|
|
6
|
+
# or, equivalently:
|
|
7
|
+
from plm_skill_kernel.sdk import skill, SkillContext
|
|
8
|
+
|
|
9
|
+
The decorator + context + wire types are the V1-stable authoring
|
|
10
|
+
surface; internal helpers (the registry implementation, validation
|
|
11
|
+
helpers) live under ``plm_skill_kernel.sdk._registry`` and are NOT
|
|
12
|
+
re-exported.
|
|
13
|
+
"""
|
|
14
|
+
from .context import SkillContext
|
|
15
|
+
from .decorator import SkillDecoratorError, skill
|
|
16
|
+
from .types import (
|
|
17
|
+
SkillErrorEnvelope,
|
|
18
|
+
SkillInvokeRequest,
|
|
19
|
+
SkillInvokeResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"SkillContext",
|
|
24
|
+
"SkillDecoratorError",
|
|
25
|
+
"SkillErrorEnvelope",
|
|
26
|
+
"SkillInvokeRequest",
|
|
27
|
+
"SkillInvokeResult",
|
|
28
|
+
"skill",
|
|
29
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Internal in-process decorator registry.
|
|
2
|
+
|
|
3
|
+
The :func:`plm_skill_kernel.sdk.skill` decorator stamps each
|
|
4
|
+
``async def`` it wraps into this module-level registry at import
|
|
5
|
+
time. The Kernel dispatcher then resolves ``(skill_id, version)``
|
|
6
|
+
back to the callable at dispatch time.
|
|
7
|
+
|
|
8
|
+
Hot-reload (V1.1) replaces a single entry atomically; V1 callers that
|
|
9
|
+
re-import a skill module will get a duplicate-registration error so
|
|
10
|
+
test suites can detect accidental re-registration.
|
|
11
|
+
|
|
12
|
+
Decision #25 / #41 / #52 / #53 V1 in-memory + Protocol pattern:
|
|
13
|
+
the registry is a module-level dict + an ``RLock``. Wave 4+ may swap
|
|
14
|
+
the backing store for a process-shared cache without changing the
|
|
15
|
+
public surface (which is just ``register`` + ``resolve`` + ``list_all``).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import threading
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Awaitable, Callable, Dict, List, Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from .context import SkillContext
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Type alias for a registered skill body — every skill is
|
|
27
|
+
# ``async def fn(payload: dict, ctx: SkillContext) -> dict``.
|
|
28
|
+
SkillCallable = Callable[
|
|
29
|
+
[Dict, SkillContext], Awaitable[Dict]
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class RegisteredSkill:
|
|
35
|
+
"""One entry in the decorator registry.
|
|
36
|
+
|
|
37
|
+
The ``min_autonomy`` + ``streaming`` + ``hitl_required`` fields
|
|
38
|
+
are advisory metadata the SDK can pass through to the federation
|
|
39
|
+
manifest builder (CR.9) at boot. They are NOT enforced at dispatch
|
|
40
|
+
time — Core's selector + gating already ran upstream.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
skill_id: str
|
|
44
|
+
version: str
|
|
45
|
+
fn: SkillCallable
|
|
46
|
+
description: Optional[str]
|
|
47
|
+
min_autonomy: str # "L1" / "L2" / "L3"
|
|
48
|
+
streaming: bool
|
|
49
|
+
hitl_required: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _Registry:
|
|
53
|
+
"""Thread-safe registry of decorated skills.
|
|
54
|
+
|
|
55
|
+
Keyed by ``(skill_id, version)``. Adding a duplicate raises a
|
|
56
|
+
:class:`RuntimeError` so test fixtures catch double-registration
|
|
57
|
+
rather than silently shadowing.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self._lock = threading.RLock()
|
|
62
|
+
self._entries: Dict[Tuple[str, str], RegisteredSkill] = {}
|
|
63
|
+
|
|
64
|
+
def register(self, entry: RegisteredSkill) -> None:
|
|
65
|
+
key = (entry.skill_id, entry.version)
|
|
66
|
+
with self._lock:
|
|
67
|
+
existing = self._entries.get(key)
|
|
68
|
+
if existing is not None and existing.fn is not entry.fn:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
f"skill {entry.skill_id!r} version {entry.version!r} "
|
|
71
|
+
"is already registered with a different callable; "
|
|
72
|
+
"duplicate registration is forbidden in V1"
|
|
73
|
+
)
|
|
74
|
+
self._entries[key] = entry
|
|
75
|
+
|
|
76
|
+
def resolve(
|
|
77
|
+
self, skill_id: str, version: str
|
|
78
|
+
) -> Optional[RegisteredSkill]:
|
|
79
|
+
with self._lock:
|
|
80
|
+
return self._entries.get((skill_id, version))
|
|
81
|
+
|
|
82
|
+
def list_all(self) -> List[RegisteredSkill]:
|
|
83
|
+
with self._lock:
|
|
84
|
+
return list(self._entries.values())
|
|
85
|
+
|
|
86
|
+
def clear(self) -> None:
|
|
87
|
+
"""Test-only — unregister everything. Production callers MUST NOT."""
|
|
88
|
+
with self._lock:
|
|
89
|
+
self._entries.clear()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Module-level singleton. Imported by the decorator + the dispatcher.
|
|
93
|
+
_REGISTRY = _Registry()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_registry() -> _Registry:
|
|
97
|
+
"""Return the module-level registry singleton.
|
|
98
|
+
|
|
99
|
+
Tests that need isolation can call ``get_registry().clear()`` in a
|
|
100
|
+
fixture's setup; production callers never construct a fresh registry
|
|
101
|
+
(skills register at import time into the singleton).
|
|
102
|
+
"""
|
|
103
|
+
return _REGISTRY
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = [
|
|
107
|
+
"RegisteredSkill",
|
|
108
|
+
"SkillCallable",
|
|
109
|
+
"_Registry",
|
|
110
|
+
"get_registry",
|
|
111
|
+
]
|