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
8 changes: 4 additions & 4 deletions examples/integrations/openwebui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ These examples support both sync (`0.8.x`) and async (`0.9.x`) user lookup.
The minimal pipe path below is the easiest first-run flow and was runtime-validated in Docker via API flow with a real backend model.

1. Import `open_webui_pipe.py` (recommended/default) as a Function by URL.
2. Open WebUI installs `context-compiler>=0.7.0` from the function frontmatter requirements.
2. Open WebUI installs `context-compiler>=0.7.2` from the function frontmatter requirements.
3. Enable the function.
4. Set `BASE_MODEL_ID` to a valid Open WebUI model id (required).
5. Select the pipe model in chat.
Expand All @@ -27,7 +27,7 @@ Open WebUI is a separate runtime and must already be installed/configured separa
Open WebUI also needs at least one real backend model/provider configured (for example Ollama or OpenAI) so `BASE_MODEL_ID` resolves to an actual model.
Note: The `PROVIDER` environment contract used in LiteLLM examples/demos does not apply to OpenWebUI. OpenWebUI manages providers via its own connection settings and model IDs.

Checkpoint continuation in these examples requires `context-compiler>=0.7.0`.
Checkpoint continuation in these examples requires `context-compiler>=0.7.2`.

### Model configuration

Expand Down Expand Up @@ -64,8 +64,8 @@ If frontmatter dependency installs are disabled, offline, or unavailable:
1. Open a shell in the Open WebUI container:
- `docker exec -it <openwebui-container> sh`
2. Install the package manually:
- Minimal pipe: `pip install "context-compiler>=0.7.0"`
- Preprocessor pipe: `pip install "context-compiler[experimental]>=0.7.0"`
- Minimal pipe: `pip install "context-compiler>=0.7.2"`
- Preprocessor pipe: `pip install "context-compiler[experimental]>=0.7.2"`
3. Import and enable the function in Open WebUI, then configure valves.

### Finding valid model ids
Expand Down
66 changes: 44 additions & 22 deletions examples/integrations/openwebui/open_webui_pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
author: rlippmann
author_url: https://github.com/rlippmann/context-compiler
funding_url: https://github.com/rlippmann/context-compiler
version: 0.9.0
requirements: context-compiler>=0.7.0
version: 0.9.1
requirements: context-compiler>=0.7.2

Minimal Open WebUI Pipe integration for Context Compiler.

Expand Down Expand Up @@ -193,6 +193,12 @@ def _normalize_state(value: object) -> State:
return {"premise": None, "policies": {}, "version": 2}


def _has_non_empty_authoritative_state(state: State) -> bool:
if get_premise_value(state) is not None:
return True
return bool(get_policy_items(state, "use") or get_policy_items(state, "prohibit"))


