Skip to content

Commit d2d921c

Browse files
cknollclaude
andcommitted
feat(nemobridge): persistent opt-in for nemo delegation via pyirk config
The delegation flag was env-only (PYIRK_NEMO_DELEGATION), which is tedious for a developer who wants the speedup in all their own runs. Add a delegation_enabled() resolver: env var (decisive in both directions) over pyirk config [nemo] delegation over a shipped default of off. The general user is unaffected unless they opt in; an individual can enable it persistently in their config. Also fixes a latent bug: the old 'if os.environ.get(...)' treated PYIRK_NEMO_DELEGATION=0 / =false as truthy and wrongly ENABLED delegation, contradicting the docs. Falsy tokens (0/false/no/off) now disable correctly. Docs (how-to + README) document the config toggle and precedence; 7 new unit tests cover the env/config/default matrix. Full suite green with flag off (197 passed); rule-based reasoning green with flag on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 163554b commit d2d921c

5 files changed

Lines changed: 130 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Note that, originally pyirk (imperative representation of knowledge) was called
4747

4848
# Optional: Nemo rule-engine delegation
4949

50-
For performance-critical rule sets, pyirk can optionally delegate evaluation of a curated subset of rules to the Nemo datalog engine via its `nmo` CLI (see the upstream Nemo project for installation instructions). The feature is **off by default**; set the environment variable `PYIRK_NEMO_DELEGATION=1` to enable it (and optionally `PYIRK_NEMO_BIN=/abs/path/to/nmo` to pin the binary). The native Python engine remains the authoritative reference and the automatic fallback path. See the how-to guide [`docs/source/howto/nemo_delegation.md`](docs/source/howto/nemo_delegation.md) for resolver order, version policy, and known limits.
50+
For performance-critical rule sets, pyirk can optionally delegate evaluation of a curated subset of rules to the Nemo datalog engine via its `nmo` CLI (see the upstream Nemo project for installation instructions). The feature is **off by default**; enable it either per-invocation via the environment variable `PYIRK_NEMO_DELEGATION=1` or persistently via `[nemo] delegation = true` in your pyirk config (optionally pin the binary with `PYIRK_NEMO_BIN=/abs/path/to/nmo`). The native Python engine remains the authoritative reference and the automatic fallback path. See the how-to guide [`docs/source/howto/nemo_delegation.md`](docs/source/howto/nemo_delegation.md) for resolver order, version policy, and known limits.
5151

5252
# Documentation
5353

docs/source/howto/nemo_delegation.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,36 @@ does **not** bundle or vendor the `nmo` binary.
3434
Two environment variables control the feature; both are read fresh at every
3535
rule-engine entry point.
3636

37-
* `PYIRK_NEMO_DELEGATION=1` — enables the delegation path. Any other value
38-
(or absence) keeps the native Python engine in charge.
37+
* `PYIRK_NEMO_DELEGATION=1` — enables the delegation path. Recognised truthy
38+
tokens are `1`/`true`/`yes`/`on`; `0`/`false`/`no`/`off` explicitly
39+
*disable* it (overriding the config below). An unset or unrecognised value
40+
falls through to the config setting.
3941
* `PYIRK_NEMO_BIN=/abs/path/to/nmo` — optional override that pins the
4042
`nmo` binary used by pyirk. Useful for CI, container images, or when
4143
multiple Nemo versions co-exist on a developer machine.
4244

45+
### Persistent opt-in via the pyirk config
46+
47+
Setting the env var on every invocation is tedious for a developer who
48+
simply wants the speedup in all their own runs. The delegation flag is
49+
therefore *also* read from the pyirk config file (the same
50+
`config.toml` that already carries `[package.ocse]`):
51+
52+
```toml
53+
[nemo]
54+
delegation = true
55+
```
56+
57+
Precedence: the `PYIRK_NEMO_DELEGATION` env var wins in both directions when
58+
set to a recognised token; otherwise the config value decides; otherwise the
59+
shipped default is **off**. This keeps delegation strictly opt-in for the
60+
general user (no behaviour change unless they opt in) while letting an
61+
individual enable it persistently for their own environment.
62+
63+
Note that even when enabled, the hook still checks that an `nmo` binary is
64+
present and version-compatible, and silently falls back to the native engine
65+
otherwise — so a config opt-in on a machine without `nmo` is harmless.
66+
4367
### Resolver order
4468

4569
When delegation is enabled, the binary is resolved in the following

src/pyirk/nemobridge/delegation.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,60 @@
4141
# avoid a hard import-time dependency between the two modules.
4242
_LITERAL_PREFIX = "LIT:"
4343

