context-compiler-example-integrations 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- context_compiler_example_integrations/README.md +71 -0
- context_compiler_example_integrations/__init__.py +1 -0
- context_compiler_example_integrations/examples/__init__.py +12 -0
- context_compiler_example_integrations/examples/_shared/__init__.py +1 -0
- context_compiler_example_integrations/examples/_shared/litellm_request.py +61 -0
- context_compiler_example_integrations/examples/_shared/provider_mode.py +91 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/README.md +77 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/example.py +169 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/README.md +85 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/app.py +205 -0
- context_compiler_example_integrations/examples/execution_authorization/README.md +31 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/README.md +77 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/__init__.py +1 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/example.py +168 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/README.md +97 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/__init__.py +1 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/app.py +312 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/model_approval.py +98 -0
- context_compiler_example_integrations/examples/gateway_middleware/README.md +49 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/README.md +40 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/__init__.py +1 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/example.py +220 -0
- context_compiler_example_integrations/examples/prompt_construction/README.md +27 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/README.md +197 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/basic.py +392 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/confirmation_helper.py +91 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/with_directive_drafter.py +524 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/README.md +92 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/__init__.py +1 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/example.py +205 -0
- context_compiler_example_integrations/examples/retrieval_filtering/README.md +65 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/README.md +120 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/__init__.py +1 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/example.py +320 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/README.md +123 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/__init__.py +1 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/example.py +300 -0
- context_compiler_example_integrations/examples/schema_selection/README.md +41 -0
- context_compiler_example_integrations/examples/schema_selection/litellm_response_format/response_format.py +218 -0
- context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/README.md +60 -0
- context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/example.py +171 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/README.md +95 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/__init__.py +1 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/example.py +180 -0
- context_compiler_example_integrations/examples/tool_gating/README.md +55 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/README.md +34 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/__init__.py +1 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/example.py +195 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/README.md +93 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/__init__.py +1 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/example.py +250 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/live_model.py +369 -0
- context_compiler_example_integrations/reference_integrations/__init__.py +12 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/README.md +251 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/_checkpoint_support.py +146 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/config.example.yaml +23 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook.py +163 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +289 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/README.md +233 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe.py +750 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +1045 -0
- context_compiler_example_integrations-0.1.0.dist-info/METADATA +110 -0
- context_compiler_example_integrations-0.1.0.dist-info/RECORD +65 -0
- context_compiler_example_integrations-0.1.0.dist-info/WHEEL +4 -0
- context_compiler_example_integrations-0.1.0.dist-info/licenses/LICENSE +173 -0
context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe.py
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
"""
|
|
2
|
+
title: Context Compiler Open WebUI Pipe
|
|
3
|
+
author: rlippmann
|
|
4
|
+
author_url: https://github.com/rlippmann/context-compiler-example-integrations
|
|
5
|
+
version: 0.9.4
|
|
6
|
+
requirements: context-compiler>=0.8.3
|
|
7
|
+
|
|
8
|
+
Minimal Open WebUI Pipe integration for Context Compiler.
|
|
9
|
+
|
|
10
|
+
This integration demonstrates mapping Context Compiler `Decision` output into
|
|
11
|
+
Open WebUI request flow.
|
|
12
|
+
|
|
13
|
+
Scope is intentionally limited:
|
|
14
|
+
- Single Pipe Function for Open WebUI 0.8.x and 0.9.x.
|
|
15
|
+
- In-memory per-process engine map keyed by chat key.
|
|
16
|
+
- No persistence, no multi-worker coordination, no external storage.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import inspect
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import AsyncIterator
|
|
24
|
+
from typing import Any, cast
|
|
25
|
+
|
|
26
|
+
from fastapi import Request # type: ignore[import-not-found]
|
|
27
|
+
from open_webui.models.users import Users # type: ignore[import-not-found]
|
|
28
|
+
from open_webui.utils.chat import generate_chat_completion # type: ignore[import-not-found]
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from pydantic import BaseModel, Field
|
|
32
|
+
except ModuleNotFoundError:
|
|
33
|
+
# Keep this import optional: CI/tests run without integration extras.
|
|
34
|
+
# These lightweight fallbacks keep import-time behavior deterministic so
|
|
35
|
+
# coverage exercises the pipe module without pydantic installed.
|
|
36
|
+
class BaseModel: # type: ignore[no-redef]
|
|
37
|
+
def __init__(self, **kwargs: object) -> None:
|
|
38
|
+
for key, value in kwargs.items():
|
|
39
|
+
setattr(self, key, value)
|
|
40
|
+
|
|
41
|
+
def Field(*, default: Any, description: str = "") -> Any: # type: ignore[no-redef]
|
|
42
|
+
del description
|
|
43
|
+
return default
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
from context_compiler import (
|
|
47
|
+
DECISION_CLARIFY,
|
|
48
|
+
DECISION_PASSTHROUGH,
|
|
49
|
+
DECISION_UPDATE,
|
|
50
|
+
POLICY_PROHIBIT,
|
|
51
|
+
POLICY_USE,
|
|
52
|
+
State,
|
|
53
|
+
create_engine,
|
|
54
|
+
get_clarify_prompt,
|
|
55
|
+
get_decision_state,
|
|
56
|
+
get_policy_items,
|
|
57
|
+
get_premise_value,
|
|
58
|
+
is_clarify,
|
|
59
|
+
is_passthrough,
|
|
60
|
+
is_update,
|
|
61
|
+
state_diff,
|
|
62
|
+
)
|
|
63
|
+
from context_compiler.engine import Engine
|
|
64
|
+
|
|
65
|
+
logger = logging.getLogger(__name__)
|
|
66
|
+
|
|
67
|
+
_CC_MARKER = "[[cc_state]]"
|
|
68
|
+
_ENGINES_BY_CHAT_KEY: dict[str, Engine] = {}
|
|
69
|
+
# Example-only in-memory checkpoint store.
|
|
70
|
+
# This keeps continuation state only for the current process lifetime.
|
|
71
|
+
# Real deployments should persist checkpoints externally (DB/Redis/etc.),
|
|
72
|
+
# or restart continuity for pending flows will be lost.
|
|
73
|
+
_CHECKPOINTS_BY_CHAT_KEY: dict[str, str] = {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolve_chat_key(
|
|
77
|
+
user: dict[str, Any],
|
|
78
|
+
chat_id: str | None,
|
|
79
|
+
metadata: dict[str, Any] | None,
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Resolve chat key from reserved args with a minimal fallback.
|
|
82
|
+
|
|
83
|
+
Resolution order:
|
|
84
|
+
1. ``__chat_id__``
|
|
85
|
+
2. ``__metadata__["chat_id"]``
|
|
86
|
+
3. ``no-chat-id:<user_id>``
|
|
87
|
+
|
|
88
|
+
The fallback key is a degraded convenience for this minimal integration and
|
|
89
|
+
is not a strong chat-isolation guarantee.
|
|
90
|
+
"""
|
|
91
|
+
if chat_id:
|
|
92
|
+
return chat_id
|
|
93
|
+
if isinstance(metadata, dict):
|
|
94
|
+
metadata_chat_id = metadata.get("chat_id")
|
|
95
|
+
if isinstance(metadata_chat_id, str) and metadata_chat_id:
|
|
96
|
+
return metadata_chat_id
|
|
97
|
+
user_id = str(user["id"])
|
|
98
|
+
return f"no-chat-id:{user_id}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _extract_latest_user_text(messages: list[dict[str, Any]]) -> str | None:
|
|
102
|
+
"""Return latest plain-text user content, scanning from the end.
|
|
103
|
+
|
|
104
|
+
Uses the last message with ``role == "user"``. Only plain string content is
|
|
105
|
+
eligible for compilation. Non-text or missing-user cases return ``None`` so
|
|
106
|
+
the caller can bypass compiler behavior.
|
|
107
|
+
"""
|
|
108
|
+
for message in reversed(messages):
|
|
109
|
+
if message.get("role") != "user":
|
|
110
|
+
continue
|
|
111
|
+
content = message.get("content")
|
|
112
|
+
if isinstance(content, str):
|
|
113
|
+
return content
|
|
114
|
+
return None
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _render_compiler_state_block(state: State) -> str:
|
|
119
|
+
"""Render deterministic compiler-owned state block text.
|
|
120
|
+
|
|
121
|
+
The first line is ``[[cc_state]]``. Optional lines follow for ``Premise``,
|
|
122
|
+
``Use``, and ``Prohibit``. Policy items are rendered alphabetically, and
|
|
123
|
+
identical state must produce identical output bytes.
|
|
124
|
+
"""
|
|
125
|
+
lines: list[str] = [_CC_MARKER]
|
|
126
|
+
|
|
127
|
+
premise = get_premise_value(state)
|
|
128
|
+
if premise is not None:
|
|
129
|
+
lines.append(f"Premise: {premise}")
|
|
130
|
+
|
|
131
|
+
use_items = sorted(get_policy_items(state, POLICY_USE))
|
|
132
|
+
if use_items:
|
|
133
|
+
lines.append("Use: " + ", ".join(use_items))
|
|
134
|
+
|
|
135
|
+
prohibit_items = sorted(get_policy_items(state, POLICY_PROHIBIT))
|
|
136
|
+
if prohibit_items:
|
|
137
|
+
lines.append("Prohibit: " + ", ".join(prohibit_items))
|
|
138
|
+
|
|
139
|
+
return "\n".join(lines)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _render_show_state_summary(engine: Engine) -> str:
|
|
143
|
+
premise = get_premise_value(engine.state)
|
|
144
|
+
use_items = sorted(get_policy_items(engine.state, POLICY_USE))
|
|
145
|
+
prohibit_items = sorted(get_policy_items(engine.state, POLICY_PROHIBIT))
|
|
146
|
+
pending = engine.has_pending_clarification()
|
|
147
|
+
|
|
148
|
+
use_text = ", ".join(use_items) if use_items else "none"
|
|
149
|
+
prohibit_text = ", ".join(prohibit_items) if prohibit_items else "none"
|
|
150
|
+
premise_text = premise if premise is not None else "none"
|
|
151
|
+
pending_text = "yes" if pending else "no"
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
f"Premise: {premise_text}\n"
|
|
155
|
+
f"Use: {use_text}\n"
|
|
156
|
+
f"Prohibit: {prohibit_text}\n"
|
|
157
|
+
f"Pending clarification: {pending_text}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _replace_compiler_system_message(
|
|
162
|
+
messages: list[dict[str, Any]],
|
|
163
|
+
rendered_state_block: str,
|
|
164
|
+
) -> list[dict[str, Any]]:
|
|
165
|
+
"""Replace compiler-owned state messages while preserving other order.
|
|
166
|
+
|
|
167
|
+
Compiler-owned messages are identified by ``[[cc_state]]`` prefix. Existing
|
|
168
|
+
compiler-owned system messages are removed, and one fresh compiler-owned
|
|
169
|
+
system message is inserted after the last remaining system message, else at
|
|
170
|
+
index ``0``. Relative order of non-compiler messages is preserved.
|
|
171
|
+
|
|
172
|
+
Invariant: exactly one compiler-owned state message exists afterward.
|
|
173
|
+
"""
|
|
174
|
+
filtered_messages: list[dict[str, Any]] = []
|
|
175
|
+
last_system_index = -1
|
|
176
|
+
|
|
177
|
+
for message in messages:
|
|
178
|
+
role = message.get("role")
|
|
179
|
+
content = message.get("content")
|
|
180
|
+
if (
|
|
181
|
+
role == "system"
|
|
182
|
+
and isinstance(content, str)
|
|
183
|
+
and content.startswith(_CC_MARKER)
|
|
184
|
+
):
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
filtered_messages.append(message)
|
|
188
|
+
if role == "system":
|
|
189
|
+
last_system_index = len(filtered_messages) - 1
|
|
190
|
+
|
|
191
|
+
insert_at = last_system_index + 1 if last_system_index >= 0 else 0
|
|
192
|
+
compiler_message: dict[str, Any] = {
|
|
193
|
+
"role": "system",
|
|
194
|
+
"content": rendered_state_block,
|
|
195
|
+
}
|
|
196
|
+
return [
|
|
197
|
+
*filtered_messages[:insert_at],
|
|
198
|
+
compiler_message,
|
|
199
|
+
*filtered_messages[insert_at:],
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _normalize_state(value: object) -> State:
|
|
204
|
+
if isinstance(value, dict):
|
|
205
|
+
return cast(State, value)
|
|
206
|
+
return {"premise": None, "policies": {}, "version": 2}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _has_non_empty_authoritative_state(state: State) -> bool:
|
|
210
|
+
if get_premise_value(state) is not None:
|
|
211
|
+
return True
|
|
212
|
+
return bool(
|
|
213
|
+
get_policy_items(state, POLICY_USE) or get_policy_items(state, POLICY_PROHIBIT)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _render_state_summary_line(state: object) -> str:
|
|
218
|
+
if not isinstance(state, dict):
|
|
219
|
+
return "unavailable"
|
|
220
|
+
typed_state = cast(State, state)
|
|
221
|
+
|
|
222
|
+
premise = get_premise_value(typed_state)
|
|
223
|
+
use_items = sorted(get_policy_items(typed_state, POLICY_USE))
|
|
224
|
+
prohibit_items = sorted(get_policy_items(typed_state, POLICY_PROHIBIT))
|
|
225
|
+
return (
|
|
226
|
+
f"premise={premise if premise is not None else '(none)'}; "
|
|
227
|
+
f"use={', '.join(use_items) if use_items else '(none)'}; "
|
|
228
|
+
f"prohibit={', '.join(prohibit_items) if prohibit_items else '(none)'}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _build_compact_trace_text(
|
|
233
|
+
*,
|
|
234
|
+
decision: object,
|
|
235
|
+
state_before: object,
|
|
236
|
+
state_after: object,
|
|
237
|
+
llm_called: bool,
|
|
238
|
+
state_injected: str,
|
|
239
|
+
) -> str:
|
|
240
|
+
kind = decision.get("kind", "unknown") if isinstance(decision, dict) else "unknown"
|
|
241
|
+
changed = "unknown"
|
|
242
|
+
if isinstance(state_before, dict) and isinstance(state_after, dict):
|
|
243
|
+
changed = (
|
|
244
|
+
"yes"
|
|
245
|
+
if state_diff(cast(State, state_before), cast(State, state_after))[
|
|
246
|
+
"changed"
|
|
247
|
+
]
|
|
248
|
+
else "no"
|
|
249
|
+
)
|
|
250
|
+
return "\n".join(
|
|
251
|
+
[
|
|
252
|
+
"Context Compiler trace",
|
|
253
|
+
f"- decision: {kind}",
|
|
254
|
+
f"- llm_called: {'yes' if llm_called else 'no'}",
|
|
255
|
+
f"- state_changed: {changed}",
|
|
256
|
+
f"- state_injected: {state_injected}",
|
|
257
|
+
f"- state_before: {_render_state_summary_line(state_before)}",
|
|
258
|
+
f"- state_after: {_render_state_summary_line(state_after)}",
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _strip_trace_block_from_text(content: str) -> str:
|
|
264
|
+
marker = "Context Compiler trace"
|
|
265
|
+
index = content.find(marker)
|
|
266
|
+
if index < 0:
|
|
267
|
+
return content
|
|
268
|
+
return content[:index].rstrip()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _strip_trace_blocks_from_messages(
|
|
272
|
+
messages: list[dict[str, Any]],
|
|
273
|
+
) -> list[dict[str, Any]]:
|
|
274
|
+
cleaned: list[dict[str, Any]] = []
|
|
275
|
+
for message in messages:
|
|
276
|
+
msg = dict(message)
|
|
277
|
+
content = msg.get("content")
|
|
278
|
+
if isinstance(content, str):
|
|
279
|
+
msg["content"] = _strip_trace_block_from_text(content)
|
|
280
|
+
cleaned.append(msg)
|
|
281
|
+
return cleaned
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _build_forward_messages(
|
|
285
|
+
raw_messages: object,
|
|
286
|
+
*,
|
|
287
|
+
state: State | None = None,
|
|
288
|
+
) -> list[dict[str, Any]]:
|
|
289
|
+
"""Build forwarded messages with trace stripping and optional state injection."""
|
|
290
|
+
messages = (
|
|
291
|
+
_strip_trace_blocks_from_messages(
|
|
292
|
+
[msg for msg in raw_messages if isinstance(msg, dict)]
|
|
293
|
+
)
|
|
294
|
+
if isinstance(raw_messages, list)
|
|
295
|
+
else []
|
|
296
|
+
)
|
|
297
|
+
if state is not None and _has_non_empty_authoritative_state(state):
|
|
298
|
+
return _replace_compiler_system_message(
|
|
299
|
+
messages,
|
|
300
|
+
_render_compiler_state_block(state),
|
|
301
|
+
)
|
|
302
|
+
return messages
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _strip_existing_trace_from_chunk(chunk: object) -> object:
|
|
306
|
+
if isinstance(chunk, str):
|
|
307
|
+
return _strip_trace_block_from_text(chunk)
|
|
308
|
+
if isinstance(chunk, bytes):
|
|
309
|
+
decoded = chunk.decode("utf-8", errors="ignore")
|
|
310
|
+
cleaned = _strip_trace_block_from_text(decoded)
|
|
311
|
+
return cleaned.encode("utf-8")
|
|
312
|
+
return chunk
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _render_item_label(value: str) -> str:
|
|
316
|
+
return re.sub(r"\s+", " ", value).strip().lower()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _near_miss_directive_clarify(value: str) -> str | None:
|
|
320
|
+
normalized = re.sub(r"\s+", " ", value.strip())
|
|
321
|
+
lower = normalized.lower()
|
|
322
|
+
|
|
323
|
+
if lower in {"reset premise", "reset premises", "clear premises"}:
|
|
324
|
+
return "Unknown directive.\nUse 'clear premise' or 'reset policies'."
|
|
325
|
+
if lower.startswith("set premise to "):
|
|
326
|
+
return "Invalid premise syntax.\nUse 'set premise <value>'."
|
|
327
|
+
if lower.startswith("change premise ") and not lower.startswith(
|
|
328
|
+
"change premise to "
|
|
329
|
+
):
|
|
330
|
+
return "Invalid premise syntax.\nUse 'change premise to <value>'."
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _summarize_update_from_input(user_input: str) -> str:
|
|
335
|
+
normalized = re.sub(r"\s+", " ", user_input.strip())
|
|
336
|
+
lower = normalized.lower()
|
|
337
|
+
|
|
338
|
+
if lower == "clear state":
|
|
339
|
+
return "State cleared."
|
|
340
|
+
if lower == "clear premise":
|
|
341
|
+
return "Premise cleared."
|
|
342
|
+
if lower == "reset policies":
|
|
343
|
+
return "Policies reset."
|
|
344
|
+
|
|
345
|
+
replacement_match = re.match(
|
|
346
|
+
r"^use\s+(.+?)\s+instead\s+of\s+(.+)$", normalized, flags=re.IGNORECASE
|
|
347
|
+
)
|
|
348
|
+
if replacement_match is not None:
|
|
349
|
+
item = _render_item_label(replacement_match.group(1).rstrip(" .!?"))
|
|
350
|
+
if item:
|
|
351
|
+
return f"State updated: Use {item}."
|
|
352
|
+
|
|
353
|
+
use_match = re.match(r"^use\s+(.+)$", normalized, flags=re.IGNORECASE)
|
|
354
|
+
if use_match is not None:
|
|
355
|
+
item = _render_item_label(use_match.group(1).rstrip(" .!?"))
|
|
356
|
+
if item:
|
|
357
|
+
return f"State updated: Use {item}."
|
|
358
|
+
|
|
359
|
+
prohibit_match = re.match(r"^prohibit\s+(.+)$", normalized, flags=re.IGNORECASE)
|
|
360
|
+
if prohibit_match is not None:
|
|
361
|
+
item = _render_item_label(prohibit_match.group(1).rstrip(" .!?"))
|
|
362
|
+
if item:
|
|
363
|
+
return f"State updated: Prohibit {item}."
|
|
364
|
+
|
|
365
|
+
remove_policy_match = re.match(
|
|
366
|
+
r"^remove\s+policy\s+(.+)$", normalized, flags=re.IGNORECASE
|
|
367
|
+
)
|
|
368
|
+
if remove_policy_match is not None:
|
|
369
|
+
item = _render_item_label(remove_policy_match.group(1).rstrip(" .!?"))
|
|
370
|
+
if item:
|
|
371
|
+
return f"State updated: Removed policy {item}."
|
|
372
|
+
|
|
373
|
+
return "State updated."
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _is_administrative_update_input(user_input: str) -> bool:
|
|
377
|
+
normalized = re.sub(r"\s+", " ", user_input.strip()).lower()
|
|
378
|
+
return (
|
|
379
|
+
normalized == "clear state"
|
|
380
|
+
or normalized == "clear premise"
|
|
381
|
+
or normalized == "reset policies"
|
|
382
|
+
or normalized.startswith("remove policy ")
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class Pipe:
|
|
387
|
+
"""Map Context Compiler decisions into Open WebUI pipe behavior.
|
|
388
|
+
|
|
389
|
+
- ``clarify`` returns plain text and skips model forwarding.
|
|
390
|
+
- ``passthrough`` forwards with minimal mutation.
|
|
391
|
+
- ``update`` returns deterministic local acknowledgement (no model call).
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
class Valves(BaseModel):
|
|
395
|
+
BASE_MODEL_ID: str = Field(
|
|
396
|
+
default="",
|
|
397
|
+
description=(
|
|
398
|
+
"Required Open WebUI model id used for forwarding. Must exactly match a "
|
|
399
|
+
"configured model id in Open WebUI (not arbitrary text), for example: "
|
|
400
|
+
"llama3.1:8b."
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
SHOW_CONTEXT_COMPILER_TRACE: bool = Field(
|
|
404
|
+
default=False,
|
|
405
|
+
description="Include concise Context Compiler trace text in responses.",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def __init__(self) -> None:
|
|
409
|
+
self.valves = self.Valves()
|
|
410
|
+
|
|
411
|
+
def _is_model_not_found_text(self, value: object) -> bool:
|
|
412
|
+
if not isinstance(value, str):
|
|
413
|
+
return False
|
|
414
|
+
return "model not found" in value.lower()
|
|
415
|
+
|
|
416
|
+
def _contains_model_not_found(self, value: object) -> bool:
|
|
417
|
+
if self._is_model_not_found_text(value):
|
|
418
|
+
return True
|
|
419
|
+
if isinstance(value, dict):
|
|
420
|
+
return any(self._contains_model_not_found(v) for v in value.values())
|
|
421
|
+
if isinstance(value, list):
|
|
422
|
+
return any(self._contains_model_not_found(v) for v in value)
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
def _normalize_forward_error(self, response: Any) -> str | None:
|
|
426
|
+
if self._contains_model_not_found(response):
|
|
427
|
+
return (
|
|
428
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not "
|
|
429
|
+
"configured in Open WebUI. Configure a valid model id in "
|
|
430
|
+
"Admin Panel → Settings → Models."
|
|
431
|
+
)
|
|
432
|
+
return None
|
|
433
|
+
|
|
434
|
+
def _normalize_forward_exception(self, exc: Exception) -> str | None:
|
|
435
|
+
detail = getattr(exc, "detail", None)
|
|
436
|
+
if self._contains_model_not_found(detail) or self._contains_model_not_found(
|
|
437
|
+
str(exc)
|
|
438
|
+
):
|
|
439
|
+
return (
|
|
440
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not "
|
|
441
|
+
"configured in Open WebUI. Configure a valid model id in "
|
|
442
|
+
"Admin Panel → Settings → Models."
|
|
443
|
+
)
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
def _trace_enabled(self) -> bool:
|
|
447
|
+
return bool(getattr(self.valves, "SHOW_CONTEXT_COMPILER_TRACE", False))
|
|
448
|
+
|
|
449
|
+
def _append_trace_to_response(self, response: Any, trace_text: str) -> Any:
|
|
450
|
+
body_iterator = getattr(response, "body_iterator", None)
|
|
451
|
+
if body_iterator is not None and callable(
|
|
452
|
+
getattr(body_iterator, "__aiter__", None)
|
|
453
|
+
):
|
|
454
|
+
response.body_iterator = self._append_trace_to_stream(
|
|
455
|
+
cast(AsyncIterator[object], body_iterator), trace_text
|
|
456
|
+
)
|
|
457
|
+
return response
|
|
458
|
+
aiter = getattr(response, "__aiter__", None)
|
|
459
|
+
if callable(aiter):
|
|
460
|
+
return self._append_trace_to_stream(
|
|
461
|
+
cast(AsyncIterator[object], response), trace_text
|
|
462
|
+
)
|
|
463
|
+
if isinstance(response, str):
|
|
464
|
+
cleaned = _strip_trace_block_from_text(response)
|
|
465
|
+
return f"{cleaned}\n\n{trace_text}"
|
|
466
|
+
if isinstance(response, dict):
|
|
467
|
+
choices = response.get("choices")
|
|
468
|
+
if isinstance(choices, list) and choices:
|
|
469
|
+
first = choices[0]
|
|
470
|
+
if isinstance(first, dict):
|
|
471
|
+
message = first.get("message")
|
|
472
|
+
if isinstance(message, dict):
|
|
473
|
+
content = message.get("content")
|
|
474
|
+
if isinstance(content, str):
|
|
475
|
+
cleaned = _strip_trace_block_from_text(content)
|
|
476
|
+
message["content"] = f"{cleaned}\n\n{trace_text}"
|
|
477
|
+
return response
|
|
478
|
+
choices_attr = getattr(response, "choices", None)
|
|
479
|
+
if isinstance(choices_attr, list) and choices_attr:
|
|
480
|
+
first_choice = choices_attr[0]
|
|
481
|
+
message_attr = getattr(first_choice, "message", None)
|
|
482
|
+
content_attr = getattr(message_attr, "content", None)
|
|
483
|
+
if message_attr is not None and isinstance(content_attr, str):
|
|
484
|
+
cleaned = _strip_trace_block_from_text(content_attr)
|
|
485
|
+
message_attr.content = f"{cleaned}\n\n{trace_text}"
|
|
486
|
+
return response
|
|
487
|
+
return response
|
|
488
|
+
|
|
489
|
+
def _append_trace_to_stream(
|
|
490
|
+
self, stream: AsyncIterator[object], trace_text: str
|
|
491
|
+
) -> AsyncIterator[object]:
|
|
492
|
+
async def _wrapped() -> AsyncIterator[object]:
|
|
493
|
+
chunk_type: type[str] | type[bytes] | None = None
|
|
494
|
+
saw_done = False
|
|
495
|
+
trace_json = json.dumps(
|
|
496
|
+
{"choices": [{"delta": {"content": f"\n\n{trace_text}"}}]}
|
|
497
|
+
)
|
|
498
|
+
trace_event = f"data: {trace_json}\n\n"
|
|
499
|
+
|
|
500
|
+
def _matches_done(value: str) -> bool:
|
|
501
|
+
normalized = value.strip()
|
|
502
|
+
return normalized == "data: [DONE]" or normalized == "[DONE]"
|
|
503
|
+
|
|
504
|
+
async for chunk in stream:
|
|
505
|
+
if chunk_type is None:
|
|
506
|
+
if isinstance(chunk, bytes):
|
|
507
|
+
chunk_type = bytes
|
|
508
|
+
elif isinstance(chunk, str):
|
|
509
|
+
chunk_type = str
|
|
510
|
+
if isinstance(chunk, bytes):
|
|
511
|
+
decoded = chunk.decode("utf-8", errors="ignore")
|
|
512
|
+
if _matches_done(decoded):
|
|
513
|
+
saw_done = True
|
|
514
|
+
yield trace_event.encode("utf-8")
|
|
515
|
+
yield chunk
|
|
516
|
+
continue
|
|
517
|
+
elif isinstance(chunk, str) and _matches_done(chunk):
|
|
518
|
+
saw_done = True
|
|
519
|
+
yield trace_event
|
|
520
|
+
yield chunk
|
|
521
|
+
continue
|
|
522
|
+
yield _strip_existing_trace_from_chunk(chunk)
|
|
523
|
+
if saw_done:
|
|
524
|
+
return
|
|
525
|
+
suffix = f"\n\n{trace_text}"
|
|
526
|
+
if chunk_type is bytes:
|
|
527
|
+
yield suffix.encode("utf-8")
|
|
528
|
+
else:
|
|
529
|
+
yield suffix
|
|
530
|
+
|
|
531
|
+
return _wrapped()
|
|
532
|
+
|
|
533
|
+
def _with_trace(
|
|
534
|
+
self,
|
|
535
|
+
response: Any,
|
|
536
|
+
*,
|
|
537
|
+
original_input: str,
|
|
538
|
+
compiler_input: str,
|
|
539
|
+
decision: object,
|
|
540
|
+
state_before: object,
|
|
541
|
+
state_after: object,
|
|
542
|
+
llm_called: bool,
|
|
543
|
+
preprocessor_output: str | None = None,
|
|
544
|
+
state_injected: str = "no",
|
|
545
|
+
) -> Any:
|
|
546
|
+
if not self._trace_enabled():
|
|
547
|
+
return response
|
|
548
|
+
del original_input, compiler_input, preprocessor_output
|
|
549
|
+
trace_text = _build_compact_trace_text(
|
|
550
|
+
decision=decision,
|
|
551
|
+
state_before=state_before,
|
|
552
|
+
state_after=state_after,
|
|
553
|
+
llm_called=llm_called,
|
|
554
|
+
state_injected=state_injected,
|
|
555
|
+
)
|
|
556
|
+
return self._append_trace_to_response(response, trace_text)
|
|
557
|
+
|
|
558
|
+
async def _forward_passthrough(
|
|
559
|
+
self,
|
|
560
|
+
body: dict[str, Any],
|
|
561
|
+
user_payload: dict[str, Any],
|
|
562
|
+
request: Request,
|
|
563
|
+
*,
|
|
564
|
+
state: State | None = None,
|
|
565
|
+
) -> Any:
|
|
566
|
+
"""Forward with model override and optional compiler-owned state injection."""
|
|
567
|
+
payload = {**body}
|
|
568
|
+
payload["model"] = self.valves.BASE_MODEL_ID
|
|
569
|
+
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)
|
|
570
|
+
user = Users.get_user_by_id(user_payload["id"])
|
|
571
|
+
if inspect.isawaitable(user):
|
|
572
|
+
user = await user
|
|
573
|
+
try:
|
|
574
|
+
response = await generate_chat_completion(request, payload, user)
|
|
575
|
+
except Exception as exc:
|
|
576
|
+
normalized_exception = self._normalize_forward_exception(exc)
|
|
577
|
+
if normalized_exception is not None:
|
|
578
|
+
return normalized_exception
|
|
579
|
+
raise
|
|
580
|
+
normalized_error = self._normalize_forward_error(response)
|
|
581
|
+
if normalized_error is not None:
|
|
582
|
+
return normalized_error
|
|
583
|
+
return response
|
|
584
|
+
|
|
585
|
+
async def _forward_update(
|
|
586
|
+
self,
|
|
587
|
+
body: dict[str, Any],
|
|
588
|
+
user_payload: dict[str, Any],
|
|
589
|
+
request: Request,
|
|
590
|
+
state: State,
|
|
591
|
+
) -> Any:
|
|
592
|
+
"""Forward with one compiler-owned state message based on current state.
|
|
593
|
+
|
|
594
|
+
The body is shallow-copied, ``model`` is overridden, and exactly one
|
|
595
|
+
compiler-owned message is inserted/replaced before forwarding.
|
|
596
|
+
"""
|
|
597
|
+
payload = {**body}
|
|
598
|
+
payload["model"] = self.valves.BASE_MODEL_ID
|
|
599
|
+
|
|
600
|
+
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)
|
|
601
|
+
|
|
602
|
+
user = Users.get_user_by_id(user_payload["id"])
|
|
603
|
+
if inspect.isawaitable(user):
|
|
604
|
+
user = await user
|
|
605
|
+
try:
|
|
606
|
+
response = await generate_chat_completion(request, payload, user)
|
|
607
|
+
except Exception as exc:
|
|
608
|
+
normalized_exception = self._normalize_forward_exception(exc)
|
|
609
|
+
if normalized_exception is not None:
|
|
610
|
+
return normalized_exception
|
|
611
|
+
raise
|
|
612
|
+
normalized_error = self._normalize_forward_error(response)
|
|
613
|
+
if normalized_error is not None:
|
|
614
|
+
return normalized_error
|
|
615
|
+
return response
|
|
616
|
+
|
|
617
|
+
async def pipe(
|
|
618
|
+
self,
|
|
619
|
+
body: dict[str, Any],
|
|
620
|
+
__user__: dict[str, Any],
|
|
621
|
+
__request__: Request,
|
|
622
|
+
__chat_id__: str | None = None,
|
|
623
|
+
__metadata__: dict[str, Any] | None = None,
|
|
624
|
+
) -> Any:
|
|
625
|
+
"""Run minimal host flow around compiler decisions.
|
|
626
|
+
|
|
627
|
+
Flow:
|
|
628
|
+
- Extract latest user text.
|
|
629
|
+
- Bypass compiler for non-text or missing-user turns.
|
|
630
|
+
- Resolve chat key and get/create per-chat engine.
|
|
631
|
+
- Call ``engine.step(...)``.
|
|
632
|
+
- Map ``clarify`` / ``passthrough`` / ``update`` outcomes.
|
|
633
|
+
"""
|
|
634
|
+
raw_messages = body.get("messages")
|
|
635
|
+
messages = (
|
|
636
|
+
[msg for msg in raw_messages if isinstance(msg, dict)]
|
|
637
|
+
if isinstance(raw_messages, list)
|
|
638
|
+
else []
|
|
639
|
+
)
|
|
640
|
+
base_model_id = self.valves.BASE_MODEL_ID.strip()
|
|
641
|
+
current_model_id = str(body.get("model", "")).strip()
|
|
642
|
+
if not base_model_id:
|
|
643
|
+
return "Context Compiler pipe misconfigured: BASE_MODEL_ID is required."
|
|
644
|
+
if current_model_id and base_model_id == current_model_id:
|
|
645
|
+
return (
|
|
646
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID must not match "
|
|
647
|
+
"the selected pipe model id to avoid recursive routing."
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
latest_user_text = _extract_latest_user_text(messages)
|
|
651
|
+
logger.debug("pipe: user_input_found=%s", latest_user_text is not None)
|
|
652
|
+
|
|
653
|
+
if latest_user_text is None:
|
|
654
|
+
return await self._forward_passthrough(body, __user__, __request__)
|
|
655
|
+
|
|
656
|
+
chat_key = _resolve_chat_key(__user__, __chat_id__, __metadata__)
|
|
657
|
+
engine = _ENGINES_BY_CHAT_KEY.get(chat_key)
|
|
658
|
+
if engine is None:
|
|
659
|
+
engine = create_engine()
|
|
660
|
+
checkpoint = _CHECKPOINTS_BY_CHAT_KEY.get(chat_key)
|
|
661
|
+
if checkpoint is not None:
|
|
662
|
+
engine.import_checkpoint_json(checkpoint)
|
|
663
|
+
_ENGINES_BY_CHAT_KEY[chat_key] = engine
|
|
664
|
+
|
|
665
|
+
if latest_user_text.strip().lower() == "show state":
|
|
666
|
+
return _render_show_state_summary(engine)
|
|
667
|
+
|
|
668
|
+
state_before = engine.state
|
|
669
|
+
logger.debug("pipe: engine_input=%r", latest_user_text)
|
|
670
|
+
decision = engine.step(latest_user_text)
|
|
671
|
+
if is_clarify(decision):
|
|
672
|
+
kind = DECISION_CLARIFY
|
|
673
|
+
elif is_update(decision):
|
|
674
|
+
kind = DECISION_UPDATE
|
|
675
|
+
else:
|
|
676
|
+
kind = DECISION_PASSTHROUGH
|
|
677
|
+
logger.debug("pipe: decision=%s", kind)
|
|
678
|
+
near_miss_prompt = _near_miss_directive_clarify(latest_user_text)
|
|
679
|
+
state_after = get_decision_state(decision)
|
|
680
|
+
if state_after is None:
|
|
681
|
+
state_after = engine.state
|
|
682
|
+
|
|
683
|
+
if is_clarify(decision):
|
|
684
|
+
_CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json()
|
|
685
|
+
return self._with_trace(
|
|
686
|
+
near_miss_prompt or get_clarify_prompt(decision) or "",
|
|
687
|
+
original_input=latest_user_text,
|
|
688
|
+
compiler_input=latest_user_text,
|
|
689
|
+
decision=decision,
|
|
690
|
+
state_before=state_before,
|
|
691
|
+
state_after=state_after,
|
|
692
|
+
llm_called=False,
|
|
693
|
+
)
|
|
694
|
+
if near_miss_prompt is not None and is_passthrough(decision):
|
|
695
|
+
return self._with_trace(
|
|
696
|
+
near_miss_prompt,
|
|
697
|
+
original_input=latest_user_text,
|
|
698
|
+
compiler_input=latest_user_text,
|
|
699
|
+
decision={"kind": DECISION_CLARIFY, "prompt_to_user": near_miss_prompt},
|
|
700
|
+
state_before=state_before,
|
|
701
|
+
state_after=state_after,
|
|
702
|
+
llm_called=False,
|
|
703
|
+
)
|
|
704
|
+
if is_passthrough(decision):
|
|
705
|
+
compiled_state = _normalize_state(state_after)
|
|
706
|
+
state_injected = (
|
|
707
|
+
"yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
|
|
708
|
+
)
|
|
709
|
+
response = await self._forward_passthrough(
|
|
710
|
+
body, __user__, __request__, state=compiled_state
|
|
711
|
+
)
|
|
712
|
+
return self._with_trace(
|
|
713
|
+
response,
|
|
714
|
+
original_input=latest_user_text,
|
|
715
|
+
compiler_input=latest_user_text,
|
|
716
|
+
decision=decision,
|
|
717
|
+
state_before=state_before,
|
|
718
|
+
state_after=state_after,
|
|
719
|
+
llm_called=True,
|
|
720
|
+
state_injected=state_injected,
|
|
721
|
+
)
|
|
722
|
+
if is_update(decision):
|
|
723
|
+
_CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json()
|
|
724
|
+
return self._with_trace(
|
|
725
|
+
_summarize_update_from_input(latest_user_text),
|
|
726
|
+
original_input=latest_user_text,
|
|
727
|
+
compiler_input=latest_user_text,
|
|
728
|
+
decision=decision,
|
|
729
|
+
state_before=state_before,
|
|
730
|
+
state_after=state_after,
|
|
731
|
+
llm_called=False,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
compiled_state = _normalize_state(state_after)
|
|
735
|
+
state_injected = (
|
|
736
|
+
"yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
|
|
737
|
+
)
|
|
738
|
+
response = await self._forward_passthrough(
|
|
739
|
+
body, __user__, __request__, state=compiled_state
|
|
740
|
+
)
|
|
741
|
+
return self._with_trace(
|
|
742
|
+
response,
|
|
743
|
+
original_input=latest_user_text,
|
|
744
|
+
compiler_input=latest_user_text,
|
|
745
|
+
decision=decision,
|
|
746
|
+
state_before=state_before,
|
|
747
|
+
state_after=state_after,
|
|
748
|
+
llm_called=True,
|
|
749
|
+
state_injected=state_injected,
|
|
750
|
+
)
|