plm-skill-kernel 1.2.0__tar.gz → 1.3.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.2.0 → plm_skill_kernel-1.3.0}/PKG-INFO +2 -1
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/__init__.py +1 -1
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/mocks/__init__.py +22 -1
- plm_skill_kernel-1.3.0/plm_skill_kernel/mocks/connectors.py +182 -0
- plm_skill_kernel-1.3.0/plm_skill_kernel/mocks/engine_core_stub.py +136 -0
- plm_skill_kernel-1.3.0/plm_skill_kernel/mocks/knowledge_services_port.py +359 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/PKG-INFO +2 -1
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/SOURCES.txt +6 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/requires.txt +1 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/pyproject.toml +10 -1
- plm_skill_kernel-1.3.0/tests/test_connector_read_mock.py +128 -0
- plm_skill_kernel-1.3.0/tests/test_engine_core_stub_mock.py +141 -0
- plm_skill_kernel-1.3.0/tests/test_knowledge_services_port_mock.py +176 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/README.md +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/__main__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_cli.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_output_safety.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_serialize.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_terminal.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_verdict.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/_version.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/exit_codes.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/models.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/conformance/protocol.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/dispatcher.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/_catalog_fixture.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/binding.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/executor.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/knowledge.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/plan.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/request.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/executor/selector.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/mocks/foundation_seed.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/registry.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/_registry.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/context.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/decorator.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/sdk/types.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/server.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/agents/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/agents/suggest.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/bpmn/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/bpmn/generate.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/cleansing/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/cleansing/dedupe.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/cleansing/normalise.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/documents/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/documents/parse.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/_v1_corpus.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/get_record.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/resolve.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/retrieve.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/search.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills/knowledge/validate.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/__init__.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/knowledge_pack_loader.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/knowledge_search_engine.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/plm_skill_registry.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/plm_tool_definitions.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel/skills_legacy/skill_orchestrator.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/dependency_links.txt +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/entry_points.txt +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/plm_skill_kernel.egg-info/top_level.txt +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/setup.cfg +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_agents_suggest_asgi.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_agents_suggest_unit.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_dispatcher.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_documents_parse_asgi.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_documents_parse_unit.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_executor.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_foundation_mocks.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_sdk_decorator.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_server.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_skills_cleansing.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_skills_knowledge.py +0 -0
- {plm_skill_kernel-1.2.0 → plm_skill_kernel-1.3.0}/tests/test_skills_knowledge_registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plm-skill-kernel
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
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
5
|
Author: TracePulse
|
|
6
6
|
License: Proprietary
|
|
@@ -13,6 +13,7 @@ Requires-Dist: uvicorn>=0.27
|
|
|
13
13
|
Requires-Dist: httpx>=0.27.0
|
|
14
14
|
Requires-Dist: plm-engine-core
|
|
15
15
|
Requires-Dist: plm-knowledge
|
|
16
|
+
Requires-Dist: plm-connector-sdk<2,>=1.1
|
|
16
17
|
Provides-Extra: test
|
|
17
18
|
Requires-Dist: pytest; extra == "test"
|
|
18
19
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
@@ -14,7 +14,7 @@ sanctioned interaction is the V1.1 HTTP loopback (env-flagged on
|
|
|
14
14
|
``SKILL_KERNEL_LOOPBACK``); any Python-level reach-in is forbidden by
|
|
15
15
|
the import-linter contract in ``pyproject.toml``.
|
|
16
16
|
"""
|
|
17
|
-
__version__ = "1.
|
|
17
|
+
__version__ = "1.3.0"
|
|
18
18
|
|
|
19
19
|
# The decorator + sdk surface is the public authoring API. Re-exported
|
|
20
20
|
# here so callers write ``from plm_skill_kernel import skill`` rather
|
|
@@ -50,4 +50,25 @@ def require_mocks(what: str = "this mock") -> None:
|
|
|
50
50
|
)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
# ── governed contract package versions this mocks layer targets (AC4) ──────
|
|
54
|
+
# The MVP mocks stand in for three governed, separately-versioned packages. This
|
|
55
|
+
# dict is the single authoritative record of the exact versions the mocks were
|
|
56
|
+
# written against; ``tests/contracts/test_mvp_mocks_boundary.py`` asserts each
|
|
57
|
+
# against what is actually installed. A governed-package bump therefore surfaces
|
|
58
|
+
# as a failing pin that a human re-syncs on purpose (updating MOCKS.md), never a
|
|
59
|
+
# silent absorption. Per-mock *shape* pins (KS_CONTRACT_VERSION,
|
|
60
|
+
# CONNECTOR_SDK_VERSION, SELECTION_REQUIRED_KEYS) live in their own modules.
|
|
61
|
+
GOVERNED_CONTRACT_VERSIONS = {
|
|
62
|
+
"plm-shared": "1.2.1", # CR.10/CR.12 wire shapes the SDK + dispatcher consume
|
|
63
|
+
"plm-connector-sdk": "1.3.1", # ConnectorReadResult — AC2 reaches a connector through it
|
|
64
|
+
"plm-engine-core": "1.6.0", # runtime.llm_service — the AC3 stub stands in for it
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"GOVERNED_CONTRACT_VERSIONS",
|
|
70
|
+
"MOCKS_ENV",
|
|
71
|
+
"MocksDisabled",
|
|
72
|
+
"mocks_enabled",
|
|
73
|
+
"require_mocks",
|
|
74
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""3DEXPERIENCE connector reached behind its real ``ConnectorReadResult`` (FTR-2234 / E3, AC2).
|
|
2
|
+
|
|
3
|
+
# MOCK(MVP-DEMO): revert / re-sync under FTR-2234.
|
|
4
|
+
#
|
|
5
|
+
# AC2 is explicit: *reuse the existing connector mock, build no new one.* So this
|
|
6
|
+
# module holds **no fixture data and no fake connector**. It wraps the portfolio's
|
|
7
|
+
# already-shipped, fixture-backed ``Dassault3dexperienceMockConnector``
|
|
8
|
+
# (``plm_mcp_connectors``) and exposes its reads through the connector SDK's real,
|
|
9
|
+
# frozen ``ConnectorReadResult`` contract (``plm_connector_sdk`` v1.3.1). The kernel
|
|
10
|
+
# reaches a connector *only* over this seam.
|
|
11
|
+
#
|
|
12
|
+
# Layering (mirrors reality, so the swap is a drop-in):
|
|
13
|
+
#
|
|
14
|
+
# kernel (E4/E5, later)
|
|
15
|
+
# └── ConnectorReadPort.read(method, params) ← reversible seam (this module)
|
|
16
|
+
# └── ConnectorReadResult ← connector SDK contract (real)
|
|
17
|
+
# └── Dassault3dexperienceMockConnector ← existing fixture mock
|
|
18
|
+
# └── in-tree fixture rows (ENG-0001..0003)
|
|
19
|
+
#
|
|
20
|
+
# When a real connector is wired, the reader is handed engine-core's live
|
|
21
|
+
# ``ConnectorRuntime`` instead of the fixture connector; it still returns the same
|
|
22
|
+
# ``ConnectorReadResult``, so the kernel and every task are untouched.
|
|
23
|
+
#
|
|
24
|
+
# Honesty, two ways:
|
|
25
|
+
# * the seam stays **async**. Unlike the E1 knowledge seam (pure in-memory, so
|
|
26
|
+
# AC1 could drive it synchronously), a real connector performs network I/O and
|
|
27
|
+
# must be reached through an async call path — so this reader never pretends a
|
|
28
|
+
# connector read is synchronous, even though the *fixture* behind it is.
|
|
29
|
+
# * the wrapped connector's identity is literally ``dassault_3dexperience_mock``;
|
|
30
|
+
# ``PRODUCTION_BACKED`` is ``False`` and stays that way.
|
|
31
|
+
#
|
|
32
|
+
# Everything here is quarantined behind ``PLM_MVP_MOCKS`` (see
|
|
33
|
+
# :func:`plm_skill_kernel.mocks.require_mocks`). The fixture host
|
|
34
|
+
# (``plm_mcp_connectors``) is imported **lazily**, inside the constructor, so this
|
|
35
|
+
# module stays importable — for the frozen-contract pin below — in an environment
|
|
36
|
+
# that has the connector SDK but not the demo fixture package.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from typing import Any, List, Mapping, Optional, Protocol, runtime_checkable
|
|
41
|
+
|
|
42
|
+
# The connector SDK is a governed contract package (like plm-shared), pinned as a
|
|
43
|
+
# kernel dependency, so its frozen result type is imported at module top. The
|
|
44
|
+
# *fixture host* is not a kernel dependency and is imported lazily below.
|
|
45
|
+
from plm_connector_sdk.context import ConnectorContext
|
|
46
|
+
from plm_connector_sdk.types import ConnectorReadRequest, ConnectorReadResult
|
|
47
|
+
|
|
48
|
+
from . import require_mocks
|
|
49
|
+
|
|
50
|
+
# ── pinned connector SDK identity ──────────────────────────────────────────
|
|
51
|
+
# Mirror of plm-connector-sdk's package version. Pinned as a literal (the SDK
|
|
52
|
+
# does not export ``__version__``) so the freeze test asserts the exact contract
|
|
53
|
+
# generation this mock was written against; a bump must be re-synced on purpose.
|
|
54
|
+
CONNECTOR_SDK_VERSION = "1.3.1"
|
|
55
|
+
|
|
56
|
+
# The fixture connector this mock reuses (built under plm-mcp-connectors, NOT here).
|
|
57
|
+
FIXTURE_CONNECTOR_MODULE = "plm_mcp_connectors.dassault_3dexperience_mock"
|
|
58
|
+
|
|
59
|
+
# The read methods the fixture connector serves (§ FTR-2234 AC2). Asking for
|
|
60
|
+
# anything else is a programming error, not a connector "not found".
|
|
61
|
+
SUPPORTED_METHODS = frozenset({"get_eng_item", "search_eng_items", "get_bom"})
|
|
62
|
+
|
|
63
|
+
DEFAULT_TENANT = "mvp-demo"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class UnsupportedConnectorMethod(RuntimeError):
|
|
67
|
+
"""Raised when the reader is asked for a method the fixture connector lacks."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@runtime_checkable
|
|
71
|
+
class ConnectorReadPort(Protocol):
|
|
72
|
+
"""The reversible connector seam: any object that answers a named read with a
|
|
73
|
+
:class:`~plm_connector_sdk.types.ConnectorReadResult`.
|
|
74
|
+
|
|
75
|
+
The demo satisfies it with :class:`FixtureConnectorReader` (wrapping the
|
|
76
|
+
fixture mock); a real deployment satisfies it with an engine-core connector
|
|
77
|
+
runtime. The kernel depends only on this surface, never on which side backs it.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
async def read(
|
|
81
|
+
self, method: str, params: Mapping[str, Any], *, tenant_id: str = ...
|
|
82
|
+
) -> ConnectorReadResult:
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FixtureConnectorReader:
|
|
87
|
+
"""Reads the existing 3DEXPERIENCE **fixture** connector behind ``ConnectorReadResult`` (AC2).
|
|
88
|
+
|
|
89
|
+
Builds no connector and no fixture data of its own: it constructs the
|
|
90
|
+
portfolio's :class:`Dassault3dexperienceMockConnector` and forwards each read
|
|
91
|
+
to it, wrapping the call in the SDK's real ``ConnectorReadRequest`` /
|
|
92
|
+
``ConnectorContext`` and returning the connector's real ``ConnectorReadResult``.
|
|
93
|
+
|
|
94
|
+
Gated: constructing without ``PLM_MVP_MOCKS`` raises
|
|
95
|
+
:class:`~plm_skill_kernel.mocks.MocksDisabled`.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Honesty flags — parity with the KS mock's descriptor (AC1). A fixture
|
|
99
|
+
# connector is never a production claim.
|
|
100
|
+
PRODUCTION_BACKED = False
|
|
101
|
+
TEST_ONLY = True
|
|
102
|
+
|
|
103
|
+
def __init__(self, *, tenant_id: str = DEFAULT_TENANT) -> None:
|
|
104
|
+
require_mocks("FixtureConnectorReader")
|
|
105
|
+
# Lazy import: keeps this module importable (for the frozen-contract pin)
|
|
106
|
+
# without the demo fixture package present.
|
|
107
|
+
import importlib
|
|
108
|
+
|
|
109
|
+
fixture = importlib.import_module(FIXTURE_CONNECTOR_MODULE)
|
|
110
|
+
self._connector = fixture.Dassault3dexperienceMockConnector()
|
|
111
|
+
self.connector_id: str = fixture.CONNECTOR_ID
|
|
112
|
+
self.connector_version: str = fixture.CONNECTOR_VERSION
|
|
113
|
+
self._tenant_id = tenant_id
|
|
114
|
+
|
|
115
|
+
async def read(
|
|
116
|
+
self,
|
|
117
|
+
method: str,
|
|
118
|
+
params: Mapping[str, Any],
|
|
119
|
+
*,
|
|
120
|
+
tenant_id: Optional[str] = None,
|
|
121
|
+
) -> ConnectorReadResult:
|
|
122
|
+
"""Forward a named read to the fixture connector, returning its ``ConnectorReadResult``.
|
|
123
|
+
|
|
124
|
+
``method`` must be one of :data:`SUPPORTED_METHODS`; an unknown method is a
|
|
125
|
+
caller bug (:class:`UnsupportedConnectorMethod`), distinct from a connector
|
|
126
|
+
"not found" — which the connector itself reports as ``ok=False`` with an
|
|
127
|
+
error envelope.
|
|
128
|
+
"""
|
|
129
|
+
if method not in SUPPORTED_METHODS:
|
|
130
|
+
raise UnsupportedConnectorMethod(
|
|
131
|
+
f"{method!r} is not served by {self.connector_id}; "
|
|
132
|
+
f"supported: {sorted(SUPPORTED_METHODS)}"
|
|
133
|
+
)
|
|
134
|
+
request = ConnectorReadRequest(
|
|
135
|
+
params=dict(params), version=self.connector_version
|
|
136
|
+
)
|
|
137
|
+
context = ConnectorContext(
|
|
138
|
+
connector_id=self.connector_id,
|
|
139
|
+
connector_version=self.connector_version,
|
|
140
|
+
method_name=method,
|
|
141
|
+
tenant_id=tenant_id or self._tenant_id,
|
|
142
|
+
)
|
|
143
|
+
bound = getattr(self._connector, method)
|
|
144
|
+
return await bound(request, context)
|
|
145
|
+
|
|
146
|
+
# ── convenience wrappers over read() (the three fixture reads) ──────────
|
|
147
|
+
async def get_eng_item(
|
|
148
|
+
self, physical_id: str, *, tenant_id: Optional[str] = None
|
|
149
|
+
) -> ConnectorReadResult:
|
|
150
|
+
return await self.read(
|
|
151
|
+
"get_eng_item", {"physical_id": physical_id}, tenant_id=tenant_id
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def search_eng_items(
|
|
155
|
+
self,
|
|
156
|
+
query: str,
|
|
157
|
+
*,
|
|
158
|
+
max_results: Optional[int] = None,
|
|
159
|
+
tenant_id: Optional[str] = None,
|
|
160
|
+
) -> ConnectorReadResult:
|
|
161
|
+
params: dict[str, Any] = {"query": query}
|
|
162
|
+
if max_results is not None:
|
|
163
|
+
params["max_results"] = max_results
|
|
164
|
+
return await self.read("search_eng_items", params, tenant_id=tenant_id)
|
|
165
|
+
|
|
166
|
+
async def get_bom(
|
|
167
|
+
self, physical_id: str, *, tenant_id: Optional[str] = None
|
|
168
|
+
) -> ConnectorReadResult:
|
|
169
|
+
return await self.read(
|
|
170
|
+
"get_bom", {"physical_id": physical_id}, tenant_id=tenant_id
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
__all__: List[str] = [
|
|
175
|
+
"CONNECTOR_SDK_VERSION",
|
|
176
|
+
"DEFAULT_TENANT",
|
|
177
|
+
"FIXTURE_CONNECTOR_MODULE",
|
|
178
|
+
"SUPPORTED_METHODS",
|
|
179
|
+
"ConnectorReadPort",
|
|
180
|
+
"FixtureConnectorReader",
|
|
181
|
+
"UnsupportedConnectorMethod",
|
|
182
|
+
]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Core LLM / DB paths mocked, kept OFF the critical path (FTR-2234 / E3, AC3).
|
|
2
|
+
|
|
3
|
+
# MOCK(MVP-DEMO): revert / re-sync under FTR-2234.
|
|
4
|
+
#
|
|
5
|
+
# The MVP critical path — parse_request -> select_skill -> SkillExecutor.run ->
|
|
6
|
+
# ordered tasks -> KnowledgeProvider.fetch — is deterministic and touches
|
|
7
|
+
# neither an LLM nor a database. Determinism is the property that makes the demo
|
|
8
|
+
# reproducible, not a lack of ambition: the request is routed by keyword rules
|
|
9
|
+
# (executor/selector.py), not by a model.
|
|
10
|
+
#
|
|
11
|
+
# There are exactly two ways the *kernel* could reach engine-core at runtime,
|
|
12
|
+
# and both are behind the federation boundary import-linter already fences off
|
|
13
|
+
# (Decision #51, CR.9 §6 — see pyproject.toml [tool.importlinter]):
|
|
14
|
+
#
|
|
15
|
+
# * the LLM path — the legacy ``SkillOrchestrator`` (plan-mode selection) does
|
|
16
|
+
# ``from plm_engine_core.runtime.llm_service import llm_service`` lazily,
|
|
17
|
+
# inside ``select_skill`` (skill_orchestrator.py:115). That single line is
|
|
18
|
+
# the *only* allowlisted exception to "the kernel must not import
|
|
19
|
+
# plm_engine_core". The MVP demo path never calls it.
|
|
20
|
+
# * the DB path — engine-core owns persistence. The kernel has NO database
|
|
21
|
+
# seam of its own: nothing under ``plm_skill_kernel`` imports a DB driver or
|
|
22
|
+
# ``plm_engine_core.*`` persistence. So there is nothing kernel-side to
|
|
23
|
+
# "mock" for the DB — the import-linter forbidden contract already keeps that
|
|
24
|
+
# path off every kernel module, critical or not.
|
|
25
|
+
#
|
|
26
|
+
# This module supplies the LLM half: a deterministic, in-memory stand-in for the
|
|
27
|
+
# ``llm_service`` singleton, so that IF the legacy plan-mode selection path is
|
|
28
|
+
# exercised in a demo it runs against canned, schema-valid output instead of
|
|
29
|
+
# LiteLLM + a live model — importing neither ``plm_engine_core`` nor ``litellm``.
|
|
30
|
+
#
|
|
31
|
+
# What it is NOT: it is never imported by any production path and never wired
|
|
32
|
+
# into the executor. A caller opts in explicitly, under ``PLM_MVP_MOCKS``. The
|
|
33
|
+
# freeze pin (tests/contracts/test_mvp_engine_core_stub_freeze.py) asserts, by
|
|
34
|
+
# source scan, that this file imports no engine-core / LiteLLM symbol — so the
|
|
35
|
+
# "no heavy import" promise cannot rot silently.
|
|
36
|
+
"""
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
from typing import Any, Dict, List, Optional
|
|
41
|
+
|
|
42
|
+
from . import require_mocks
|
|
43
|
+
|
|
44
|
+
# ── the reply contract the legacy orchestrator expects ─────────────────────
|
|
45
|
+
# SkillOrchestrator.generate_reply returns a *string* that json.loads-es into an
|
|
46
|
+
# object with exactly these keys (its ``_SELECTION_SCHEMA["required"]``). Pinned
|
|
47
|
+
# here as a literal; the behavioural test cross-checks it against the real
|
|
48
|
+
# orchestrator schema so the two cannot drift apart unnoticed.
|
|
49
|
+
SELECTION_REQUIRED_KEYS = ("profile_id", "reasoning", "confidence", "considered_profiles")
|
|
50
|
+
|
|
51
|
+
# The orchestrator's own documented fallback profile id (used when no candidate
|
|
52
|
+
# keyword matches). Kept as a literal so this module needs no registry import.
|
|
53
|
+
DEFAULT_PROFILE_ID = "generique-plm"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class StubLLMService:
|
|
57
|
+
"""Deterministic, in-memory stand-in for ``plm_engine_core.runtime.llm_service``.
|
|
58
|
+
|
|
59
|
+
Satisfies the single coroutine the legacy :class:`SkillOrchestrator` calls —
|
|
60
|
+
``generate_reply(system_prompt, user_prompt, temperature)`` — and returns a
|
|
61
|
+
canned, schema-valid selection JSON *string* (never touching a model). Same
|
|
62
|
+
``user_prompt`` in → same reply out; there is no clock, no network, no
|
|
63
|
+
provider, and no ``plm_engine_core`` / ``litellm`` import anywhere in reach.
|
|
64
|
+
|
|
65
|
+
Gated: constructing without ``PLM_MVP_MOCKS`` raises
|
|
66
|
+
:class:`~plm_skill_kernel.mocks.MocksDisabled`.
|
|
67
|
+
|
|
68
|
+
A caller may pass ``candidates`` (the valid profile ids for the run); the stub
|
|
69
|
+
picks, deterministically, the first candidate whose id appears as a substring
|
|
70
|
+
of the user prompt, else :attr:`DEFAULT_PROFILE_ID`. That keeps the fake
|
|
71
|
+
"selection" explainable without pretending to be intelligent.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Honesty flags — parity with the KS mock (AC1) and connector reader (AC2).
|
|
75
|
+
PRODUCTION_BACKED = False
|
|
76
|
+
TEST_ONLY = True
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
profile_id: str = DEFAULT_PROFILE_ID,
|
|
82
|
+
confidence: float = 0.9,
|
|
83
|
+
candidates: Optional[List[str]] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
require_mocks("StubLLMService")
|
|
86
|
+
self._profile_id = profile_id
|
|
87
|
+
self._confidence = float(confidence)
|
|
88
|
+
# Candidate ids to route among; defaults to just the fallback profile.
|
|
89
|
+
self._candidates: List[str] = list(candidates or [profile_id])
|
|
90
|
+
# Every call recorded verbatim, so a test can assert the seam was used
|
|
91
|
+
# and with what prompts — without a real model to introspect.
|
|
92
|
+
self.calls: List[Dict[str, Any]] = []
|
|
93
|
+
|
|
94
|
+
async def generate_reply(
|
|
95
|
+
self, *, system_prompt: str, user_prompt: str, temperature: float = 0.1
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Return canned, schema-valid selection JSON (a *string*, like the real service).
|
|
98
|
+
|
|
99
|
+
Keyword-only args mirror the orchestrator's call site
|
|
100
|
+
(``generate_reply(system_prompt=..., user_prompt=..., temperature=0.1)``).
|
|
101
|
+
"""
|
|
102
|
+
self.calls.append(
|
|
103
|
+
{
|
|
104
|
+
"system_prompt": system_prompt,
|
|
105
|
+
"user_prompt": user_prompt,
|
|
106
|
+
"temperature": temperature,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
chosen = self._select(user_prompt)
|
|
110
|
+
payload = {
|
|
111
|
+
"profile_id": chosen,
|
|
112
|
+
"reasoning": "Deterministic MVP stub selection (no live model was called).",
|
|
113
|
+
"confidence": self._confidence,
|
|
114
|
+
# top-3 considered, selected id first — matches the schema's intent
|
|
115
|
+
"considered_profiles": self._considered(chosen),
|
|
116
|
+
}
|
|
117
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
118
|
+
|
|
119
|
+
# ── deterministic, explainable "selection" ─────────────────────────────
|
|
120
|
+
def _select(self, user_prompt: str) -> str:
|
|
121
|
+
hay = (user_prompt or "").lower()
|
|
122
|
+
for candidate in self._candidates:
|
|
123
|
+
if candidate.lower() in hay:
|
|
124
|
+
return candidate
|
|
125
|
+
return self._profile_id
|
|
126
|
+
|
|
127
|
+
def _considered(self, chosen: str) -> List[str]:
|
|
128
|
+
ordered = [chosen] + [c for c in self._candidates if c != chosen]
|
|
129
|
+
return ordered[:3]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__: List[str] = [
|
|
133
|
+
"DEFAULT_PROFILE_ID",
|
|
134
|
+
"SELECTION_REQUIRED_KEYS",
|
|
135
|
+
"StubLLMService",
|
|
136
|
+
]
|