From a5879860adf8216c5e8db44d1396e0e7e4638cd3 Mon Sep 17 00:00:00 2001 From: David de Best Date: Mon, 30 Mar 2026 18:02:44 +0200 Subject: [PATCH 1/6] Implement registration states for KB and KIs --- src/knowledge_base.py | 111 +++++++++++++++++++++----------- src/knowledge_interaction.py | 7 +++ tests/test_kb_lifespan.py | 7 ++- tests/test_ki_registration.py | 115 +++++++++++++++++++++++++++++++++- 4 files changed, 198 insertions(+), 42 deletions(-) diff --git a/src/knowledge_base.py b/src/knowledge_base.py index 22eb945..be58534 100644 --- a/src/knowledge_base.py +++ b/src/knowledge_base.py @@ -1,5 +1,6 @@ import logging from collections.abc import Callable +from enum import StrEnum from functools import wraps from .ke import Client @@ -13,15 +14,23 @@ KnowledgeInteractionInfo, PostReactInteractionInfo, ) -from .knowledge_interaction import Handler, KnowledgeInteractionContext +from .knowledge_interaction import ( + Handler, + KnowledgeInteractionContext, + KnowledgeInteractionStatus, +) logger = logging.getLogger(__name__) +class KnowledgeBaseState(StrEnum): + UNREGISTERED = "unregistered" + REGISTERED = "registered" + + class KnowledgeBase: def __init__(self, id: str, name: str, description: str, ke_url: str): - self.registered = False - self.deferred_kis: list[KnowledgeInteractionContext] = [] + self.state = KnowledgeBaseState.UNREGISTERED self.ki_registry: dict[str, KnowledgeInteractionContext] = {} self.client = Client(ke_url) self.info = KnowledgeBaseInfo( @@ -32,8 +41,7 @@ def __init__(self, id: str, name: str, description: str, ke_url: str): def connect(self) -> None: """Checks whether the KE runtime is available and raises an exception if not.""" - connected = self.client.ke_is_available() - if not connected: + if not self.client.ke_is_available(): raise KnowledgeEngineNotAvailableError(self.client.ke_url) def register(self) -> None: @@ -41,8 +49,8 @@ def register(self) -> None: "Registering knowledge base '%s' (%s).", self.info.id, self.info.name ) self.client.register_kb(self.info) - self.registered = True - self.register_deferred_kis() + self.state = KnowledgeBaseState.REGISTERED + self.sync_knowledge_interactions() return def unregister(self) -> None: @@ -50,26 +58,44 @@ def unregister(self) -> None: "Unregistering knowledge base '%s' (%s).", self.info.id, self.info.name ) self.client.unregister_kb(self.info.id) - self.registered = False + self.state = KnowledgeBaseState.UNREGISTERED + for ki_ctx in self.ki_registry.values(): + ki_ctx.status = KnowledgeInteractionStatus.UNREGISTERED return def register_ki( - self, ki_ctx: KnowledgeInteractionContext, defer_registration: bool = False + self, ki_ctx: KnowledgeInteractionContext, defer_ke_registration: bool = False ) -> KnowledgeInteractionInfo: - if defer_registration: - self.deferred_kis.append(ki_ctx) - return ki_ctx.info - else: - registered_ki = self.client.register_ki( - kb_id=self.info.id, - ki=ki_ctx.info, + if self.state != KnowledgeBaseState.REGISTERED and not defer_ke_registration: + raise ValueError( + f"Cannot register KI '{ki_ctx.info.name}' because the KB is not " + f"registered. Consider setting defer_ke_registration=True to defer " + f"registration until the KB itself is registered." + ) + if ki_ctx.info.name in (ki.info.name for ki in self.ki_registry.values()): + raise ValueError( + f"A KI named '{ki_ctx.info.name}' is already registered for this KB." + ) + if ki_ctx.status == KnowledgeInteractionStatus.REGISTERED: + raise ValueError( + f"Cannot register KI '{ki_ctx.info.name}' because it is already " + f"registered." ) - ki_ctx.info = registered_ki - self.ki_registry[registered_ki.id] = ki_ctx - return registered_ki + + self.ki_registry[ki_ctx.info.name] = ki_ctx + if defer_ke_registration: + return ki_ctx.info + + registered_ki = self.client.register_ki( + kb_id=self.info.id, + ki=ki_ctx.info, + ) + ki_ctx.info = registered_ki + ki_ctx.status = KnowledgeInteractionStatus.REGISTERED + return registered_ki def _register_ki_decorator( - self, info: KnowledgeInteractionInfo, defer_registration: bool + self, info: KnowledgeInteractionInfo, defer_ke_registration: bool ) -> Callable[[Handler], Handler]: def decorator(func: Handler) -> Handler: @wraps(func) @@ -77,17 +103,31 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) self.register_ki( - KnowledgeInteractionContext(info=info, handler=func), - defer_registration=defer_registration, + KnowledgeInteractionContext( + info=info, + handler=func, + status=KnowledgeInteractionStatus.UNREGISTERED, + ), + defer_ke_registration=defer_ke_registration, ) return wrapper return decorator - def register_deferred_kis(self) -> None: - for ki_ctx in self.deferred_kis: - self.register_ki(ki_ctx, defer_registration=False) - self.deferred_kis.clear() + def sync_knowledge_interactions(self) -> None: + if self.state != KnowledgeBaseState.REGISTERED: + raise ValueError( + "Cannot sync KIs because the KB is not registered. Please register " + "the KB first." + ) + for ki_ctx in self.ki_registry.values(): + if ki_ctx.status == KnowledgeInteractionStatus.REGISTERED: + continue + ki_ctx.info = self.client.register_ki( + kb_id=self.info.id, + ki=ki_ctx.info, + ) + ki_ctx.status = KnowledgeInteractionStatus.REGISTERED return def ask_ki( @@ -95,7 +135,7 @@ def ask_ki( name: str, graph_pattern: str, prefixes: dict = None, - defer_registration: bool = True, + defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: return self._register_ki_decorator( info=AskAnswerInteractionInfo( @@ -104,7 +144,7 @@ def ask_ki( prefixes=prefixes or dict(), graph_pattern=graph_pattern, ), - defer_registration=defer_registration, + defer_ke_registration=defer_ke_registration, ) def answer_ki( @@ -112,7 +152,7 @@ def answer_ki( name: str, graph_pattern: str, prefixes: dict = None, - defer_registration: bool = True, + defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: return self._register_ki_decorator( info=AskAnswerInteractionInfo( @@ -121,7 +161,7 @@ def answer_ki( prefixes=prefixes or dict(), graph_pattern=graph_pattern, ), - defer_registration=defer_registration, + defer_ke_registration=defer_ke_registration, ) def post_ki( @@ -130,7 +170,7 @@ def post_ki( argument_graph_pattern: str, result_graph_pattern: str, prefixes: dict = None, - defer_registration: bool = True, + defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: return self._register_ki_decorator( info=PostReactInteractionInfo( @@ -140,7 +180,7 @@ def post_ki( argument_graph_pattern=argument_graph_pattern, result_graph_pattern=result_graph_pattern, ), - defer_registration=defer_registration, + defer_ke_registration=defer_ke_registration, ) def react_ki( @@ -149,7 +189,7 @@ def react_ki( argument_graph_pattern: str, result_graph_pattern: str, prefixes: dict = None, - defer_registration: bool = True, + defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: return self._register_ki_decorator( info=PostReactInteractionInfo( @@ -159,13 +199,10 @@ def react_ki( argument_graph_pattern=argument_graph_pattern, result_graph_pattern=result_graph_pattern, ), - defer_registration=defer_registration, + defer_ke_registration=defer_ke_registration, ) def call(self, binding_set: BindingSet, ki_id: str) -> BindingSet: - if ki_id not in self.ki_registry: - raise ValueError(f"Knowledge Interaction '{ki_id}' is not registered.") - ki_ctx = self.ki_registry[ki_id] result = ki_ctx.handler(binding_set) return result diff --git a/src/knowledge_interaction.py b/src/knowledge_interaction.py index 5f89049..27d3b76 100644 --- a/src/knowledge_interaction.py +++ b/src/knowledge_interaction.py @@ -1,6 +1,7 @@ import inspect from collections.abc import Callable from dataclasses import dataclass +from enum import StrEnum from typing import Concatenate from src.ke.models import BindingModel, KnowledgeInteractionInfo @@ -10,10 +11,16 @@ ] +class KnowledgeInteractionStatus(StrEnum): + REGISTERED = "registered" + UNREGISTERED = "unregistered" + + @dataclass class KnowledgeInteractionContext: info: KnowledgeInteractionInfo handler: Handler + status: KnowledgeInteractionStatus = KnowledgeInteractionStatus.UNREGISTERED def __post_init__(self): if not callable(self.handler): diff --git a/tests/test_kb_lifespan.py b/tests/test_kb_lifespan.py index ca35aa7..e9124a8 100644 --- a/tests/test_kb_lifespan.py +++ b/tests/test_kb_lifespan.py @@ -3,7 +3,7 @@ import pytest from src.ke.errors import KnowledgeEngineNotAvailableError -from src.knowledge_base import KnowledgeBase +from src.knowledge_base import KnowledgeBase, KnowledgeBaseState from tests.fake_client import FakeClient @@ -39,8 +39,9 @@ def test_connect_raises_if_ke_unavailable(kb: KnowledgeBase): def test_register_unregister_cycle(kb: KnowledgeBase, client: FakeClient): kb.connect() kb.register() - assert kb.registered + assert kb.state == KnowledgeBaseState.REGISTERED assert client.get_knowledge_base(kb.info.id) is not None kb.unregister() - assert not kb.registered + assert kb.state == KnowledgeBaseState.UNREGISTERED assert client.get_knowledge_base(kb.info.id) is None + diff --git a/tests/test_ki_registration.py b/tests/test_ki_registration.py index 58f49ce..73bced2 100644 --- a/tests/test_ki_registration.py +++ b/tests/test_ki_registration.py @@ -1,5 +1,11 @@ -from src.ke.models import BindingSet, KiTypes +import pytest + +from src.ke.models import AskAnswerInteractionInfo, BindingSet, KiTypes from src.knowledge_base import KnowledgeBase +from src.knowledge_interaction import ( + KnowledgeInteractionContext, + KnowledgeInteractionStatus, +) from tests.fake_client import FakeClient @@ -21,6 +27,88 @@ def shared_prefixes(): } +def ki_ctx_setup() -> KnowledgeInteractionContext: + return KnowledgeInteractionContext( + info=AskAnswerInteractionInfo( + name="test-ki", + type=KiTypes.ANSWER, + graph_pattern="""?s ?p ?o . """, + prefixes=shared_prefixes(), + ), + handler=lambda binding_set: binding_set, # type: ignore + status=KnowledgeInteractionStatus.UNREGISTERED, + ) + + +def test_register_ki(): + kb = kb_setup() + kb.register() + kb.register_ki(ki_ctx=ki_ctx_setup()) + assert len(kb.ki_registry) == 1 + ki_ctx = next(iter(kb.ki_registry.values())) + assert ki_ctx.info.name == "test-ki" + + +def test_register_ki_before_kb_registration(): + kb = kb_setup() + with pytest.raises(ValueError): + kb.register_ki(ki_ctx=ki_ctx_setup()) + + +def test_register_ki_old_name(): + kb = kb_setup() + kb.register() + ki_ctx = ki_ctx_setup() + kb.register_ki(ki_ctx=ki_ctx) + with pytest.raises(ValueError): + kb.register_ki(ki_ctx=ki_ctx) + + +def test_register_ki_already_registered(): + kb = kb_setup() + kb.register() + ki_ctx = ki_ctx_setup() + ki_ctx.status = KnowledgeInteractionStatus.REGISTERED + with pytest.raises(ValueError): + kb.register_ki(ki_ctx=ki_ctx) + + +def test_sync_ki(): + kb = kb_setup() + kb.register() + ki_ctx = ki_ctx_setup() + kb.register_ki(ki_ctx=ki_ctx, defer_ke_registration=True) + assert len(kb.ki_registry) == 1 + assert ( + next(iter(kb.ki_registry.values())).status + == KnowledgeInteractionStatus.UNREGISTERED + ) + + kb.sync_knowledge_interactions() + assert ( + next(iter(kb.ki_registry.values())).status + == KnowledgeInteractionStatus.REGISTERED + ) + + +def test_sych_ki_before_kb_registration(): + kb = kb_setup() + with pytest.raises(ValueError): + kb.sync_knowledge_interactions() + + +def test_unregister_ki_after_kb_unregistration(): + kb = kb_setup() + kb.register() + ki_ctx = ki_ctx_setup() + kb.register_ki(ki_ctx=ki_ctx) + kb.unregister() + assert ( + next(iter(kb.ki_registry.values())).status + == KnowledgeInteractionStatus.UNREGISTERED + ) + + def test_register_answer_ki(): kb = kb_setup() @@ -69,6 +157,29 @@ def react_test(binding_set: BindingSet) -> BindingSet: assert ki_info.type == KiTypes.REACT +def test_register_ki_with_same_name(): + kb = kb_setup() + + @kb.answer_ki( + name="duplicate-name", + graph_pattern=""" + ?s ?p ?o . + """, + ) + def first_handler(binding_set: BindingSet) -> BindingSet: + pass + + with pytest.raises(ValueError): + + @kb.react_ki( + name="duplicate-name", + argument_graph_pattern="""?s ?p ?o . """, + result_graph_pattern="""?s ?p ?o . """, + ) + def second_handler(binding_set: BindingSet) -> BindingSet: + pass + + def test_handler_registration_no_binding_set_param(): kb = kb_setup() @@ -107,5 +218,5 @@ def echo_handler(binding_set: BindingSet) -> BindingSet: ki_info = next(iter(kb.ki_registry.values())).info input_binding_set = [{"input": "test:Input1", "value": "Hello"}] - result = kb.call(binding_set=input_binding_set, ki_id=ki_info.id) + result = kb.call(binding_set=input_binding_set, ki_id=ki_info.name) assert result == input_binding_set From e8ee0a0088b1bb2fa5f02b7c2011fcf65c964620 Mon Sep 17 00:00:00 2001 From: David de Best Date: Tue, 31 Mar 2026 09:06:09 +0200 Subject: [PATCH 2/6] Add synchronization of KI's and tests --- src/knowledge_base.py | 16 +++++++++++++++- tests/test_kb_lifespan.py | 9 +++++++++ tests/test_ki_registration.py | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/knowledge_base.py b/src/knowledge_base.py index be58534..7bd76e7 100644 --- a/src/knowledge_base.py +++ b/src/knowledge_base.py @@ -48,12 +48,20 @@ def register(self) -> None: logger.info( "Registering knowledge base '%s' (%s).", self.info.id, self.info.name ) - self.client.register_kb(self.info) + self.client.register_kb(self.info, reregister=True) self.state = KnowledgeBaseState.REGISTERED self.sync_knowledge_interactions() return def unregister(self) -> None: + if self.state != KnowledgeBaseState.REGISTERED: + logger.warning( + "Knowledge base '%s' (%s) is not registered, cannot unregister.", + self.info.id, + self.info.name, + ) + return + logger.info( "Unregistering knowledge base '%s' (%s).", self.info.id, self.info.name ) @@ -208,6 +216,12 @@ def call(self, binding_set: BindingSet, ki_id: str) -> BindingSet: return result def start_handling_loop(self, loops: int = None) -> None: + if self.state != KnowledgeBaseState.REGISTERED: + raise RuntimeError( + "Cannot start handling loop because the KB is not registered. Please " + "register the KB first." + ) + loops_done = 0 while loops is None or loops_done < loops: loops_done += 1 diff --git a/tests/test_kb_lifespan.py b/tests/test_kb_lifespan.py index e9124a8..1e9d71f 100644 --- a/tests/test_kb_lifespan.py +++ b/tests/test_kb_lifespan.py @@ -45,3 +45,12 @@ def test_register_unregister_cycle(kb: KnowledgeBase, client: FakeClient): assert kb.state == KnowledgeBaseState.UNREGISTERED assert client.get_knowledge_base(kb.info.id) is None + +def test_unregister_without_registering(kb: KnowledgeBase): + kb.connect() + kb.unregister() # Should not raise an exception, just log a warning + + +def test_start_handling_loop_without_registering(kb: KnowledgeBase): + with pytest.raises(RuntimeError): + kb.start_handling_loop(loops=1) diff --git a/tests/test_ki_registration.py b/tests/test_ki_registration.py index 73bced2..1602aeb 100644 --- a/tests/test_ki_registration.py +++ b/tests/test_ki_registration.py @@ -91,7 +91,7 @@ def test_sync_ki(): ) -def test_sych_ki_before_kb_registration(): +def test_sync_ki_before_kb_registration(): kb = kb_setup() with pytest.raises(ValueError): kb.sync_knowledge_interactions() From 9abaf3879f553330137b6f5b83fbdac2860121af Mon Sep 17 00:00:00 2001 From: David de Best Date: Tue, 31 Mar 2026 11:12:21 +0200 Subject: [PATCH 3/6] Add docstrings to client and knowledge base class --- src/__init__.py | 2 + src/ke/client.py | 102 ++++++++++++++++++++++++++++---- src/knowledge_base.py | 131 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 221 insertions(+), 14 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index ee79b5a..25a43f1 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,5 +1,7 @@ import logging +from knowledge_base import KnowledgeBase + __version__ = "0.1.0a0" _handler = logging.StreamHandler() diff --git a/src/ke/client.py b/src/ke/client.py index 9dd058f..b768865 100644 --- a/src/ke/client.py +++ b/src/ke/client.py @@ -37,22 +37,100 @@ class HandleRequest(BaseModel): class ClientProtocol(Protocol): - def ke_is_available(self) -> bool: ... - def ke_version(self) -> str: ... - def get_knowledge_base(self, id: str) -> KnowledgeBaseInfo | None: ... - def get_all_knowledge_bases(self) -> list[KnowledgeBaseInfo]: ... - def register_kb(self, info: KnowledgeBaseInfo, reregister: bool = True) -> None: ... - def unregister_kb(self, id: str) -> None: ... - def get_knowledge_interactions( - self, kb_id: str - ) -> list[KnowledgeInteractionInfo]: ... + """Interface for communicating with a Knowledge Engine runtime.""" + + def ke_is_available(self) -> bool: + """Return ``True`` if the KE runtime is reachable, ``False`` otherwise.""" + ... + + def ke_version(self) -> str: + """Return the version string of the KE runtime. + + Raises: + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + def get_knowledge_base(self, id: str) -> KnowledgeBaseInfo | None: + """Return the KB with the given ID, or ``None`` if it does not exist. + + Raises: + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + def get_all_knowledge_bases(self) -> list[KnowledgeBaseInfo]: + """Return all KBs registered at the KE runtime. + + Raises: + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + def register_kb(self, info: KnowledgeBaseInfo, reregister: bool = True) -> None: + """Register a KB at the KE runtime, optionally re-registering if it already + exists. + + Raises: + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + def unregister_kb(self, id: str) -> None: + """Unregister the KB with the given ID from the KE runtime. + + Raises: + SmartConnectorNotFoundError: If no smart connector exists for the given KB + ID. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + def get_knowledge_interactions(self, kb_id: str) -> list[KnowledgeInteractionInfo]: + """Return all knowledge interactions registered for the given KB. + + Raises: + SmartConnectorNotFoundError: If no smart connector exists for the given KB + ID. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + def register_ki( self, kb_id: str, ki: KnowledgeInteractionInfo - ) -> KnowledgeInteractionInfo: ... - def poll_ki_call(self, kb_id: str) -> PollResult: ... + ) -> KnowledgeInteractionInfo: + """Register a knowledge interaction for the given KB and return it with its + assigned ID set in the info. + + Raises: + SmartConnectorNotFoundError: If no smart connector exists for the given KB + ID. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + + def poll_ki_call(self, kb_id: str) -> PollResult: + """Poll the KE runtime for an incoming KI call for the given KB. + + Raises: + SmartConnectorNotFoundError: If no smart connector exists for the given KB + ID. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ + ... + +class Client(ClientProtocol): + """HTTP client for the Knowledge Engine REST API.""" -class Client: def __init__(self, ke_url: str): self.ke_url = ke_url diff --git a/src/knowledge_base.py b/src/knowledge_base.py index 7bd76e7..ad38a1d 100644 --- a/src/knowledge_base.py +++ b/src/knowledge_base.py @@ -29,6 +29,11 @@ class KnowledgeBaseState(StrEnum): class KnowledgeBase: + """This knowledge base is used for registering and unregistering at a Knowledge + Engine runtime, registering knowledge interactions and calling its handlers. + Starts in unregistered state. + """ + def __init__(self, id: str, name: str, description: str, ke_url: str): self.state = KnowledgeBaseState.UNREGISTERED self.ki_registry: dict[str, KnowledgeInteractionContext] = {} @@ -40,11 +45,22 @@ def __init__(self, id: str, name: str, description: str, ke_url: str): ) def connect(self) -> None: - """Checks whether the KE runtime is available and raises an exception if not.""" + """Checks whether the KE runtime is available and raises an exception if not. + + Raises: + KnowledgeEngineNotAvailableError: If the KE runtime cannot be reached. + """ if not self.client.ke_is_available(): raise KnowledgeEngineNotAvailableError(self.client.ke_url) def register(self) -> None: + """Register this knowledge base at the KE runtime, reregister if already + registered. Automatically syncs knowledge interactions with KE runtime. + + Raises: + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ logger.info( "Registering knowledge base '%s' (%s).", self.info.id, self.info.name ) @@ -54,6 +70,15 @@ def register(self) -> None: return def unregister(self) -> None: + """Unregister this knowledge base at the KE runtime, do nothing if not currently + registered. Knowledge interactions automatically unregistered. + + Raises: + SmartConnectorNotFoundError: If the KB's smart connector is not found in the + KE runtime. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ if self.state != KnowledgeBaseState.REGISTERED: logger.warning( "Knowledge base '%s' (%s) is not registered, cannot unregister.", @@ -74,6 +99,18 @@ def unregister(self) -> None: def register_ki( self, ki_ctx: KnowledgeInteractionContext, defer_ke_registration: bool = False ) -> KnowledgeInteractionInfo: + """Register a knowledge interaction for this knowledge base at the KE runtime + and store it in this object's registry of interactions. + + Raises: + ValueError: If the KB is not yet registered and ``defer_ke_registration`` is + ``False``, if a KI with the same name is already registered, or if the + given KI context is already in a registered state. + SmartConnectorNotFoundError: If the KB's smart connector is not found in the + KE runtime (only when ``defer_ke_registration`` is ``False``). + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response (only when ``defer_ke_registration`` is ``False``). + """ if self.state != KnowledgeBaseState.REGISTERED and not defer_ke_registration: raise ValueError( f"Cannot register KI '{ki_ctx.info.name}' because the KB is not " @@ -105,6 +142,17 @@ def register_ki( def _register_ki_decorator( self, info: KnowledgeInteractionInfo, defer_ke_registration: bool ) -> Callable[[Handler], Handler]: + """Return a decorator that registers the decorated function as a KI handler. + + Raises: + ValueError: Propagated from ``register_ki`` if registration constraints are + violated. + SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting + the KE runtime. + UnexpectedHttpResponseError: Propagated from ``register_ki`` when contacting + the KE runtime. + """ + def decorator(func: Handler) -> Handler: @wraps(func) def wrapper(*args, **kwargs): @@ -123,6 +171,17 @@ def wrapper(*args, **kwargs): return decorator def sync_knowledge_interactions(self) -> None: + """Synchronize registration of knowledge interactions in this object's registry + with the interactions registered at the KE runtime, so all unregistered KIs in + the registry are registered. + + Raises: + ValueError: If the KB is not registered. + SmartConnectorNotFoundError: If the KB's smart connector is not found in + the KE runtime. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + """ if self.state != KnowledgeBaseState.REGISTERED: raise ValueError( "Cannot sync KIs because the KB is not registered. Please register " @@ -145,6 +204,17 @@ def ask_ki( prefixes: dict = None, defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: + """Return a decorator that registers the decorated function as an ASK KI + handler. + + Raises: + ValueError: Propagated from ``register_ki`` if registration constraints are + violated. + SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting + the KE runtime. + UnexpectedHttpResponseError: Propagated from ``register_ki`` when contacting + the KE runtime. + """ return self._register_ki_decorator( info=AskAnswerInteractionInfo( type=KiTypes.ASK, @@ -162,6 +232,17 @@ def answer_ki( prefixes: dict = None, defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: + """Return a decorator that registers the decorated function as an ANSWER KI + handler. + + Raises: + ValueError: Propagated from ``register_ki`` if registration constraints are + violated. + SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting + the KE runtime. + UnexpectedHttpResponseError: Propagated from ``register_ki`` when contacting + the KE runtime. + """ return self._register_ki_decorator( info=AskAnswerInteractionInfo( type=KiTypes.ANSWER, @@ -180,6 +261,17 @@ def post_ki( prefixes: dict = None, defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: + """Return a decorator that registers the decorated function as a POST KI + handler. + + Raises: + ValueError: Propagated from ``register_ki`` if registration constraints are + violated. + SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting + the KE runtime. + UnexpectedHttpResponseError: Propagated from ``register_ki`` when contacting + the KE runtime. + """ return self._register_ki_decorator( info=PostReactInteractionInfo( type=KiTypes.POST, @@ -199,6 +291,17 @@ def react_ki( prefixes: dict = None, defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: + """Return a decorator that registers the decorated function as a REACT KI + handler. + + Raises: + ValueError: Propagated from ``register_ki`` if registration constraints are + violated. + SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting + the KE runtime. + UnexpectedHttpResponseError: Propagated from ``register_ki`` when contacting + the KE runtime. + """ return self._register_ki_decorator( info=PostReactInteractionInfo( type=KiTypes.REACT, @@ -211,17 +314,36 @@ def react_ki( ) def call(self, binding_set: BindingSet, ki_id: str) -> BindingSet: + """Invoke the handler of a registered KI by its ID. + + Raises: + KeyError: If ``ki_id`` is not found in the local KI registry. + """ ki_ctx = self.ki_registry[ki_id] result = ki_ctx.handler(binding_set) return result def start_handling_loop(self, loops: int = None) -> None: + """Poll the KE runtime for incoming KI calls and dispatch them to handlers. + + Runs until an EXIT signal is received from the KE runtime, or until + ``loops`` iterations have been completed if ``loops`` is specified. + + Raises: + RuntimeError: If the KB is not registered. + KeyError: If the KE runtime refers to a KI not found in the local registry. + SmartConnectorNotFoundError: If the KB's smart connector is not found in + the KE runtime. + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + response. + Exception: If an unexpected poll result is returned. + """ if self.state != KnowledgeBaseState.REGISTERED: raise RuntimeError( "Cannot start handling loop because the KB is not registered. Please " "register the KB first." ) - + loops_done = 0 while loops is None or loops_done < loops: loops_done += 1 @@ -244,3 +366,8 @@ def start_handling_loop(self, loops: int = None) -> None: f"Unexpected poll result: {poll_result} or request:" f"{maybe_handle_request}" ) + + @property + def is_registered(self) -> bool: + """Is the knowledge base in the registered state""" + return self.state == KnowledgeBaseState.REGISTERED From 69402cbd97dd897950e89926117d94eb21f20f94 Mon Sep 17 00:00:00 2001 From: David de Best Date: Tue, 31 Mar 2026 11:14:59 +0200 Subject: [PATCH 4/6] Fix imports in tests --- src/__init__.py | 2 +- src/knowledge_base.py | 20 ++++++++++---------- tests/test_kb_lifespan.py | 3 ++- tests/test_ki_registration.py | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 25a43f1..ac75543 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,6 +1,6 @@ import logging -from knowledge_base import KnowledgeBase +from .knowledge_base import KnowledgeBase __version__ = "0.1.0a0" diff --git a/src/knowledge_base.py b/src/knowledge_base.py index ad38a1d..ca46c66 100644 --- a/src/knowledge_base.py +++ b/src/knowledge_base.py @@ -58,7 +58,7 @@ def register(self) -> None: registered. Automatically syncs knowledge interactions with KE runtime. Raises: - UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP response. """ logger.info( @@ -177,9 +177,9 @@ def sync_knowledge_interactions(self) -> None: Raises: ValueError: If the KB is not registered. - SmartConnectorNotFoundError: If the KB's smart connector is not found in + SmartConnectorNotFoundError: If the KB's smart connector is not found in the KE runtime. - UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP + UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP response. """ if self.state != KnowledgeBaseState.REGISTERED: @@ -204,11 +204,11 @@ def ask_ki( prefixes: dict = None, defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: - """Return a decorator that registers the decorated function as an ASK KI + """Return a decorator that registers the decorated function as an ASK KI handler. Raises: - ValueError: Propagated from ``register_ki`` if registration constraints are + ValueError: Propagated from ``register_ki`` if registration constraints are violated. SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting the KE runtime. @@ -232,11 +232,11 @@ def answer_ki( prefixes: dict = None, defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: - """Return a decorator that registers the decorated function as an ANSWER KI + """Return a decorator that registers the decorated function as an ANSWER KI handler. Raises: - ValueError: Propagated from ``register_ki`` if registration constraints are + ValueError: Propagated from ``register_ki`` if registration constraints are violated. SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting the KE runtime. @@ -261,11 +261,11 @@ def post_ki( prefixes: dict = None, defer_ke_registration: bool = True, ) -> Callable[[Handler], Handler]: - """Return a decorator that registers the decorated function as a POST KI + """Return a decorator that registers the decorated function as a POST KI handler. Raises: - ValueError: Propagated from ``register_ki`` if registration constraints are + ValueError: Propagated from ``register_ki`` if registration constraints are violated. SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting the KE runtime. @@ -295,7 +295,7 @@ def react_ki( handler. Raises: - ValueError: Propagated from ``register_ki`` if registration constraints are + ValueError: Propagated from ``register_ki`` if registration constraints are violated. SmartConnectorNotFoundError: Propagated from ``register_ki`` when contacting the KE runtime. diff --git a/tests/test_kb_lifespan.py b/tests/test_kb_lifespan.py index 1e9d71f..4a10203 100644 --- a/tests/test_kb_lifespan.py +++ b/tests/test_kb_lifespan.py @@ -2,8 +2,9 @@ import pytest +from src import KnowledgeBase from src.ke.errors import KnowledgeEngineNotAvailableError -from src.knowledge_base import KnowledgeBase, KnowledgeBaseState +from src.knowledge_base import KnowledgeBaseState from tests.fake_client import FakeClient diff --git a/tests/test_ki_registration.py b/tests/test_ki_registration.py index 1602aeb..dffe73b 100644 --- a/tests/test_ki_registration.py +++ b/tests/test_ki_registration.py @@ -1,7 +1,7 @@ import pytest +from src import KnowledgeBase from src.ke.models import AskAnswerInteractionInfo, BindingSet, KiTypes -from src.knowledge_base import KnowledgeBase from src.knowledge_interaction import ( KnowledgeInteractionContext, KnowledgeInteractionStatus, From 78a5e8f2cde5fc1cfa1796b9338761c336bf89c0 Mon Sep 17 00:00:00 2001 From: David de Best Date: Tue, 7 Apr 2026 09:26:24 +0200 Subject: [PATCH 5/6] Solve comments PR #13 --- src/ke/client.py | 8 ++++++-- src/knowledge_base.py | 11 ++++++----- tests/test_client.py | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/ke/client.py b/src/ke/client.py index b768865..f3dccb2 100644 --- a/src/ke/client.py +++ b/src/ke/client.py @@ -91,7 +91,9 @@ def unregister_kb(self, id: str) -> None: """ ... - def get_knowledge_interactions(self, kb_id: str) -> list[KnowledgeInteractionInfo]: + def get_all_knowledge_interactions( + self, kb_id: str + ) -> list[KnowledgeInteractionInfo]: """Return all knowledge interactions registered for the given KB. Raises: @@ -191,7 +193,9 @@ def unregister_kb(self, id: str) -> None: raise UnexpectedHttpResponseError(response) return - def get_knowledge_interactions(self, kb_id: str) -> list[KnowledgeInteractionInfo]: + def get_all_knowledge_interactions( + self, kb_id: str + ) -> list[KnowledgeInteractionInfo]: response = requests.get( f"{self.ke_url}/sc/ki", headers={"Knowledge-Base-Id": kb_id}, diff --git a/src/knowledge_base.py b/src/knowledge_base.py index ca46c66..af44d1c 100644 --- a/src/knowledge_base.py +++ b/src/knowledge_base.py @@ -171,9 +171,9 @@ def wrapper(*args, **kwargs): return decorator def sync_knowledge_interactions(self) -> None: - """Synchronize registration of knowledge interactions in this object's registry - with the interactions registered at the KE runtime, so all unregistered KIs in - the registry are registered. + """Synchronize registration of knowledge interactions in this object's local + KI registry with the interactions registered at the KE runtime, so all + unregistered KIs in the local registry are registered. Raises: ValueError: If the KB is not registered. @@ -336,7 +336,8 @@ def start_handling_loop(self, loops: int = None) -> None: the KE runtime. UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP response. - Exception: If an unexpected poll result is returned. + RuntimeError: If an unknown long-polling result is obtained from the KE + client. """ if self.state != KnowledgeBaseState.REGISTERED: raise RuntimeError( @@ -362,7 +363,7 @@ def start_handling_loop(self, loops: int = None) -> None: logger.info("Received exit signal from KE, stopping handling loop.") return case _: - raise Exception( + raise RuntimeError( f"Unexpected poll result: {poll_result} or request:" f"{maybe_handle_request}" ) diff --git a/tests/test_client.py b/tests/test_client.py index 0a62a6b..3153505 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -96,7 +96,7 @@ def test_get_knowledge_interactions(client: Client): ] with patch("requests.get", return_value=mock_response) as mock_get: - interactions = client.get_knowledge_interactions("http://example.org/test#kb") + interactions = client.get_all_knowledge_interactions("http://example.org/test#kb") mock_get.assert_called_once_with( "http://fake-ke/sc/ki", From a4779ad587d095d65456fcec6b3df18201e07ab0 Mon Sep 17 00:00:00 2001 From: David de Best Date: Tue, 7 Apr 2026 09:28:32 +0200 Subject: [PATCH 6/6] Fix formatting --- src/knowledge_base.py | 6 +++--- tests/test_client.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/knowledge_base.py b/src/knowledge_base.py index af44d1c..1cd8767 100644 --- a/src/knowledge_base.py +++ b/src/knowledge_base.py @@ -171,8 +171,8 @@ def wrapper(*args, **kwargs): return decorator def sync_knowledge_interactions(self) -> None: - """Synchronize registration of knowledge interactions in this object's local - KI registry with the interactions registered at the KE runtime, so all + """Synchronize registration of knowledge interactions in this object's local + KI registry with the interactions registered at the KE runtime, so all unregistered KIs in the local registry are registered. Raises: @@ -336,7 +336,7 @@ def start_handling_loop(self, loops: int = None) -> None: the KE runtime. UnexpectedHttpResponseError: If the KE runtime returns an unexpected HTTP response. - RuntimeError: If an unknown long-polling result is obtained from the KE + RuntimeError: If an unknown long-polling result is obtained from the KE client. """ if self.state != KnowledgeBaseState.REGISTERED: diff --git a/tests/test_client.py b/tests/test_client.py index 3153505..53dadbf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -96,7 +96,9 @@ def test_get_knowledge_interactions(client: Client): ] with patch("requests.get", return_value=mock_response) as mock_get: - interactions = client.get_all_knowledge_interactions("http://example.org/test#kb") + interactions = client.get_all_knowledge_interactions( + "http://example.org/test#kb" + ) mock_get.assert_called_once_with( "http://fake-ke/sc/ki",