Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions examples/integrations/openwebui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ with real chat ids, restart state loss, and non-text bypass behavior.
Note: In the OpenWebUI example pipes, recognized directive-only `update`
decisions return deterministic local acknowledgments and do not call the
downstream LLM.
Both pipes support an exact local inspection command: `show state`.
When the latest user message is exactly `show state` (case-insensitive after trim),
the pipe returns current compiler state locally and does not call the downstream model.
When trace is enabled, responses include concise evidence of decision kind,
active state, downstream LLM call/no-call, and whether state was injected.

Expand All @@ -102,36 +105,36 @@ active state, downstream LLM call/no-call, and whether state was injected.
- base model: “To adjust the tone… provide the original content…”
- basic pipe: `No premise exists yet. Use 'set premise ...' first.`
- preprocessor pipe: `No premise exists yet. Use 'set premise ...' first.`
- why this is a real win: lifecycle rule is enforced in a fixed, repeatable way; base model drifts into generic rewriting help.
- why this matters: lifecycle rule is enforced in a fixed, repeatable way; base model drifts into generic rewriting help.

**Case 2**

- prompt(s): `clear state` → `use docker` → `prohibit docker`
- base model: generic Docker/prohibition guidance text
- basic pipe: `'docker' is already in use. Only one policy per item is allowed. Use 'reset policies' to change it.`
- preprocessor pipe: same conflict clarify
- why this is a real win: explicit conflict semantics are preserved instead of conversational interpretation.
- why this matters: explicit conflict semantics are preserved instead of conversational interpretation.

**Case 3**

- prompt(s): `clear state` → `use podman instead of docker`
- base model: generic “how to switch to Podman” tutorial
- basic pipe: `No exact policy found for "docker". Replacement requires an exact policy match...`
- preprocessor pipe: same replacement clarify
- why this is a real win: replacement precondition (old item must exist) is enforced.
- why this matters: replacement precondition (old item must exist) is enforced.

**Case 4**

- prompt(s): `clear state` → `set premise to concise replies`
- base model: accepts conversational style phrasing
- basic pipe: `Did you mean 'set premise concise replies'?`
- preprocessor pipe: same clarify (near-miss is not rewritten)
- why this is a real win: preprocessor stays reject-first and preserves engine-owned clarify behavior.
- why this matters: preprocessor stays reject-first and preserves engine-owned clarify behavior.

**Case 5**

- prompt(s): `clear state` → `change premise concise replies`
- base model: generic “please clarify changes” response
- basic pipe: `Did you mean 'change premise to concise replies'?`
- preprocessor pipe: same clarify (near-miss is passed through unchanged)
- why this is a real win: near-miss inputs are not canonicalized, so directive semantics stay engine-owned.
- why this matters: near-miss inputs are not canonicalized, so directive semantics stay engine-owned.
22 changes: 22 additions & 0 deletions examples/integrations/openwebui/open_webui_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,25 @@ def _render_compiler_state_block(state: State) -> str:
return "\n".join(lines)


def _render_show_state_summary(engine: Engine) -> str:
premise = get_premise_value(engine.state)
use_items = sorted(get_policy_items(engine.state, "use"))
prohibit_items = sorted(get_policy_items(engine.state, "prohibit"))
pending = engine.has_pending_clarification()

use_text = ", ".join(use_items) if use_items else "none"
prohibit_text = ", ".join(prohibit_items) if prohibit_items else "none"
premise_text = premise if premise is not None else "none"
pending_text = "yes" if pending else "no"

return (
f"Premise: {premise_text}\n"
f"Use: {use_text}\n"
f"Prohibit: {prohibit_text}\n"
f"Pending clarification: {pending_text}"
)