def _build_compact_trace_text(
*,
decision: object,
Expand Down Expand Up @@ -229,6 +235,25 @@ def _strip_trace_blocks_from_messages(messages: list[dict[str, Any]]) -> list[di
return cleaned


def _build_forward_messages(
raw_messages: object,
*,
state: State | None = None,
) -> list[dict[str, Any]]:
"""Build forwarded messages with trace stripping and optional state injection."""
messages = (
_strip_trace_blocks_from_messages([msg for msg in raw_messages if isinstance(msg, dict)])
if isinstance(raw_messages, list)
else []
)
if state is not None and _has_non_empty_authoritative_state(state):
return _replace_compiler_system_message(
messages,
_render_compiler_state_block(state),
)
return messages


def _strip_existing_trace_from_chunk(chunk: object) -> object:
if isinstance(chunk, str):
return _strip_trace_block_from_text(chunk)
Expand Down Expand Up @@ -475,15 +500,13 @@ async def _forward_passthrough(
body: dict[str, Any],
user_payload: dict[str, Any],
request: Request,
*,
state: State | None = None,
) -> Any:
"""Forward with a shallow body copy and model override only."""
"""Forward with model override and optional compiler-owned state injection."""
payload = {**body}
payload["model"] = self.valves.BASE_MODEL_ID
raw_messages = body.get("messages")
if isinstance(raw_messages, list):
payload["messages"] = _strip_trace_blocks_from_messages(
[msg for msg in raw_messages if isinstance(msg, dict)]
)
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)
user = Users.get_user_by_id(user_payload["id"])
if inspect.isawaitable(user):
user = await user
Expand Down Expand Up @@ -514,18 +537,7 @@ async def _forward_update(
payload = {**body}
payload["model"] = self.valves.BASE_MODEL_ID

raw_messages = body.get("messages")
messages = (
_strip_trace_blocks_from_messages(
[msg for msg in raw_messages if isinstance(msg, dict)]
)
if isinstance(raw_messages, list)
else []
)
payload["messages"] = _replace_compiler_system_message(
messages,
_render_compiler_state_block(state),
)
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)

user = Users.get_user_by_id(user_payload["id"])
if inspect.isawaitable(user):
Expand Down Expand Up @@ -625,7 +637,11 @@ async def pipe(
llm_called=False,
)
if kind == DECISION_PASSTHROUGH:
response = await self._forward_passthrough(body, __user__, __request__)
compiled_state = _normalize_state(state_after)
state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
response = await self._forward_passthrough(
body, __user__, __request__, state=compiled_state
)
return self._with_trace(
response,
original_input=latest_user_text,
Expand All @@ -634,6 +650,7 @@ async def pipe(
state_before=state_before,
state_after=state_after,
llm_called=True,
state_injected=state_injected,
)
if kind == DECISION_UPDATE:
_CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json()
Expand All @@ -647,7 +664,11 @@ async def pipe(
llm_called=False,
)

response = await self._forward_passthrough(body, __user__, __request__)
compiled_state = _normalize_state(state_after)
state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
response = await self._forward_passthrough(
body, __user__, __request__, state=compiled_state
)
return self._with_trace(
response,
original_input=latest_user_text,
Expand All @@ -656,4 +677,5 @@ async def pipe(
state_before=state_before,
state_after=state_after,
llm_called=True,
state_injected=state_injected,
)
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""
title: Context Compiler Preprocessor Pipe
title: Context Compiler Pipe (Preprocessor)
author: rlippmann
author_url: https://github.com/rlippmann/context-compiler
funding_url: https://github.com/rlippmann/context-compiler
version: 0.9.0
requirements: context-compiler[experimental]>=0.7.0
version: 0.9.1
requirements: context-compiler[experimental]>=0.7.2

Open WebUI integration with Context Compiler preprocessor.

Expand Down Expand Up @@ -198,6 +198,12 @@ def _normalize_state(value: object) -> State:
return {"premise": None, "policies": {}, "version": 2}


def _has_non_empty_authoritative_state(state: State) -> bool:
if get_premise_value(state) is not None:
return True
return bool(get_policy_items(state, "use") or get_policy_items(state, "prohibit"))


def _build_compact_trace_text(
*,
decision: object,
Expand Down Expand Up @@ -234,6 +240,25 @@ def _strip_trace_blocks_from_messages(messages: list[dict[str, Any]]) -> list[di
return cleaned


def _build_forward_messages(
raw_messages: object,
*,
state: State | None = None,
) -> list[dict[str, Any]]:
"""Build forwarded messages with trace stripping and optional state injection."""
messages = (
_strip_trace_blocks_from_messages([msg for msg in raw_messages if isinstance(msg, dict)])
if isinstance(raw_messages, list)
else []
)
if state is not None and _has_non_empty_authoritative_state(state):
return _replace_compiler_system_message(
messages,
_render_compiler_state_block(state),
)
return messages


def _strip_existing_trace_from_chunk(chunk: object) -> object:
if isinstance(chunk, str):
return _strip_trace_block_from_text(chunk)
Expand Down Expand Up @@ -693,6 +718,7 @@ async def _forward_passthrough(
request: Request,
*,
base_model_id: str | None,
state: State | None = None,
) -> Any:
if base_model_id is None:
if self._allow_missing_base_model_for_debug():
Expand All @@ -706,11 +732,7 @@ async def _forward_passthrough(
)
payload = {**body}
payload["model"] = base_model_id
raw_messages = body.get("messages")
if isinstance(raw_messages, list):
payload["messages"] = _strip_trace_blocks_from_messages(
[msg for msg in raw_messages if isinstance(msg, dict)]
)
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)
user = Users.get_user_by_id(user_payload["id"])
if inspect.isawaitable(user):
user = await user
Expand Down Expand Up @@ -748,18 +770,7 @@ async def _forward_update(
payload = {**body}
payload["model"] = base_model_id

raw_messages = body.get("messages")
messages = (
_strip_trace_blocks_from_messages(
[msg for msg in raw_messages if isinstance(msg, dict)]
)
if isinstance(raw_messages, list)
else []
)
payload["messages"] = _replace_compiler_system_message(
messages,
_render_compiler_state_block(state),
)
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)

user = Users.get_user_by_id(user_payload["id"])
if inspect.isawaitable(user):
Expand Down Expand Up @@ -906,11 +917,14 @@ async def pipe(
llm_called=False,
)
if kind == DECISION_PASSTHROUGH:
compiled_state = _normalize_state(state_after)
state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
response = await self._forward_passthrough(
body,
__user__,
__request__,
base_model_id=base_model_id,
state=compiled_state,
)
return self._with_trace(
response,
Expand All @@ -921,6 +935,7 @@ async def pipe(
state_after=state_after,
preprocessor_output=preprocessd,
llm_called=base_model_id is not None,
state_injected=state_injected,
)
if kind == DECISION_UPDATE:
_CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json()
Expand All @@ -935,11 +950,14 @@ async def pipe(
llm_called=False,
)

compiled_state = _normalize_state(state_after)
state_injected = "yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
response = await self._forward_passthrough(
body,
__user__,
__request__,
base_model_id=base_model_id,
state=compiled_state,
)
return self._with_trace(
response,
Expand All @@ -950,4 +968,5 @@ async def pipe(
state_after=state_after,
preprocessor_output=preprocessd,
llm_called=base_model_id is not None,
state_injected=state_injected,
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "context-compiler"
version = "0.7.1"
version = "0.7.2"
description = "Deterministic conversational state engine for LLM applications."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion src/context_compiler/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def build_compact_trace_text(

lines.append(f"active state: {_active_state_summary(state_after)}")
lines.append(f"downstream LLM call: {'yes' if llm_called else 'no'}")
lines.append("state injected: no")
lines.append(f"state injected: {state_injected}")
return "\n".join(lines)


Expand Down
Loading
Loading