@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-sdk"
|
||||
version = "1.2.8b1+1019"
|
||||
version = "2.0.0b1+1021"
|
||||
description = "Typed, capability-limited extension API for JWeBot plugins"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = []
|
||||
|
||||
@@ -8,6 +8,7 @@ from .context import (
|
||||
PluginLogger,
|
||||
PluginStorage,
|
||||
Scheduler,
|
||||
ServiceCommandAdapter,
|
||||
SingleGroupContext,
|
||||
)
|
||||
from .contracts import (
|
||||
@@ -35,7 +36,8 @@ __all__ = [
|
||||
"PluginStorage",
|
||||
"ReplyPlan",
|
||||
"Scheduler",
|
||||
"ServiceCommandAdapter",
|
||||
"SingleGroupContext",
|
||||
]
|
||||
|
||||
__version__ = "1.2.8b1+1019"
|
||||
__version__ = "2.0.0b1+1021"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import Protocol
|
||||
|
||||
from .contracts import Event, ExtensionResult, Fact
|
||||
from .contracts import Event, ExtensionResult, Fact, ReplyPlan
|
||||
|
||||
|
||||
CommandHandler = Callable[[Event, str], ExtensionResult]
|
||||
@@ -68,3 +68,67 @@ class SingleGroupContext(Protocol):
|
||||
facts: FactWriter
|
||||
scheduler: Scheduler
|
||||
logger: PluginLogger
|
||||
|
||||
|
||||
class ServiceCommandAdapter:
|
||||
"""Register commands that delegate only to declared kernel service profiles."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
profile: str,
|
||||
*,
|
||||
operation: str = "execute",
|
||||
metadata: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
self.profile = self._bounded_text(profile, "profile")
|
||||
self.operation = self._bounded_text(operation, "operation")
|
||||
self.metadata = {
|
||||
str(key): str(value)
|
||||
for key, value in (metadata or {}).items()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _bounded_text(value: object, name: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text or len(text) > 80:
|
||||
raise ValueError(f"{name} is invalid")
|
||||
return text
|
||||
|
||||
def _handler(self, context: SingleGroupContext, command: str) -> CommandHandler:
|
||||
command_name = self._bounded_text(command, "command")
|
||||
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
response = context.services.call(
|
||||
self.profile,
|
||||
operation=self.operation,
|
||||
payload={
|
||||
"command": command_name,
|
||||
"arguments": str(arguments),
|
||||
"event": event.to_mapping(),
|
||||
},
|
||||
)
|
||||
texts = response.get("texts")
|
||||
if isinstance(texts, list) and texts:
|
||||
if not all(isinstance(item, str) and item.strip() for item in texts):
|
||||
raise ValueError("service returned invalid text parts")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.multi_text(texts),
|
||||
metadata=self.metadata,
|
||||
)
|
||||
text = response.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
raise ValueError("service returned invalid text")
|
||||
return ExtensionResult(
|
||||
reply=ReplyPlan.text(text),
|
||||
metadata=self.metadata,
|
||||
)
|
||||
|
||||
return handle
|
||||
|
||||
def register(
|
||||
self,
|
||||
context: SingleGroupContext,
|
||||
commands: Iterable[str],
|
||||
) -> None:
|
||||
for command in commands:
|
||||
context.commands.register(command, self._handler(context, command))
|
||||
|
||||
Reference in New Issue
Block a user