44+
45+
_TRUTHY_TOKENS = ("1", "true", "yes", "on")
46+
_FALSY_TOKENS = ("0", "false", "no", "off", "")
47+
48+
49+
def _coerce_flag(value) -> "bool | None":
50+
"""Interpret a config/env value as an on/off flag.
51+
52+
Returns ``True``/``False`` for a recognised token, or ``None`` if the
53+
value is absent (``None``) or unrecognised (caller decides the default).
54+
A genuine ``bool`` (e.g. from a TOML ``delegation = true``) passes through.
55+
"""
56+
if value is None:
57+
return None
58+
if isinstance(value, bool):
59+
return value
60+
token = str(value).strip().lower()
61+
if token in _TRUTHY_TOKENS:
62+
return True
63+
if token in _FALSY_TOKENS:
64+
return False
65+
return None
66+
67+
68+
def delegation_enabled() -> bool:
69+
"""Whether the Nemo-delegation path should be used.
70+
71+
Resolution order (first decisive wins):
72+
1. ``PYIRK_NEMO_DELEGATION`` env var — an explicit truthy/falsy token
73+
decides in BOTH directions (so ``=0`` disables even if the config
74+
enables it). An unrecognised value is ignored, not treated as on.
75+
2. pyirk config ``[nemo] delegation`` (``p.CONF``) — lets a user opt in
76+
persistently without setting the env var on every invocation.
77+
3. Default: ``False`` (delegation stays opt-in; the native Python engine
78+
is the shipped default).
79+
80+
Note: this only expresses *intent*. The hook still verifies that an
81+
``nmo`` binary is available and version-compatible before delegating,
82+
and falls back silently otherwise.
83+
"""
84+
env_flag = _coerce_flag(os.environ.get("PYIRK_NEMO_DELEGATION"))
85+
if env_flag is not None:
86+
return env_flag
87+
88+
try:
89+
import pyirk as p
90+
cfg_flag = _coerce_flag(p.CONF.get("nemo", {}).get("delegation"))
91+
if cfg_flag is not None:
92+
return cfg_flag
93+
except Exception:
94+
pass
95+
96+
return False
97+
4498
# ── Deployment-Robustheit (H5 Deployment) ────────────────────────────────────
4599
# Modul-State für idempotente Logs/Warnings — pro Prozess genau einmal.
46100
# Tests setzen diese Flags via monkeypatch zurück.

src/pyirk/ruleengine.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def apply_semantic_rules(*rules: List, mod_context_uri: str = None, exhaust=Fals
7070
7171
:param exhaust: boolean flag; if True: repeat rule application until no new statements are created
7272
73-
Bei aktivem PYIRK_NEMO_DELEGATION, verfügbarem Nemo-Binary und nicht-leerer
73+
Bei aktivierter Delegation (siehe ``delegation_enabled()``: Env-Variable
74+
``PYIRK_NEMO_DELEGATION`` ODER pyirk-Config ``[nemo] delegation``),
75+
verfügbarem Nemo-Binary und nicht-leerer
7476
delegable-Teilmenge wird der V4-Pfad gewählt: ein beschränkter Fixpunkt-Loop
7577
nemo↔python, der pro Iteration zuerst die delegierbaren Regeln via Nemo bis
7678
zu deren Fixpunkt materialisiert und danach die remaining-Regeln genau
@@ -88,7 +90,8 @@ def apply_semantic_rules(*rules: List, mod_context_uri: str = None, exhaust=Fals
8890
delegated: list = []
8991
remaining: list = list(rules)
9092

91-
if os.environ.get("PYIRK_NEMO_DELEGATION"):
93+
from pyirk.nemobridge.delegation import delegation_enabled
94+
if delegation_enabled():
9295
from pyirk.nemobridge.delegation import (
9396
_nemo_available,
9497
_resolve_nmo_bin,

tests/test_nemobridge_resolver.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,47 @@ def test_fires_only_once_across_many_calls(self, caplog):
353353
delegation.mark_nmo_failed_warned(RuntimeError(f"call {i}"))
354354
warns = _records_for(caplog, logging.WARNING, "Nemo delegation failed")
355355
assert len(warns) == 1
356+
357+
358+
class TestDelegationEnabled:
359+
"""``delegation_enabled()`` — env-var (both directions) over pyirk config
360+
over a shipped default of off."""
361+
362+
def test_env_truthy_tokens_enable(self, monkeypatch):
363+
for tok in ("1", "true", "TRUE", "yes", "on"):
364+
monkeypatch.setenv("PYIRK_NEMO_DELEGATION", tok)
365+
assert delegation.delegation_enabled() is True
366+
367+
def test_env_falsy_tokens_disable(self, monkeypatch):
368+
# Regression: the old ``if os.environ.get(...)`` treated "0"/"false"
369+
# as truthy and wrongly ENABLED delegation.
370+
for tok in ("0", "false", "no", "off", ""):
371+
monkeypatch.setenv("PYIRK_NEMO_DELEGATION", tok)
372+
assert delegation.delegation_enabled() is False
373+
374+
def test_env_disable_overrides_config_enable(self, monkeypatch):
375+
monkeypatch.setenv("PYIRK_NEMO_DELEGATION", "0")
376+
monkeypatch.setattr(p, "CONF", {"nemo": {"delegation": True}}, raising=False)
377+
assert delegation.delegation_enabled() is False
378+
379+
def test_config_enables_when_env_unset(self, monkeypatch):
380+
monkeypatch.delenv("PYIRK_NEMO_DELEGATION", raising=False)
381+
monkeypatch.setattr(p, "CONF", {"nemo": {"delegation": True}}, raising=False)
382+
assert delegation.delegation_enabled() is True
383+
384+
def test_config_falsy_keeps_disabled(self, monkeypatch):
385+
monkeypatch.delenv("PYIRK_NEMO_DELEGATION", raising=False)
386+
monkeypatch.setattr(p, "CONF", {"nemo": {"delegation": False}}, raising=False)
387+
assert delegation.delegation_enabled() is False
388+
389+
def test_default_off_when_nothing_set(self, monkeypatch):
390+
monkeypatch.delenv("PYIRK_NEMO_DELEGATION", raising=False)
391+
monkeypatch.setattr(p, "CONF", {}, raising=False)
392+
assert delegation.delegation_enabled() is False
393+
394+
def test_unrecognised_env_falls_through_to_config(self, monkeypatch):
395+
# An unrecognised env token is ignored, not treated as on; the config
396+
# then decides.
397+
monkeypatch.setenv("PYIRK_NEMO_DELEGATION", "maybe")
398+
monkeypatch.setattr(p, "CONF", {"nemo": {"delegation": True}}, raising=False)
399+
assert delegation.delegation_enabled() is True

0 commit comments

Comments
 (0)