plm-skill-kernel 1.0.0__py3-none-any.whl
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/__init__.py +33 -0
- plm_skill_kernel/__main__.py +13 -0
- plm_skill_kernel/dispatcher.py +141 -0
- plm_skill_kernel/registry.py +80 -0
- plm_skill_kernel/sdk/__init__.py +29 -0
- plm_skill_kernel/sdk/_registry.py +111 -0
- plm_skill_kernel/sdk/context.py +66 -0
- plm_skill_kernel/sdk/decorator.py +147 -0
- plm_skill_kernel/sdk/types.py +86 -0
- plm_skill_kernel/server.py +173 -0
- plm_skill_kernel/skills/__init__.py +16 -0
- plm_skill_kernel/skills/agents/__init__.py +13 -0
- plm_skill_kernel/skills/agents/suggest.py +191 -0
- plm_skill_kernel/skills/bpmn/__init__.py +9 -0
- plm_skill_kernel/skills/bpmn/generate.py +147 -0
- plm_skill_kernel/skills/cleansing/__init__.py +10 -0
- plm_skill_kernel/skills/cleansing/dedupe.py +162 -0
- plm_skill_kernel/skills/cleansing/normalise.py +178 -0
- plm_skill_kernel/skills/documents/__init__.py +12 -0
- plm_skill_kernel/skills/documents/parse.py +202 -0
- plm_skill_kernel/skills/knowledge/__init__.py +32 -0
- plm_skill_kernel/skills/knowledge/_v1_corpus.py +153 -0
- plm_skill_kernel/skills/knowledge/get_record.py +102 -0
- plm_skill_kernel/skills/knowledge/resolve.py +143 -0
- plm_skill_kernel/skills/knowledge/retrieve.py +143 -0
- plm_skill_kernel/skills/knowledge/search.py +161 -0
- plm_skill_kernel/skills/knowledge/validate.py +222 -0
- plm_skill_kernel/skills_legacy/__init__.py +0 -0
- plm_skill_kernel/skills_legacy/knowledge_pack_loader.py +17 -0
- plm_skill_kernel/skills_legacy/knowledge_search_engine.py +25 -0
- plm_skill_kernel/skills_legacy/plm_skill_registry.py +573 -0
- plm_skill_kernel/skills_legacy/plm_tool_definitions.py +44 -0
- plm_skill_kernel/skills_legacy/skill_orchestrator.py +163 -0
- plm_skill_kernel-1.0.0.dist-info/METADATA +135 -0
- plm_skill_kernel-1.0.0.dist-info/RECORD +38 -0
- plm_skill_kernel-1.0.0.dist-info/WHEEL +5 -0
- plm_skill_kernel-1.0.0.dist-info/entry_points.txt +2 -0
- plm_skill_kernel-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""V1 in-memory deterministic seed corpus for the knowledge.* skills.
|
|
2
|
+
|
|
3
|
+
FTR-603 Wave 5 Conv C / Decision #122 — V1 thin-proxy harvest. The 5
|
|
4
|
+
active primitives operate on a deterministic in-process seed corpus
|
|
5
|
+
so the kernel container is self-contained at boot (no Azure / file-
|
|
6
|
+
system dependency). V1.1 replaces this seed with a real backend bind
|
|
7
|
+
through the lazy-import boundary documented on each skill module.
|
|
8
|
+
|
|
9
|
+
The seed shape is the minimum that keeps the 5 primitives' V1
|
|
10
|
+
contract demonstrable:
|
|
11
|
+
|
|
12
|
+
* Two corpora — ``demo`` (3 BPMN-flavoured records) + ``foundation``
|
|
13
|
+
(2 entity records) so cross-corpus + cross-record cases are
|
|
14
|
+
exercisable from tests.
|
|
15
|
+
* One entity catalog — keyed by ``(entity_kind, surface_form)`` for
|
|
16
|
+
``resolve`` exact-match.
|
|
17
|
+
* One rule set — ``v1_smoke`` for ``validate`` keyed by required
|
|
18
|
+
fields.
|
|
19
|
+
|
|
20
|
+
Adding fixtures here is a deliberate, additive YAML-style edit; the
|
|
21
|
+
runtime contract on the request / response Pydantic schemas is the
|
|
22
|
+
frozen V1 contract — this seed is wallpaper.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any, Dict, List, Tuple
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Records (search / get_record / retrieve) ──────────────────────────
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
CORPUS_RECORDS: Dict[str, Dict[str, Dict[str, Any]]] = {
|
|
33
|
+
"demo": {
|
|
34
|
+
"rec-001": {
|
|
35
|
+
"title": "Approval flow",
|
|
36
|
+
"body": "Route every change order through the design-review gate.",
|
|
37
|
+
"source_uri": "doc://demo/rec-001",
|
|
38
|
+
"revision": "rev-1",
|
|
39
|
+
"payload": {"tags": ["bpmn", "approval"]},
|
|
40
|
+
},
|
|
41
|
+
"rec-002": {
|
|
42
|
+
"title": "Manufacturing release",
|
|
43
|
+
"body": "Promote a part to released after BOM consolidation.",
|
|
44
|
+
"source_uri": "doc://demo/rec-002",
|
|
45
|
+
"revision": "rev-1",
|
|
46
|
+
"payload": {"tags": ["bpmn", "release"]},
|
|
47
|
+
},
|
|
48
|
+
"rec-003": {
|
|
49
|
+
"title": "ECO impact analysis",
|
|
50
|
+
"body": "Evaluate the impact of a change on dependent assemblies.",
|
|
51
|
+
"source_uri": "doc://demo/rec-003",
|
|
52
|
+
"revision": "rev-2",
|
|
53
|
+
"payload": {"tags": ["change", "impact"]},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"foundation": {
|
|
57
|
+
"ent-part-001": {
|
|
58
|
+
"title": "Part WASHER-M6",
|
|
59
|
+
"body": "Foundation entity for the WASHER-M6 standard hardware part.",
|
|
60
|
+
"source_uri": "foundation://parts/WASHER-M6",
|
|
61
|
+
"revision": "rev-1",
|
|
62
|
+
"payload": {"kind": "Part", "category": "hardware"},
|
|
63
|
+
},
|
|
64
|
+
"ent-part-002": {
|
|
65
|
+
"title": "Part BRACKET-A1",
|
|
66
|
+
"body": "Foundation entity for the BRACKET-A1 fabricated bracket.",
|
|
67
|
+
"source_uri": "foundation://parts/BRACKET-A1",
|
|
68
|
+
"revision": "rev-1",
|
|
69
|
+
"payload": {"kind": "Part", "category": "fabricated"},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
"""Two-corpus seed: ``demo`` (3 records) + ``foundation`` (2 records)."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── Entity resolution catalog (resolve) ───────────────────────────────
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
ENTITY_CATALOG: Dict[Tuple[str, str], Dict[str, Any]] = {
|
|
80
|
+
("part", "washer-m6"): {
|
|
81
|
+
"canonical_id": "ent-part-001",
|
|
82
|
+
"label": "WASHER-M6",
|
|
83
|
+
"confidence": 0.95,
|
|
84
|
+
},
|
|
85
|
+
("part", "bracket-a1"): {
|
|
86
|
+
"canonical_id": "ent-part-002",
|
|
87
|
+
"label": "BRACKET-A1",
|
|
88
|
+
"confidence": 0.92,
|
|
89
|
+
},
|
|
90
|
+
("part", "bracket"): {
|
|
91
|
+
"canonical_id": "ent-part-002",
|
|
92
|
+
"label": "BRACKET-A1",
|
|
93
|
+
"confidence": 0.55,
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
"""V1 entity catalog keyed by ``(entity_kind, surface_form_lower)``."""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── Rule sets (validate) ──────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
RULE_SETS: Dict[str, List[Dict[str, Any]]] = {
|
|
103
|
+
"v1_smoke": [
|
|
104
|
+
{
|
|
105
|
+
"rule_id": "v1.required.title",
|
|
106
|
+
"severity": "error",
|
|
107
|
+
"message": "Field 'title' is required.",
|
|
108
|
+
"kind": "required",
|
|
109
|
+
"field": "title",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"rule_id": "v1.required.body",
|
|
113
|
+
"severity": "warning",
|
|
114
|
+
"message": "Field 'body' is recommended.",
|
|
115
|
+
"kind": "required",
|
|
116
|
+
"field": "body",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"rule_id": "v1.evidence_refs.curation",
|
|
120
|
+
"severity": "error",
|
|
121
|
+
"message": (
|
|
122
|
+
"Each evidence_refs entry must be backed by a "
|
|
123
|
+
"connector_access_index row (curation index)."
|
|
124
|
+
),
|
|
125
|
+
"kind": "curation",
|
|
126
|
+
"field": "evidence_refs",
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
"""V1 ``v1_smoke`` rule set with 3 rules covering required-fields +
|
|
131
|
+
the curation-index evidence-ref check (per readiness sweep §3.2)."""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def known_corpus(corpus_id: str) -> bool:
|
|
135
|
+
return corpus_id in CORPUS_RECORDS
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def list_records(corpus_id: str) -> List[Tuple[str, Dict[str, Any]]]:
|
|
139
|
+
"""Return ``[(record_id, record_payload), ...]`` for the corpus.
|
|
140
|
+
|
|
141
|
+
Empty list when the corpus is unknown — V1 contract is open-world
|
|
142
|
+
on corpus_id (no error; consumer treats empty as miss).
|
|
143
|
+
"""
|
|
144
|
+
return list(CORPUS_RECORDS.get(corpus_id, {}).items())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = [
|
|
148
|
+
"CORPUS_RECORDS",
|
|
149
|
+
"ENTITY_CATALOG",
|
|
150
|
+
"RULE_SETS",
|
|
151
|
+
"known_corpus",
|
|
152
|
+
"list_records",
|
|
153
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""``knowledge.get_record`` / 1.0.0 / L1 — fetch a record by id.
|
|
2
|
+
|
|
3
|
+
FTR-603 Wave 5 Conv C / Decision #122. V1 thin-proxy harvest of the
|
|
4
|
+
D-LOCKED-23 active V1 ``get_record`` primitive (PRD §8.11.3).
|
|
5
|
+
|
|
6
|
+
V1 contract:
|
|
7
|
+
|
|
8
|
+
payload = {
|
|
9
|
+
"corpus_id": str, # required
|
|
10
|
+
"record_id": str, # required
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Output:
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
"payload": {"record": <KnowledgeRecord as dict | None>},
|
|
17
|
+
"source_refs": [<KnowledgeSourceRef as dict>] | [],
|
|
18
|
+
"evidence_refs": [],
|
|
19
|
+
"connector_access": [],
|
|
20
|
+
"baseline_version": "v1-corpus@rev-1",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
V1 implementation: dictionary lookup against the in-process seed corpus.
|
|
24
|
+
Missing record → ``payload.record = None`` (consumer maps to 404-style
|
|
25
|
+
outcome). V1.1 backs onto a real corpus connector through the same
|
|
26
|
+
public contract.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any, Dict
|
|
31
|
+
|
|
32
|
+
from pydantic import ValidationError
|
|
33
|
+
|
|
34
|
+
from plm_shared.knowledge.primitives_v1 import (
|
|
35
|
+
KnowledgeGetRecordRequest,
|
|
36
|
+
KnowledgeGetRecordResponse,
|
|
37
|
+
KnowledgeRecord,
|
|
38
|
+
)
|
|
39
|
+
from plm_shared.knowledge_envelope import KnowledgeSourceRef
|
|
40
|
+
from plm_skill_kernel import SkillContext, skill
|
|
41
|
+
|
|
42
|
+
from ._v1_corpus import CORPUS_RECORDS
|
|
43
|
+
from .search import V1_BASELINE_VERSION
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@skill(
|
|
47
|
+
id="knowledge.get_record",
|
|
48
|
+
version="1.0.0",
|
|
49
|
+
description=(
|
|
50
|
+
"Fetch a Knowledge record by id from a corpus. L1 read-only "
|
|
51
|
+
"surface; V1 thin-proxy against the in-process seed corpus."
|
|
52
|
+
),
|
|
53
|
+
min_autonomy="L1",
|
|
54
|
+
streaming=False,
|
|
55
|
+
hitl_required=False,
|
|
56
|
+
)
|
|
57
|
+
async def knowledge_get_record(
|
|
58
|
+
payload: Dict[str, Any], ctx: SkillContext
|
|
59
|
+
) -> Dict[str, Any]:
|
|
60
|
+
"""Validate the request + emit the record (or None) + envelope artefacts."""
|
|
61
|
+
try:
|
|
62
|
+
request = KnowledgeGetRecordRequest.model_validate(payload)
|
|
63
|
+
except ValidationError as exc:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"knowledge.get_record: invalid request payload — {exc.errors()}"
|
|
66
|
+
) from exc
|
|
67
|
+
|
|
68
|
+
raw = CORPUS_RECORDS.get(request.corpus_id, {}).get(request.record_id)
|
|
69
|
+
|
|
70
|
+
if raw is None:
|
|
71
|
+
response = KnowledgeGetRecordResponse(record=None)
|
|
72
|
+
source_refs: list[dict] = []
|
|
73
|
+
else:
|
|
74
|
+
record = KnowledgeRecord(
|
|
75
|
+
record_id=request.record_id,
|
|
76
|
+
source_uri=raw["source_uri"],
|
|
77
|
+
corpus_id=request.corpus_id,
|
|
78
|
+
revision=raw["revision"],
|
|
79
|
+
title=raw.get("title"),
|
|
80
|
+
body=raw.get("body"),
|
|
81
|
+
payload=dict(raw.get("payload", {})),
|
|
82
|
+
)
|
|
83
|
+
response = KnowledgeGetRecordResponse(record=record)
|
|
84
|
+
source_refs = [
|
|
85
|
+
KnowledgeSourceRef(
|
|
86
|
+
ref_id=request.record_id,
|
|
87
|
+
ref_type=f"corpus:{request.corpus_id}",
|
|
88
|
+
title=record.title,
|
|
89
|
+
locator=record.source_uri,
|
|
90
|
+
).model_dump()
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"payload": response.model_dump(),
|
|
95
|
+
"source_refs": source_refs,
|
|
96
|
+
"evidence_refs": [],
|
|
97
|
+
"connector_access": [],
|
|
98
|
+
"baseline_version": V1_BASELINE_VERSION,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
__all__ = ["knowledge_get_record"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""``knowledge.resolve`` / 1.0.0 / L1 — entity resolution.
|
|
2
|
+
|
|
3
|
+
FTR-603 Wave 5 Conv C / Decision #122. V1 thin-proxy harvest of the
|
|
4
|
+
D-LOCKED-23 active V1 ``resolve`` primitive (PRD §8.11.3).
|
|
5
|
+
|
|
6
|
+
V1 contract:
|
|
7
|
+
|
|
8
|
+
payload = {
|
|
9
|
+
"entity_kind": str, # required
|
|
10
|
+
"surface_form": str, # required
|
|
11
|
+
"corpus_id": str, # required
|
|
12
|
+
"context": dict, # default {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Output:
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
"payload": {
|
|
19
|
+
"candidates": [<KnowledgeResolution as dict>, ...],
|
|
20
|
+
"best": <KnowledgeResolution as dict | None>,
|
|
21
|
+
},
|
|
22
|
+
"source_refs": [<KnowledgeSourceRef as dict>, ...],
|
|
23
|
+
"evidence_refs": [],
|
|
24
|
+
"connector_access": [],
|
|
25
|
+
"baseline_version": "v1-catalog@rev-1",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
V1 implementation: exact + normalised-substring match against the
|
|
29
|
+
in-process entity catalog
|
|
30
|
+
(:data:`plm_skill_kernel.skills.knowledge._v1_corpus.ENTITY_CATALOG`).
|
|
31
|
+
The catalog is keyed by ``(entity_kind, surface_form_lower)``;
|
|
32
|
+
returns 0..N candidates ordered by descending confidence.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from typing import Any, Dict, List
|
|
37
|
+
|
|
38
|
+
from pydantic import ValidationError
|
|
39
|
+
|
|
40
|
+
from plm_shared.knowledge.primitives_v1 import (
|
|
41
|
+
KnowledgeResolution,
|
|
42
|
+
KnowledgeResolveRequest,
|
|
43
|
+
KnowledgeResolveResponse,
|
|
44
|
+
)
|
|
45
|
+
from plm_shared.knowledge_envelope import KnowledgeSourceRef
|
|
46
|
+
from plm_skill_kernel import SkillContext, skill
|
|
47
|
+
|
|
48
|
+
from ._v1_corpus import ENTITY_CATALOG
|
|
49
|
+
|
|
50
|
+
V1_BASELINE_VERSION = "v1-catalog@rev-1"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _candidates(entity_kind: str, surface_form: str) -> List[KnowledgeResolution]:
|
|
54
|
+
"""V1 deterministic resolution.
|
|
55
|
+
|
|
56
|
+
Returns 0..N candidates ordered by descending confidence. Match
|
|
57
|
+
rules (V1):
|
|
58
|
+
|
|
59
|
+
* Exact lookup ``(entity_kind, surface_form_lower)`` — full confidence.
|
|
60
|
+
* Substring fallback — every entry whose surface_form is a
|
|
61
|
+
substring of the requested surface_form (or vice versa) is
|
|
62
|
+
emitted with a 0.4 confidence floor.
|
|
63
|
+
|
|
64
|
+
The catalog is small (≤4 entries V1) so the linear scan is fine;
|
|
65
|
+
V1.1 swaps in a real resolver behind the same skill contract.
|
|
66
|
+
"""
|
|
67
|
+
needle = (entity_kind, surface_form.strip().lower())
|
|
68
|
+
out: List[KnowledgeResolution] = []
|
|
69
|
+
seen: set[str] = set()
|
|
70
|
+
|
|
71
|
+
direct = ENTITY_CATALOG.get(needle)
|
|
72
|
+
if direct is not None:
|
|
73
|
+
out.append(KnowledgeResolution(**direct))
|
|
74
|
+
seen.add(direct["canonical_id"])
|
|
75
|
+
|
|
76
|
+
needle_lower = needle[1]
|
|
77
|
+
for (kind, form), candidate in ENTITY_CATALOG.items():
|
|
78
|
+
if kind != entity_kind:
|
|
79
|
+
continue
|
|
80
|
+
if candidate["canonical_id"] in seen:
|
|
81
|
+
continue
|
|
82
|
+
if form in needle_lower or needle_lower in form:
|
|
83
|
+
out.append(
|
|
84
|
+
KnowledgeResolution(
|
|
85
|
+
canonical_id=candidate["canonical_id"],
|
|
86
|
+
label=candidate.get("label"),
|
|
87
|
+
confidence=min(0.4, candidate["confidence"]),
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
seen.add(candidate["canonical_id"])
|
|
91
|
+
|
|
92
|
+
out.sort(key=lambda r: r.confidence, reverse=True)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@skill(
|
|
97
|
+
id="knowledge.resolve",
|
|
98
|
+
version="1.0.0",
|
|
99
|
+
description=(
|
|
100
|
+
"Entity resolution to a canonical id (Foundation). L1 "
|
|
101
|
+
"read-only surface; V1 thin-proxy against the in-process "
|
|
102
|
+
"entity catalog."
|
|
103
|
+
),
|
|
104
|
+
min_autonomy="L1",
|
|
105
|
+
streaming=False,
|
|
106
|
+
hitl_required=False,
|
|
107
|
+
)
|
|
108
|
+
async def knowledge_resolve(
|
|
109
|
+
payload: Dict[str, Any], ctx: SkillContext
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
"""Validate the request + emit candidates + envelope artefacts."""
|
|
112
|
+
try:
|
|
113
|
+
request = KnowledgeResolveRequest.model_validate(payload)
|
|
114
|
+
except ValidationError as exc:
|
|
115
|
+
raise ValueError(
|
|
116
|
+
f"knowledge.resolve: invalid request payload — {exc.errors()}"
|
|
117
|
+
) from exc
|
|
118
|
+
|
|
119
|
+
candidates = _candidates(request.entity_kind, request.surface_form)
|
|
120
|
+
best = candidates[0] if candidates else None
|
|
121
|
+
|
|
122
|
+
response = KnowledgeResolveResponse(candidates=candidates, best=best)
|
|
123
|
+
|
|
124
|
+
source_refs = [
|
|
125
|
+
KnowledgeSourceRef(
|
|
126
|
+
ref_id=c.canonical_id,
|
|
127
|
+
ref_type=f"entity:{request.entity_kind}",
|
|
128
|
+
title=c.label,
|
|
129
|
+
locator=f"foundation://entities/{c.canonical_id}",
|
|
130
|
+
).model_dump()
|
|
131
|
+
for c in candidates
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"payload": response.model_dump(),
|
|
136
|
+
"source_refs": source_refs,
|
|
137
|
+
"evidence_refs": [],
|
|
138
|
+
"connector_access": [],
|
|
139
|
+
"baseline_version": V1_BASELINE_VERSION,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = ["V1_BASELINE_VERSION", "knowledge_resolve"]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""``knowledge.retrieve`` / 1.0.0 / L1 — top-K ranked retrieval.
|
|
2
|
+
|
|
3
|
+
FTR-603 Wave 5 Conv C / Decision #122. V1 thin-proxy harvest of the
|
|
4
|
+
D-LOCKED-23 active V1 ``retrieve`` primitive (PRD §8.11.3).
|
|
5
|
+
|
|
6
|
+
V1 contract:
|
|
7
|
+
|
|
8
|
+
payload = {
|
|
9
|
+
"corpus_id": str, # required
|
|
10
|
+
"query": str, # required
|
|
11
|
+
"baseline_version": str, # required (obligation 2 binding)
|
|
12
|
+
"top_k": int, # default 10, 1..100
|
|
13
|
+
"filters": dict, # default {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Output:
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
"payload": {"hits": [<KnowledgeHit as dict>, ...]},
|
|
20
|
+
"source_refs": [<KnowledgeSourceRef as dict>, ...],
|
|
21
|
+
"evidence_refs": [<KnowledgeSourceRef as dict>, ...],
|
|
22
|
+
"connector_access": [],
|
|
23
|
+
"baseline_version": <echo of request.baseline_version>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
The retrieve primitive's distinguishing feature vs ``search`` is the
|
|
27
|
+
mandatory ``baseline_version`` binding (PRD §8.11.3 obligation 2).
|
|
28
|
+
The skill echoes the caller-supplied baseline back into the response
|
|
29
|
+
artefacts; the engine-core envelope stamps it onto the wire envelope.
|
|
30
|
+
|
|
31
|
+
V1 implementation: substring + position score (same scorer as
|
|
32
|
+
``search``) over a baseline-filtered slice. Records whose ``revision``
|
|
33
|
+
differs from the requested ``baseline_version`` (when the version is
|
|
34
|
+
prefixed ``rev-``) are excluded; ``v1-corpus@*`` baselines accept
|
|
35
|
+
every record.
|
|
36
|
+
|
|
37
|
+
The retrieve primitive emits ``evidence_refs`` (obligation 4) — every
|
|
38
|
+
hit also surfaces as an evidence pointer so consumers can audit the
|
|
39
|
+
provenance chain without a follow-up call.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
from typing import Any, Dict, List
|
|
44
|
+
|
|
45
|
+
from pydantic import ValidationError
|
|
46
|
+
|
|
47
|
+
from plm_shared.knowledge.primitives_v1 import (
|
|
48
|
+
KnowledgeHit,
|
|
49
|
+
KnowledgeRecord,
|
|
50
|
+
KnowledgeRetrieveRequest,
|
|
51
|
+
KnowledgeRetrieveResponse,
|
|
52
|
+
)
|
|
53
|
+
from plm_shared.knowledge_envelope import KnowledgeSourceRef
|
|
54
|
+
from plm_skill_kernel import SkillContext, skill
|
|
55
|
+
|
|
56
|
+
from ._v1_corpus import CORPUS_RECORDS
|
|
57
|
+
from .search import _score_substring
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _accepts_baseline(record_revision: str, baseline_version: str) -> bool:
|
|
61
|
+
"""V1 baseline filter — accept all when baseline starts with ``v1-``."""
|
|
62
|
+
if baseline_version.startswith("v1-"):
|
|
63
|
+
return True
|
|
64
|
+
if baseline_version.startswith("rev-"):
|
|
65
|
+
return record_revision == baseline_version
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@skill(
|
|
70
|
+
id="knowledge.retrieve",
|
|
71
|
+
version="1.0.0",
|
|
72
|
+
description=(
|
|
73
|
+
"Top-K ranked retrieval over a baseline-versioned slice. L1 "
|
|
74
|
+
"read-only surface; V1 thin-proxy with mandatory "
|
|
75
|
+
"baseline_version binding (PRD §8.11.3 obligation 2)."
|
|
76
|
+
),
|
|
77
|
+
min_autonomy="L1",
|
|
78
|
+
streaming=False,
|
|
79
|
+
hitl_required=False,
|
|
80
|
+
)
|
|
81
|
+
async def knowledge_retrieve(
|
|
82
|
+
payload: Dict[str, Any], ctx: SkillContext
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""Validate the request + emit ranked hits + envelope artefacts."""
|
|
85
|
+
try:
|
|
86
|
+
request = KnowledgeRetrieveRequest.model_validate(payload)
|
|
87
|
+
except ValidationError as exc:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"knowledge.retrieve: invalid request payload — {exc.errors()}"
|
|
90
|
+
) from exc
|
|
91
|
+
|
|
92
|
+
records = CORPUS_RECORDS.get(request.corpus_id, {})
|
|
93
|
+
scored: List[KnowledgeHit] = []
|
|
94
|
+
for rec_id, raw in records.items():
|
|
95
|
+
if not _accepts_baseline(raw["revision"], request.baseline_version):
|
|
96
|
+
continue
|
|
97
|
+
text_blob = " ".join(
|
|
98
|
+
v
|
|
99
|
+
for v in (raw.get("title"), raw.get("body"))
|
|
100
|
+
if isinstance(v, str)
|
|
101
|
+
)
|
|
102
|
+
score = _score_substring(text_blob, request.query)
|
|
103
|
+
if score <= 0.0:
|
|
104
|
+
continue
|
|
105
|
+
scored.append(
|
|
106
|
+
KnowledgeHit(
|
|
107
|
+
score=score,
|
|
108
|
+
record=KnowledgeRecord(
|
|
109
|
+
record_id=rec_id,
|
|
110
|
+
source_uri=raw["source_uri"],
|
|
111
|
+
corpus_id=request.corpus_id,
|
|
112
|
+
revision=raw["revision"],
|
|
113
|
+
title=raw.get("title"),
|
|
114
|
+
body=raw.get("body"),
|
|
115
|
+
payload=dict(raw.get("payload", {})),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
scored.sort(key=lambda h: h.score, reverse=True)
|
|
120
|
+
hits = scored[: request.top_k]
|
|
121
|
+
|
|
122
|
+
response = KnowledgeRetrieveResponse(hits=hits)
|
|
123
|
+
|
|
124
|
+
refs = [
|
|
125
|
+
KnowledgeSourceRef(
|
|
126
|
+
ref_id=h.record.record_id,
|
|
127
|
+
ref_type=f"corpus:{h.record.corpus_id}",
|
|
128
|
+
title=h.record.title,
|
|
129
|
+
locator=h.record.source_uri,
|
|
130
|
+
).model_dump()
|
|
131
|
+
for h in hits
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"payload": response.model_dump(),
|
|
136
|
+
"source_refs": refs,
|
|
137
|
+
"evidence_refs": refs,
|
|
138
|
+
"connector_access": [],
|
|
139
|
+
"baseline_version": request.baseline_version,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = ["knowledge_retrieve"]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""``knowledge.search`` / 1.0.0 / L1 — semantic / hybrid search.
|
|
2
|
+
|
|
3
|
+
FTR-603 Wave 5 Conv C / Decision #122. V1 thin-proxy harvest of the
|
|
4
|
+
D-LOCKED-23 active V1 ``search`` primitive (PRD §8.11.3).
|
|
5
|
+
|
|
6
|
+
V1 contract surface (from
|
|
7
|
+
:class:`plm_shared.knowledge.primitives_v1.KnowledgeSearchRequest`):
|
|
8
|
+
|
|
9
|
+
payload = {
|
|
10
|
+
"corpus_id": str, # required
|
|
11
|
+
"query": str, # required
|
|
12
|
+
"top_k": int, # default 10, 1..100
|
|
13
|
+
"filters": dict, # default {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Output:
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
"payload": {
|
|
20
|
+
"hits": [<KnowledgeHit as dict>, ...],
|
|
21
|
+
"total": int,
|
|
22
|
+
},
|
|
23
|
+
"source_refs": [<KnowledgeSourceRef as dict>, ...],
|
|
24
|
+
"evidence_refs": [<KnowledgeSourceRef as dict>, ...],
|
|
25
|
+
"connector_access": [], # V1 corpus is local — no connector reach-out
|
|
26
|
+
"baseline_version": "v1-corpus@rev-1",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
V1 implementation: deterministic substring-match against the in-process
|
|
30
|
+
seed corpus (:mod:`plm_skill_kernel.skills.knowledge._v1_corpus`).
|
|
31
|
+
V1.1 will swap to the production retrieval backend through the same
|
|
32
|
+
public skill contract — the lazy backend-binding pattern in the
|
|
33
|
+
``agents.suggest`` precedent (Wave 3 Conv A) is the V1.1 template.
|
|
34
|
+
|
|
35
|
+
Read-only by design — no connector_access entries emitted (Foundation /
|
|
36
|
+
local corpus reads are not connector calls). Obligation 8 / invariant
|
|
37
|
+
#11 trivially satisfied.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from typing import Any, Dict, List
|
|
42
|
+
|
|
43
|
+
from pydantic import ValidationError
|
|
44
|
+
|
|
45
|
+
from plm_shared.knowledge.primitives_v1 import (
|
|
46
|
+
KnowledgeHit,
|
|
47
|
+
KnowledgeRecord,
|
|
48
|
+
KnowledgeSearchRequest,
|
|
49
|
+
KnowledgeSearchResponse,
|
|
50
|
+
)
|
|
51
|
+
from plm_shared.knowledge_envelope import KnowledgeSourceRef
|
|
52
|
+
from plm_skill_kernel import SkillContext, skill
|
|
53
|
+
|
|
54
|
+
from ._v1_corpus import CORPUS_RECORDS
|
|
55
|
+
|
|
56
|
+
V1_BASELINE_VERSION = "v1-corpus@rev-1"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _score_substring(haystack: str, needle: str) -> float:
|
|
60
|
+
"""V1 deterministic relevance score in [0, 1].
|
|
61
|
+
|
|
62
|
+
Returns 0.0 when the needle is not found; otherwise weights inverse
|
|
63
|
+
position + length match. The score is corpus-relative, NOT a
|
|
64
|
+
normalised semantic score — consumers MUST treat it as opaque.
|
|
65
|
+
"""
|
|
66
|
+
if not needle:
|
|
67
|
+
return 0.0
|
|
68
|
+
h = haystack.lower()
|
|
69
|
+
n = needle.lower()
|
|
70
|
+
idx = h.find(n)
|
|
71
|
+
if idx < 0:
|
|
72
|
+
return 0.0
|
|
73
|
+
return round(1.0 - (idx / (len(h) + 1)), 4)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _record_dict(corpus_id: str, record_id: str, raw: Dict[str, Any]) -> Dict[str, Any]:
|
|
77
|
+
return KnowledgeRecord(
|
|
78
|
+
record_id=record_id,
|
|
79
|
+
source_uri=raw["source_uri"],
|
|
80
|
+
corpus_id=corpus_id,
|
|
81
|
+
revision=raw["revision"],
|
|
82
|
+
title=raw.get("title"),
|
|
83
|
+
body=raw.get("body"),
|
|
84
|
+
payload=dict(raw.get("payload", {})),
|
|
85
|
+
).model_dump()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@skill(
|
|
89
|
+
id="knowledge.search",
|
|
90
|
+
version="1.0.0",
|
|
91
|
+
description=(
|
|
92
|
+
"Semantic / hybrid search over a Knowledge corpus. L1 read-only "
|
|
93
|
+
"surface; V1 thin-proxy against the in-process seed corpus."
|
|
94
|
+
),
|
|
95
|
+
min_autonomy="L1",
|
|
96
|
+
streaming=False,
|
|
97
|
+
hitl_required=False,
|
|
98
|
+
)
|
|
99
|
+
async def knowledge_search(
|
|
100
|
+
payload: Dict[str, Any], ctx: SkillContext
|
|
101
|
+
) -> Dict[str, Any]:
|
|
102
|
+
"""Validate the request + emit V1 hits + envelope artefacts."""
|
|
103
|
+
try:
|
|
104
|
+
request = KnowledgeSearchRequest.model_validate(payload)
|
|
105
|
+
except ValidationError as exc:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"knowledge.search: invalid request payload — {exc.errors()}"
|
|
108
|
+
) from exc
|
|
109
|
+
|
|
110
|
+
records = CORPUS_RECORDS.get(request.corpus_id, {})
|
|
111
|
+
scored: List[KnowledgeHit] = []
|
|
112
|
+
for rec_id, raw in records.items():
|
|
113
|
+
# V1 substring match against title + body; filters are ignored
|
|
114
|
+
# at V1 (kept on the request for V1.1 contract preservation).
|
|
115
|
+
text_blob = " ".join(
|
|
116
|
+
v
|
|
117
|
+
for v in (raw.get("title"), raw.get("body"))
|
|
118
|
+
if isinstance(v, str)
|
|
119
|
+
)
|
|
120
|
+
score = _score_substring(text_blob, request.query)
|
|
121
|
+
if score <= 0.0:
|
|
122
|
+
continue
|
|
123
|
+
scored.append(
|
|
124
|
+
KnowledgeHit(
|
|
125
|
+
score=score,
|
|
126
|
+
record=KnowledgeRecord(
|
|
127
|
+
record_id=rec_id,
|
|
128
|
+
source_uri=raw["source_uri"],
|
|
129
|
+
corpus_id=request.corpus_id,
|
|
130
|
+
revision=raw["revision"],
|
|
131
|
+
title=raw.get("title"),
|
|
132
|
+
body=raw.get("body"),
|
|
133
|
+
payload=dict(raw.get("payload", {})),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
scored.sort(key=lambda h: h.score, reverse=True)
|
|
138
|
+
hits = scored[: request.top_k]
|
|
139
|
+
|
|
140
|
+
response = KnowledgeSearchResponse(hits=hits, total=len(scored))
|
|
141
|
+
|
|
142
|
+
source_refs = [
|
|
143
|
+
KnowledgeSourceRef(
|
|
144
|
+
ref_id=h.record.record_id,
|
|
145
|
+
ref_type=f"corpus:{h.record.corpus_id}",
|
|
146
|
+
title=h.record.title,
|
|
147
|
+
locator=h.record.source_uri,
|
|
148
|
+
).model_dump()
|
|
149
|
+
for h in hits
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"payload": response.model_dump(),
|
|
154
|
+
"source_refs": source_refs,
|
|
155
|
+
"evidence_refs": [],
|
|
156
|
+
"connector_access": [],
|
|
157
|
+
"baseline_version": V1_BASELINE_VERSION,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = ["V1_BASELINE_VERSION", "knowledge_search"]
|