def _replace_compiler_system_message(
messages: list[dict[str, Any]],
rendered_state_block: str,
Expand Down Expand Up @@ -571,6 +590,9 @@ async def pipe(
engine.import_checkpoint_json(checkpoint)
_ENGINES_BY_CHAT_KEY[chat_key] = engine

if latest_user_text.strip().lower() == "show state":
return _render_show_state_summary(engine)

state_before = engine.state
logger.debug("pipe: engine_input=%r", latest_user_text)
decision = engine.step(latest_user_text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,25 @@ def _render_compiler_state_block(state: State) -> str:
return "\n".join(lines)


def _render_show_state_summary(engine: Engine) -> str:
premise = get_premise_value(engine.state)
use_items = sorted(get_policy_items(engine.state, "use"))
prohibit_items = sorted(get_policy_items(engine.state, "prohibit"))
pending = engine.has_pending_clarification()

use_text = ", ".join(use_items) if use_items else "none"
prohibit_text = ", ".join(prohibit_items) if prohibit_items else "none"
premise_text = premise if premise is not None else "none"
pending_text = "yes" if pending else "no"

return (
f"Premise: {premise_text}\n"
f"Use: {use_text}\n"
f"Prohibit: {prohibit_text}\n"
f"Pending clarification: {pending_text}"
)


def _replace_compiler_system_message(
messages: list[dict[str, Any]],
rendered_state_block: str,
Expand Down Expand Up @@ -830,6 +849,9 @@ async def pipe(
engine.import_checkpoint_json(checkpoint)
_ENGINES_BY_CHAT_KEY[chat_key] = engine

if latest_user_text.strip().lower() == "show state":
return _render_show_state_summary(engine)

state_before = engine.state

preprocessd: str | None = None
Expand Down
99 changes: 99 additions & 0 deletions tests/test_openwebui_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,105 @@ async def _track_downstream(
assert len(forwarded_payloads) == 0


def test_pipe_show_state_returns_local_summary_and_no_downstream(monkeypatch) -> None:
module = _load_module_with_openwebui_stubs("owui_pipe_show_state", monkeypatch)
module._ENGINES_BY_CHAT_KEY.clear()
module._CHECKPOINTS_BY_CHAT_KEY.clear()

downstream_calls = 0

async def _track_downstream(
_: object, payload: dict[str, object], __: object
) -> dict[str, object]:
del payload
nonlocal downstream_calls
downstream_calls += 1
return {"choices": [{"message": {"content": "downstream"}}]}

module.generate_chat_completion = _track_downstream

pipe = module.Pipe()
pipe.valves.BASE_MODEL_ID = "base-model"
pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True
chat_id = "chat-show-state"

no_pending = asyncio.run(
pipe.pipe(
{"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]},
__user__={"id": "u1"},
__request__=object(),
__chat_id__=chat_id,
)
)
assert no_pending == ("Premise: none\nUse: none\nProhibit: none\nPending clarification: no")

assert downstream_calls == 0
assert "Context Compiler trace" not in no_pending


def test_pipe_show_state_reports_pending_yes(monkeypatch) -> None:
module = _load_module_with_openwebui_stubs("owui_pipe_show_state_pending", monkeypatch)
module._ENGINES_BY_CHAT_KEY.clear()
module._CHECKPOINTS_BY_CHAT_KEY.clear()

class _PendingEngine:
state = {"premise": None, "policies": {}, "version": 2}

def has_pending_clarification(self) -> bool:
return True

def step(self, _: str) -> dict[str, object]:
raise AssertionError("show state should not step engine")

monkeypatch.setattr(module, "create_engine", lambda: _PendingEngine())
pipe = module.Pipe()
pipe.valves.BASE_MODEL_ID = "base-model"

result = asyncio.run(
pipe.pipe(
{"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]},
__user__={"id": "u1"},
__request__=object(),
__chat_id__="chat-show-state-pending",
)
)
assert result == "Premise: none\nUse: none\nProhibit: none\nPending clarification: yes"


def test_pipe_show_state_non_exact_routes_normally(monkeypatch) -> None:
module = _load_module_with_openwebui_stubs("owui_pipe_show_state_non_exact", monkeypatch)
module._ENGINES_BY_CHAT_KEY.clear()
module._CHECKPOINTS_BY_CHAT_KEY.clear()

downstream_calls = 0

async def _track_downstream(
_: object, payload: dict[str, object], __: object
) -> dict[str, object]:
del payload
nonlocal downstream_calls
downstream_calls += 1
return {"choices": [{"message": {"content": "downstream"}}]}

module.generate_chat_completion = _track_downstream
pipe = module.Pipe()
pipe.valves.BASE_MODEL_ID = "base-model"

result = asyncio.run(
pipe.pipe(
{
"model": "pipe-model",
"messages": [{"role": "user", "content": "show state please"}],
},
__user__={"id": "u1"},
__request__=object(),
__chat_id__="chat-show-state-non-exact",
)
)
assert result == {"choices": [{"message": {"content": "downstream"}}]}
assert downstream_calls == 1


def test_pipe_near_miss_directives_return_deterministic_clarify_without_downstream(
monkeypatch,
) -> None:
Expand Down
116 changes: 116 additions & 0 deletions tests/test_openwebui_preprocessor_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,122 @@ async def _track_downstream(
assert len(forwarded_payloads) == 0


def test_preprocessor_pipe_show_state_returns_local_summary_and_bypasses_preprocess_and_model(
monkeypatch,
) -> None:
module = _load_module_with_openwebui_stubs("owui_preproc_show_state", monkeypatch)
module._ENGINES_BY_CHAT_KEY.clear()
module._CHECKPOINTS_BY_CHAT_KEY.clear()

downstream_calls = 0
preprocess_calls = 0

async def _track_downstream(
_: object, payload: dict[str, object], __: object
) -> dict[str, object]:
del payload
nonlocal downstream_calls
downstream_calls += 1
return {"choices": [{"message": {"content": "downstream"}}]}

async def _track_preprocess(
self, *args: object, **kwargs: object
) -> tuple[str | None, str | None]:
del self, args, kwargs
nonlocal preprocess_calls
preprocess_calls += 1
return None, None

monkeypatch.setattr(module, "generate_chat_completion", _track_downstream)
monkeypatch.setattr(module.Pipe, "_preprocess_user_input", _track_preprocess)

pipe = module.Pipe()
pipe.valves.BASE_MODEL_ID = "base-model"
pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model"
pipe.valves.SHOW_CONTEXT_COMPILER_TRACE = True
chat_id = "chat-preproc-show-state"

no_pending = asyncio.run(
pipe.pipe(
{"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]},
__user__={"id": "u1"},
__request__=object(),
__chat_id__=chat_id,
)
)
assert no_pending == ("Premise: none\nUse: none\nProhibit: none\nPending clarification: no")

assert downstream_calls == 0
assert preprocess_calls == 0
assert "Context Compiler trace" not in no_pending


def test_preprocessor_pipe_show_state_reports_pending_yes(monkeypatch) -> None:
module = _load_module_with_openwebui_stubs("owui_preproc_show_state_pending", monkeypatch)
module._ENGINES_BY_CHAT_KEY.clear()
module._CHECKPOINTS_BY_CHAT_KEY.clear()

class _PendingEngine:
state = {"premise": None, "policies": {}, "version": 2}

def has_pending_clarification(self) -> bool:
return True

def step(self, _: str) -> dict[str, object]:
raise AssertionError("show state should not step engine")

monkeypatch.setattr(module, "create_engine", lambda: _PendingEngine())
pipe = module.Pipe()
pipe.valves.BASE_MODEL_ID = "base-model"
pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model"

result = asyncio.run(
pipe.pipe(
{"model": "pipe-model", "messages": [{"role": "user", "content": "show state"}]},
__user__={"id": "u1"},
__request__=object(),
__chat_id__="chat-preproc-show-state-pending",
)
)
assert result == "Premise: none\nUse: none\nProhibit: none\nPending clarification: yes"


def test_preprocessor_pipe_show_state_non_exact_routes_normally(monkeypatch) -> None:
module = _load_module_with_openwebui_stubs("owui_preproc_show_state_non_exact", monkeypatch)
module._ENGINES_BY_CHAT_KEY.clear()
module._CHECKPOINTS_BY_CHAT_KEY.clear()

downstream_calls = 0

async def _track_downstream(
_: object, payload: dict[str, object], __: object
) -> dict[str, object]:
del payload
nonlocal downstream_calls
downstream_calls += 1
return {"choices": [{"message": {"content": "downstream"}}]}

monkeypatch.setattr(module, "generate_chat_completion", _track_downstream)

pipe = module.Pipe()
pipe.valves.BASE_MODEL_ID = "base-model"
pipe.valves.PREPROCESSOR_MODEL_ID = "prep-model"

result = asyncio.run(
pipe.pipe(
{
"model": "pipe-model",
"messages": [{"role": "user", "content": "show state please"}],
},
__user__={"id": "u1"},
__request__=object(),
__chat_id__="chat-preproc-show-state-non-exact",
)
)
assert result == {"choices": [{"message": {"content": "downstream"}}]}
assert downstream_calls >= 1


@pytest.mark.parametrize(
("confirmation",),
[
Expand Down