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
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
"""
|
|
2
|
+
title: Context Compiler Open WebUI Pipe (Directive Drafter)
|
|
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, context-compiler-directive-drafter>=0.1.2
|
|
7
|
+
|
|
8
|
+
Open WebUI integration with Context Compiler directive drafter.
|
|
9
|
+
|
|
10
|
+
This example extends `open_webui_pipe.py` by inserting a directive-drafting step:
|
|
11
|
+
|
|
12
|
+
1. Run heuristic directive drafter (fast, high-precision cases)
|
|
13
|
+
2. Fall back to Open WebUI-native model completion when needed
|
|
14
|
+
3. Pass resulting directive (or original input) to `engine.step(...)`
|
|
15
|
+
|
|
16
|
+
Core decision handling remains the same as the base integration.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import inspect
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
from collections.abc import AsyncIterator
|
|
24
|
+
from importlib.resources import as_file, files
|
|
25
|
+
from importlib.resources.abc import Traversable
|
|
26
|
+
from typing import Any, Literal, cast
|
|
27
|
+
|
|
28
|
+
from fastapi import Request # type: ignore[import-not-found]
|
|
29
|
+
from open_webui.models.users import Users # type: ignore[import-not-found]
|
|
30
|
+
from open_webui.utils.chat import generate_chat_completion # type: ignore[import-not-found]
|
|
31
|
+
from open_webui.utils.models import get_all_models # type: ignore[import-not-found]
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
from pydantic import BaseModel, Field
|
|
35
|
+
except ModuleNotFoundError:
|
|
36
|
+
# Keep this import optional: CI/tests run without integration extras.
|
|
37
|
+
# These lightweight fallbacks keep import-time behavior deterministic so
|
|
38
|
+
# coverage exercises the pipe module without pydantic installed.
|
|
39
|
+
class BaseModel: # type: ignore[no-redef]
|
|
40
|
+
def __init__(self, **kwargs: object) -> None:
|
|
41
|
+
for key, value in kwargs.items():
|
|
42
|
+
setattr(self, key, value)
|
|
43
|
+
|
|
44
|
+
def Field(*, default: Any, description: str = "") -> Any: # type: ignore[no-redef]
|
|
45
|
+
del description
|
|
46
|
+
return default
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
from context_compiler import (
|
|
50
|
+
DECISION_CLARIFY,
|
|
51
|
+
DECISION_PASSTHROUGH,
|
|
52
|
+
DECISION_UPDATE,
|
|
53
|
+
POLICY_PROHIBIT,
|
|
54
|
+
POLICY_USE,
|
|
55
|
+
State,
|
|
56
|
+
create_engine,
|
|
57
|
+
get_clarify_prompt,
|
|
58
|
+
get_decision_state,
|
|
59
|
+
get_policy_items,
|
|
60
|
+
get_premise_value,
|
|
61
|
+
is_clarify,
|
|
62
|
+
is_passthrough,
|
|
63
|
+
is_update,
|
|
64
|
+
state_diff,
|
|
65
|
+
)
|
|
66
|
+
from context_compiler.engine import Engine
|
|
67
|
+
from context_compiler_directive_drafter import (
|
|
68
|
+
PREPROCESS_OUTCOME_DIRECTIVE,
|
|
69
|
+
parse_preprocessor_output,
|
|
70
|
+
preprocess_heuristic,
|
|
71
|
+
render_prompt,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
logger = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
_CC_MARKER = "[[cc_state]]"
|
|
77
|
+
_ENGINES_BY_CHAT_KEY: dict[str, Engine] = {}
|
|
78
|
+
# Example-only in-memory checkpoint store.
|
|
79
|
+
# This keeps continuation state only for the current process lifetime.
|
|
80
|
+
# Real deployments should persist checkpoints externally (DB/Redis/etc.),
|
|
81
|
+
# or restart continuity for pending flows will be lost.
|
|
82
|
+
_CHECKPOINTS_BY_CHAT_KEY: dict[str, str] = {}
|
|
83
|
+
_PROMPTS_DIR = files("context_compiler_directive_drafter").joinpath("prompts")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _is_directive_shaped_input(message: str) -> bool:
|
|
87
|
+
normalized = re.sub(r"\s+", " ", message.strip()).lower()
|
|
88
|
+
return (
|
|
89
|
+
normalized.startswith("use")
|
|
90
|
+
or normalized.startswith("prohibit")
|
|
91
|
+
or normalized.startswith("remove policy")
|
|
92
|
+
or normalized.startswith("set premise")
|
|
93
|
+
or normalized.startswith("change premise")
|
|
94
|
+
or normalized.startswith("clear")
|
|
95
|
+
or normalized.startswith("reset")
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _prompt_file_path(profile: str) -> Traversable:
|
|
100
|
+
# Runtime prompt selection for fallback drafting:
|
|
101
|
+
# - default: most instruction-following models
|
|
102
|
+
# - llama: models that need tighter prompt guidance
|
|
103
|
+
if profile == "llama":
|
|
104
|
+
return _PROMPTS_DIR.joinpath("llama.txt")
|
|
105
|
+
return _PROMPTS_DIR.joinpath("default.txt")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolve_chat_key(
|
|
109
|
+
user: dict[str, Any],
|
|
110
|
+
chat_id: str | None,
|
|
111
|
+
metadata: dict[str, Any] | None,
|
|
112
|
+
) -> str:
|
|
113
|
+
if chat_id:
|
|
114
|
+
return chat_id
|
|
115
|
+
if isinstance(metadata, dict):
|
|
116
|
+
metadata_chat_id = metadata.get("chat_id")
|
|
117
|
+
if isinstance(metadata_chat_id, str) and metadata_chat_id:
|
|
118
|
+
return metadata_chat_id
|
|
119
|
+
user_id = str(user["id"])
|
|
120
|
+
return f"no-chat-id:{user_id}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _extract_latest_user_text(messages: list[dict[str, Any]]) -> str | None:
|
|
124
|
+
for message in reversed(messages):
|
|
125
|
+
if message.get("role") != "user":
|
|
126
|
+
continue
|
|
127
|
+
content = message.get("content")
|
|
128
|
+
if isinstance(content, str):
|
|
129
|
+
return content
|
|
130
|
+
return None
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _has_pending_clarification(engine: Engine) -> bool:
|
|
135
|
+
return engine.has_pending_clarification()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _render_compiler_state_block(state: State) -> str:
|
|
139
|
+
lines: list[str] = [_CC_MARKER]
|
|
140
|
+
|
|
141
|
+
premise = get_premise_value(state)
|
|
142
|
+
if premise is not None:
|
|
143
|
+
lines.append(f"Premise: {premise}")
|
|
144
|
+
|
|
145
|
+
use_items = sorted(get_policy_items(state, POLICY_USE))
|
|
146
|
+
if use_items:
|
|
147
|
+
lines.append("Use: " + ", ".join(use_items))
|
|
148
|
+
|
|
149
|
+
prohibit_items = sorted(get_policy_items(state, POLICY_PROHIBIT))
|
|
150
|
+
if prohibit_items:
|
|
151
|
+
lines.append("Prohibit: " + ", ".join(prohibit_items))
|
|
152
|
+
|
|
153
|
+
return "\n".join(lines)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _render_show_state_summary(engine: Engine) -> str:
|
|
157
|
+
premise = get_premise_value(engine.state)
|
|
158
|
+
use_items = sorted(get_policy_items(engine.state, POLICY_USE))
|
|
159
|
+
prohibit_items = sorted(get_policy_items(engine.state, POLICY_PROHIBIT))
|
|
160
|
+
pending = engine.has_pending_clarification()
|
|
161
|
+
|
|
162
|
+
use_text = ", ".join(use_items) if use_items else "none"
|
|
163
|
+
prohibit_text = ", ".join(prohibit_items) if prohibit_items else "none"
|
|
164
|
+
premise_text = premise if premise is not None else "none"
|
|
165
|
+
pending_text = "yes" if pending else "no"
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
f"Premise: {premise_text}\n"
|
|
169
|
+
f"Use: {use_text}\n"
|
|
170
|
+
f"Prohibit: {prohibit_text}\n"
|
|
171
|
+
f"Pending clarification: {pending_text}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _replace_compiler_system_message(
|
|
176
|
+
messages: list[dict[str, Any]],
|
|
177
|
+
rendered_state_block: str,
|
|
178
|
+
) -> list[dict[str, Any]]:
|
|
179
|
+
filtered_messages: list[dict[str, Any]] = []
|
|
180
|
+
last_system_index = -1
|
|
181
|
+
|
|
182
|
+
for message in messages:
|
|
183
|
+
role = message.get("role")
|
|
184
|
+
content = message.get("content")
|
|
185
|
+
if (
|
|
186
|
+
role == "system"
|
|
187
|
+
and isinstance(content, str)
|
|
188
|
+
and content.startswith(_CC_MARKER)
|
|
189
|
+
):
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
filtered_messages.append(message)
|
|
193
|
+
if role == "system":
|
|
194
|
+
last_system_index = len(filtered_messages) - 1
|
|
195
|
+
|
|
196
|
+
insert_at = last_system_index + 1 if last_system_index >= 0 else 0
|
|
197
|
+
compiler_message: dict[str, Any] = {
|
|
198
|
+
"role": "system",
|
|
199
|
+
"content": rendered_state_block,
|
|
200
|
+
}
|
|
201
|
+
return [
|
|
202
|
+
*filtered_messages[:insert_at],
|
|
203
|
+
compiler_message,
|
|
204
|
+
*filtered_messages[insert_at:],
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _normalize_state(value: object) -> State:
|
|
209
|
+
if isinstance(value, dict):
|
|
210
|
+
return cast(State, value)
|
|
211
|
+
return {"premise": None, "policies": {}, "version": 2}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _has_non_empty_authoritative_state(state: State) -> bool:
|
|
215
|
+
if get_premise_value(state) is not None:
|
|
216
|
+
return True
|
|
217
|
+
return bool(
|
|
218
|
+
get_policy_items(state, POLICY_USE) or get_policy_items(state, POLICY_PROHIBIT)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _render_state_summary_line(state: object) -> str:
|
|
223
|
+
if not isinstance(state, dict):
|
|
224
|
+
return "unavailable"
|
|
225
|
+
typed_state = cast(State, state)
|
|
226
|
+
|
|
227
|
+
premise = get_premise_value(typed_state)
|
|
228
|
+
use_items = sorted(get_policy_items(typed_state, POLICY_USE))
|
|
229
|
+
prohibit_items = sorted(get_policy_items(typed_state, POLICY_PROHIBIT))
|
|
230
|
+
return (
|
|
231
|
+
f"premise={premise if premise is not None else '(none)'}; "
|
|
232
|
+
f"use={', '.join(use_items) if use_items else '(none)'}; "
|
|
233
|
+
f"prohibit={', '.join(prohibit_items) if prohibit_items else '(none)'}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _build_compact_trace_text(
|
|
238
|
+
*,
|
|
239
|
+
decision: object,
|
|
240
|
+
state_before: object,
|
|
241
|
+
state_after: object,
|
|
242
|
+
llm_called: bool,
|
|
243
|
+
state_injected: str,
|
|
244
|
+
) -> str:
|
|
245
|
+
kind = decision.get("kind", "unknown") if isinstance(decision, dict) else "unknown"
|
|
246
|
+
changed = "unknown"
|
|
247
|
+
if isinstance(state_before, dict) and isinstance(state_after, dict):
|
|
248
|
+
changed = (
|
|
249
|
+
"yes"
|
|
250
|
+
if state_diff(cast(State, state_before), cast(State, state_after))[
|
|
251
|
+
"changed"
|
|
252
|
+
]
|
|
253
|
+
else "no"
|
|
254
|
+
)
|
|
255
|
+
return "\n".join(
|
|
256
|
+
[
|
|
257
|
+
"Context Compiler trace",
|
|
258
|
+
f"- decision: {kind}",
|
|
259
|
+
f"- llm_called: {'yes' if llm_called else 'no'}",
|
|
260
|
+
f"- state_changed: {changed}",
|
|
261
|
+
f"- state_injected: {state_injected}",
|
|
262
|
+
f"- state_before: {_render_state_summary_line(state_before)}",
|
|
263
|
+
f"- state_after: {_render_state_summary_line(state_after)}",
|
|
264
|
+
]
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _strip_trace_block_from_text(content: str) -> str:
|
|
269
|
+
marker = "Context Compiler trace"
|
|
270
|
+
index = content.find(marker)
|
|
271
|
+
if index < 0:
|
|
272
|
+
return content
|
|
273
|
+
return content[:index].rstrip()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _strip_trace_blocks_from_messages(
|
|
277
|
+
messages: list[dict[str, Any]],
|
|
278
|
+
) -> list[dict[str, Any]]:
|
|
279
|
+
cleaned: list[dict[str, Any]] = []
|
|
280
|
+
for message in messages:
|
|
281
|
+
msg = dict(message)
|
|
282
|
+
content = msg.get("content")
|
|
283
|
+
if isinstance(content, str):
|
|
284
|
+
msg["content"] = _strip_trace_block_from_text(content)
|
|
285
|
+
cleaned.append(msg)
|
|
286
|
+
return cleaned
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _build_forward_messages(
|
|
290
|
+
raw_messages: object,
|
|
291
|
+
*,
|
|
292
|
+
state: State | None = None,
|
|
293
|
+
) -> list[dict[str, Any]]:
|
|
294
|
+
"""Build forwarded messages with trace stripping and optional state injection."""
|
|
295
|
+
messages = (
|
|
296
|
+
_strip_trace_blocks_from_messages(
|
|
297
|
+
[msg for msg in raw_messages if isinstance(msg, dict)]
|
|
298
|
+
)
|
|
299
|
+
if isinstance(raw_messages, list)
|
|
300
|
+
else []
|
|
301
|
+
)
|
|
302
|
+
if state is not None and _has_non_empty_authoritative_state(state):
|
|
303
|
+
return _replace_compiler_system_message(
|
|
304
|
+
messages,
|
|
305
|
+
_render_compiler_state_block(state),
|
|
306
|
+
)
|
|
307
|
+
return messages
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _strip_existing_trace_from_chunk(chunk: object) -> object:
|
|
311
|
+
if isinstance(chunk, str):
|
|
312
|
+
return _strip_trace_block_from_text(chunk)
|
|
313
|
+
if isinstance(chunk, bytes):
|
|
314
|
+
decoded = chunk.decode("utf-8", errors="ignore")
|
|
315
|
+
cleaned = _strip_trace_block_from_text(decoded)
|
|
316
|
+
return cleaned.encode("utf-8")
|
|
317
|
+
return chunk
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _render_item_label(value: str) -> str:
|
|
321
|
+
return re.sub(r"\s+", " ", value).strip().lower()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _near_miss_directive_clarify(value: str) -> str | None:
|
|
325
|
+
normalized = re.sub(r"\s+", " ", value.strip())
|
|
326
|
+
lower = normalized.lower()
|
|
327
|
+
|
|
328
|
+
if lower in {"reset premise", "reset premises", "clear premises"}:
|
|
329
|
+
return "Unknown directive.\nUse 'clear premise' or 'reset policies'."
|
|
330
|
+
if lower.startswith("set premise to "):
|
|
331
|
+
return "Invalid premise syntax.\nUse 'set premise <value>'."
|
|
332
|
+
if lower.startswith("change premise ") and not lower.startswith(
|
|
333
|
+
"change premise to "
|
|
334
|
+
):
|
|
335
|
+
return "Invalid premise syntax.\nUse 'change premise to <value>'."
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _summarize_update_from_input(user_input: str) -> str:
|
|
340
|
+
normalized = re.sub(r"\s+", " ", user_input.strip())
|
|
341
|
+
lower = normalized.lower()
|
|
342
|
+
|
|
343
|
+
if lower == "clear state":
|
|
344
|
+
return "State cleared."
|
|
345
|
+
if lower == "clear premise":
|
|
346
|
+
return "Premise cleared."
|
|
347
|
+
if lower == "reset policies":
|
|
348
|
+
return "Policies reset."
|
|
349
|
+
|
|
350
|
+
replacement_match = re.match(
|
|
351
|
+
r"^use\s+(.+?)\s+instead\s+of\s+(.+)$", normalized, flags=re.IGNORECASE
|
|
352
|
+
)
|
|
353
|
+
if replacement_match is not None:
|
|
354
|
+
item = _render_item_label(replacement_match.group(1).rstrip(" .!?"))
|
|
355
|
+
if item:
|
|
356
|
+
return f"State updated: Use {item}."
|
|
357
|
+
|
|
358
|
+
use_match = re.match(r"^use\s+(.+)$", normalized, flags=re.IGNORECASE)
|
|
359
|
+
if use_match is not None:
|
|
360
|
+
item = _render_item_label(use_match.group(1).rstrip(" .!?"))
|
|
361
|
+
if item:
|
|
362
|
+
return f"State updated: Use {item}."
|
|
363
|
+
|
|
364
|
+
prohibit_match = re.match(r"^prohibit\s+(.+)$", normalized, flags=re.IGNORECASE)
|
|
365
|
+
if prohibit_match is not None:
|
|
366
|
+
item = _render_item_label(prohibit_match.group(1).rstrip(" .!?"))
|
|
367
|
+
if item:
|
|
368
|
+
return f"State updated: Prohibit {item}."
|
|
369
|
+
|
|
370
|
+
remove_policy_match = re.match(
|
|
371
|
+
r"^remove\s+policy\s+(.+)$", normalized, flags=re.IGNORECASE
|
|
372
|
+
)
|
|
373
|
+
if remove_policy_match is not None:
|
|
374
|
+
item = _render_item_label(remove_policy_match.group(1).rstrip(" .!?"))
|
|
375
|
+
if item:
|
|
376
|
+
return f"State updated: Removed policy {item}."
|
|
377
|
+
|
|
378
|
+
return "State updated."
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _is_administrative_update_input(user_input: str) -> bool:
|
|
382
|
+
normalized = re.sub(r"\s+", " ", user_input.strip()).lower()
|
|
383
|
+
return (
|
|
384
|
+
normalized == "clear state"
|
|
385
|
+
or normalized == "clear premise"
|
|
386
|
+
or normalized == "reset policies"
|
|
387
|
+
or normalized.startswith("remove policy ")
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _extract_completion_content(response: object) -> str | None:
|
|
392
|
+
choices_attr = getattr(response, "choices", None)
|
|
393
|
+
if isinstance(choices_attr, list) and choices_attr:
|
|
394
|
+
first_choice = choices_attr[0]
|
|
395
|
+
message_attr = getattr(first_choice, "message", None)
|
|
396
|
+
content_attr = getattr(message_attr, "content", None)
|
|
397
|
+
if isinstance(content_attr, str):
|
|
398
|
+
return content_attr
|
|
399
|
+
|
|
400
|
+
if isinstance(response, dict):
|
|
401
|
+
choices = response.get("choices")
|
|
402
|
+
if isinstance(choices, list) and choices:
|
|
403
|
+
first_choice = choices[0]
|
|
404
|
+
if isinstance(first_choice, dict):
|
|
405
|
+
message = first_choice.get("message")
|
|
406
|
+
if isinstance(message, dict):
|
|
407
|
+
content = message.get("content")
|
|
408
|
+
if isinstance(content, str):
|
|
409
|
+
return content
|
|
410
|
+
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _normalize_model_id(value: str | None) -> str | None:
|
|
415
|
+
if value is None:
|
|
416
|
+
return None
|
|
417
|
+
value = value.strip()
|
|
418
|
+
return value or None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _is_truthy_bool(value: object) -> bool:
|
|
422
|
+
if isinstance(value, bool):
|
|
423
|
+
return value
|
|
424
|
+
if isinstance(value, str):
|
|
425
|
+
normalized = value.strip().lower()
|
|
426
|
+
if normalized in {"true", "1", "on"}:
|
|
427
|
+
return True
|
|
428
|
+
if normalized in {"false", "0", "off"}:
|
|
429
|
+
return False
|
|
430
|
+
return False
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class Pipe:
|
|
434
|
+
"""Map Context Compiler decisions into Open WebUI pipe behavior.
|
|
435
|
+
|
|
436
|
+
This variant adds a directive-drafter stage before ``engine.step(...)``:
|
|
437
|
+
heuristic first, then Open WebUI-native LLM fallback.
|
|
438
|
+
Update decisions return deterministic local acknowledgement (no model call).
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
class Valves(BaseModel):
|
|
442
|
+
BASE_MODEL_ID: str = Field(
|
|
443
|
+
default="",
|
|
444
|
+
description=(
|
|
445
|
+
"Required Open WebUI model id used for forwarding. Must exactly match a "
|
|
446
|
+
"configured model id in Open WebUI (not arbitrary text), for example: "
|
|
447
|
+
"llama3.1:8b."
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
PREPROCESSOR_MODEL_ID: str | None = Field(
|
|
451
|
+
default=None,
|
|
452
|
+
description=(
|
|
453
|
+
"Optional model id for fallback drafting (defaults to BASE_MODEL_ID)."
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
PREPROCESSOR_PROMPT_PROFILE: Literal["default", "llama"] = Field(
|
|
457
|
+
default="default",
|
|
458
|
+
description="Prompt profile for LLM fallback drafting.",
|
|
459
|
+
)
|
|
460
|
+
ALLOW_MISSING_BASE_MODEL_FOR_DEBUG: bool = Field(
|
|
461
|
+
default=False,
|
|
462
|
+
description="Allow missing BASE_MODEL_ID for debug/testing only.",
|
|
463
|
+
)
|
|
464
|
+
SHOW_CONTEXT_COMPILER_TRACE: bool = Field(
|
|
465
|
+
default=False,
|
|
466
|
+
description="Include concise Context Compiler trace text in responses.",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def __init__(self) -> None:
|
|
470
|
+
self.valves = self.Valves()
|
|
471
|
+
|
|
472
|
+
def _allow_missing_base_model_for_debug(self) -> bool:
|
|
473
|
+
return _is_truthy_bool(
|
|
474
|
+
getattr(self.valves, "ALLOW_MISSING_BASE_MODEL_FOR_DEBUG", False)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
def _trace_enabled(self) -> bool:
|
|
478
|
+
return bool(getattr(self.valves, "SHOW_CONTEXT_COMPILER_TRACE", False))
|
|
479
|
+
|
|
480
|
+
def _append_trace_to_response(self, response: Any, trace_text: str) -> Any:
|
|
481
|
+
body_iterator = getattr(response, "body_iterator", None)
|
|
482
|
+
if body_iterator is not None and callable(
|
|
483
|
+
getattr(body_iterator, "__aiter__", None)
|
|
484
|
+
):
|
|
485
|
+
response.body_iterator = self._append_trace_to_stream(
|
|
486
|
+
cast(AsyncIterator[object], body_iterator), trace_text
|
|
487
|
+
)
|
|
488
|
+
return response
|
|
489
|
+
aiter = getattr(response, "__aiter__", None)
|
|
490
|
+
if callable(aiter):
|
|
491
|
+
return self._append_trace_to_stream(
|
|
492
|
+
cast(AsyncIterator[object], response), trace_text
|
|
493
|
+
)
|
|
494
|
+
if isinstance(response, str):
|
|
495
|
+
cleaned = _strip_trace_block_from_text(response)
|
|
496
|
+
return f"{cleaned}\n\n{trace_text}"
|
|
497
|
+
if isinstance(response, dict):
|
|
498
|
+
choices = response.get("choices")
|
|
499
|
+
if isinstance(choices, list) and choices:
|
|
500
|
+
first = choices[0]
|
|
501
|
+
if isinstance(first, dict):
|
|
502
|
+
message = first.get("message")
|
|
503
|
+
if isinstance(message, dict):
|
|
504
|
+
content = message.get("content")
|
|
505
|
+
if isinstance(content, str):
|
|
506
|
+
cleaned = _strip_trace_block_from_text(content)
|
|
507
|
+
message["content"] = f"{cleaned}\n\n{trace_text}"
|
|
508
|
+
return response
|
|
509
|
+
choices_attr = getattr(response, "choices", None)
|
|
510
|
+
if isinstance(choices_attr, list) and choices_attr:
|
|
511
|
+
first_choice = choices_attr[0]
|
|
512
|
+
message_attr = getattr(first_choice, "message", None)
|
|
513
|
+
content_attr = getattr(message_attr, "content", None)
|
|
514
|
+
if message_attr is not None and isinstance(content_attr, str):
|
|
515
|
+
cleaned = _strip_trace_block_from_text(content_attr)
|
|
516
|
+
message_attr.content = f"{cleaned}\n\n{trace_text}"
|
|
517
|
+
return response
|
|
518
|
+
return response
|
|
519
|
+
|
|
520
|
+
def _append_trace_to_stream(
|
|
521
|
+
self, stream: AsyncIterator[object], trace_text: str
|
|
522
|
+
) -> AsyncIterator[object]:
|
|
523
|
+
async def _wrapped() -> AsyncIterator[object]:
|
|
524
|
+
chunk_type: type[str] | type[bytes] | None = None
|
|
525
|
+
saw_done = False
|
|
526
|
+
trace_json = json.dumps(
|
|
527
|
+
{"choices": [{"delta": {"content": f"\n\n{trace_text}"}}]}
|
|
528
|
+
)
|
|
529
|
+
trace_event = f"data: {trace_json}\n\n"
|
|
530
|
+
|
|
531
|
+
def _matches_done(value: str) -> bool:
|
|
532
|
+
normalized = value.strip()
|
|
533
|
+
return normalized == "data: [DONE]" or normalized == "[DONE]"
|
|
534
|
+
|
|
535
|
+
async for chunk in stream:
|
|
536
|
+
if chunk_type is None:
|
|
537
|
+
if isinstance(chunk, bytes):
|
|
538
|
+
chunk_type = bytes
|
|
539
|
+
elif isinstance(chunk, str):
|
|
540
|
+
chunk_type = str
|
|
541
|
+
if isinstance(chunk, bytes):
|
|
542
|
+
decoded = chunk.decode("utf-8", errors="ignore")
|
|
543
|
+
if _matches_done(decoded):
|
|
544
|
+
saw_done = True
|
|
545
|
+
yield trace_event.encode("utf-8")
|
|
546
|
+
yield chunk
|
|
547
|
+
continue
|
|
548
|
+
elif isinstance(chunk, str) and _matches_done(chunk):
|
|
549
|
+
saw_done = True
|
|
550
|
+
yield trace_event
|
|
551
|
+
yield chunk
|
|
552
|
+
continue
|
|
553
|
+
yield _strip_existing_trace_from_chunk(chunk)
|
|
554
|
+
if saw_done:
|
|
555
|
+
return
|
|
556
|
+
suffix = f"\n\n{trace_text}"
|
|
557
|
+
if chunk_type is bytes:
|
|
558
|
+
yield suffix.encode("utf-8")
|
|
559
|
+
else:
|
|
560
|
+
yield suffix
|
|
561
|
+
|
|
562
|
+
return _wrapped()
|
|
563
|
+
|
|
564
|
+
def _with_trace(
|
|
565
|
+
self,
|
|
566
|
+
response: Any,
|
|
567
|
+
*,
|
|
568
|
+
original_input: str,
|
|
569
|
+
compiler_input: str,
|
|
570
|
+
decision: object,
|
|
571
|
+
state_before: object,
|
|
572
|
+
state_after: object,
|
|
573
|
+
llm_called: bool,
|
|
574
|
+
preprocessor_output: str | None = None,
|
|
575
|
+
state_injected: str = "no",
|
|
576
|
+
) -> Any:
|
|
577
|
+
if not self._trace_enabled():
|
|
578
|
+
return response
|
|
579
|
+
del original_input, compiler_input, preprocessor_output
|
|
580
|
+
trace_text = _build_compact_trace_text(
|
|
581
|
+
decision=decision,
|
|
582
|
+
state_before=state_before,
|
|
583
|
+
state_after=state_after,
|
|
584
|
+
llm_called=llm_called,
|
|
585
|
+
state_injected=state_injected,
|
|
586
|
+
)
|
|
587
|
+
return self._append_trace_to_response(response, trace_text)
|
|
588
|
+
|
|
589
|
+
def _is_model_not_found_text(self, value: object) -> bool:
|
|
590
|
+
if not isinstance(value, str):
|
|
591
|
+
return False
|
|
592
|
+
return "model not found" in value.lower()
|
|
593
|
+
|
|
594
|
+
def _contains_model_not_found(self, value: object) -> bool:
|
|
595
|
+
if self._is_model_not_found_text(value):
|
|
596
|
+
return True
|
|
597
|
+
if isinstance(value, dict):
|
|
598
|
+
return any(self._contains_model_not_found(v) for v in value.values())
|
|
599
|
+
if isinstance(value, list):
|
|
600
|
+
return any(self._contains_model_not_found(v) for v in value)
|
|
601
|
+
return False
|
|
602
|
+
|
|
603
|
+
def _normalize_forward_error(self, response: Any) -> str | None:
|
|
604
|
+
if self._contains_model_not_found(response):
|
|
605
|
+
return (
|
|
606
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not "
|
|
607
|
+
"configured in Open WebUI. Configure a valid model id in "
|
|
608
|
+
"Admin Panel → Settings → Models."
|
|
609
|
+
)
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
def _normalize_forward_exception(self, exc: Exception) -> str | None:
|
|
613
|
+
detail = getattr(exc, "detail", None)
|
|
614
|
+
if self._contains_model_not_found(detail) or self._contains_model_not_found(
|
|
615
|
+
str(exc)
|
|
616
|
+
):
|
|
617
|
+
return (
|
|
618
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID is invalid or not "
|
|
619
|
+
"configured in Open WebUI. Configure a valid model id in "
|
|
620
|
+
"Admin Panel → Settings → Models."
|
|
621
|
+
)
|
|
622
|
+
return None
|
|
623
|
+
|
|
624
|
+
def _normalize_preprocessor_error(self, response: Any) -> str | None:
|
|
625
|
+
if self._contains_model_not_found(response):
|
|
626
|
+
return (
|
|
627
|
+
"Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or "
|
|
628
|
+
"not configured in Open WebUI. Configure a valid model id in "
|
|
629
|
+
"Admin Panel → Settings → Models."
|
|
630
|
+
)
|
|
631
|
+
return None
|
|
632
|
+
|
|
633
|
+
def _normalize_preprocessor_exception(self, exc: Exception) -> str | None:
|
|
634
|
+
detail = getattr(exc, "detail", None)
|
|
635
|
+
if self._contains_model_not_found(detail) or self._contains_model_not_found(
|
|
636
|
+
str(exc)
|
|
637
|
+
):
|
|
638
|
+
return (
|
|
639
|
+
"Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID is invalid or "
|
|
640
|
+
"not configured in Open WebUI. Configure a valid model id in "
|
|
641
|
+
"Admin Panel → Settings → Models."
|
|
642
|
+
)
|
|
643
|
+
return None
|
|
644
|
+
|
|
645
|
+
def _resolve_preprocessor_model_id(self, base_model_id: str | None) -> str | None:
|
|
646
|
+
preprocessor_model_id = _normalize_model_id(self.valves.PREPROCESSOR_MODEL_ID)
|
|
647
|
+
return preprocessor_model_id or base_model_id
|
|
648
|
+
|
|
649
|
+
async def _validate_configured_model_ids(
|
|
650
|
+
self,
|
|
651
|
+
request: Request,
|
|
652
|
+
user_payload: dict[str, Any],
|
|
653
|
+
*,
|
|
654
|
+
base_model_id: str | None,
|
|
655
|
+
preprocessor_model_id: str | None,
|
|
656
|
+
) -> str | None:
|
|
657
|
+
base_model_id = _normalize_model_id(base_model_id)
|
|
658
|
+
preprocessor_model_id = _normalize_model_id(preprocessor_model_id)
|
|
659
|
+
# Best-effort preflight: fail closed only for clear missing-model mismatches.
|
|
660
|
+
# If model discovery fails, preserve runtime behavior and rely on call-path
|
|
661
|
+
# normalization below.
|
|
662
|
+
user = Users.get_user_by_id(user_payload["id"])
|
|
663
|
+
if inspect.isawaitable(user):
|
|
664
|
+
user = await user
|
|
665
|
+
try:
|
|
666
|
+
models = await get_all_models(request, user=user)
|
|
667
|
+
except Exception:
|
|
668
|
+
return None
|
|
669
|
+
|
|
670
|
+
known_model_ids: set[str] = set()
|
|
671
|
+
if isinstance(models, list):
|
|
672
|
+
for model in models:
|
|
673
|
+
if not isinstance(model, dict):
|
|
674
|
+
continue
|
|
675
|
+
model_id = model.get("id")
|
|
676
|
+
if isinstance(model_id, str):
|
|
677
|
+
known_model_ids.add(model_id)
|
|
678
|
+
|
|
679
|
+
if base_model_id and base_model_id not in known_model_ids:
|
|
680
|
+
return (
|
|
681
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID was not found "
|
|
682
|
+
"in Open WebUI models."
|
|
683
|
+
)
|
|
684
|
+
if preprocessor_model_id and preprocessor_model_id not in known_model_ids:
|
|
685
|
+
return (
|
|
686
|
+
"Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID was not found "
|
|
687
|
+
"in Open WebUI models."
|
|
688
|
+
)
|
|
689
|
+
return None
|
|
690
|
+
|
|
691
|
+
async def _llm_fallback_preprocess(
|
|
692
|
+
self,
|
|
693
|
+
message: str,
|
|
694
|
+
state: State,
|
|
695
|
+
*,
|
|
696
|
+
request: Request,
|
|
697
|
+
user_payload: dict[str, Any],
|
|
698
|
+
prompt_profile: str,
|
|
699
|
+
model_id: str | None,
|
|
700
|
+
) -> tuple[str | None, str | None]:
|
|
701
|
+
model_id = _normalize_model_id(model_id)
|
|
702
|
+
if model_id is None:
|
|
703
|
+
return None, None
|
|
704
|
+
with as_file(_prompt_file_path(prompt_profile)) as prompt_path:
|
|
705
|
+
prompt = render_prompt(prompt_path, state)
|
|
706
|
+
if prompt is None:
|
|
707
|
+
return None, None
|
|
708
|
+
|
|
709
|
+
payload: dict[str, Any] = {
|
|
710
|
+
"model": model_id,
|
|
711
|
+
"stream": False,
|
|
712
|
+
"messages": [
|
|
713
|
+
{"role": "system", "content": prompt},
|
|
714
|
+
{"role": "user", "content": message},
|
|
715
|
+
],
|
|
716
|
+
}
|
|
717
|
+
user = Users.get_user_by_id(user_payload["id"])
|
|
718
|
+
if inspect.isawaitable(user):
|
|
719
|
+
user = await user
|
|
720
|
+
try:
|
|
721
|
+
response = await generate_chat_completion(request, payload, user)
|
|
722
|
+
except Exception as exc:
|
|
723
|
+
normalized_exception = self._normalize_preprocessor_exception(exc)
|
|
724
|
+
if normalized_exception is not None:
|
|
725
|
+
return None, normalized_exception
|
|
726
|
+
return None, None
|
|
727
|
+
|
|
728
|
+
normalized_error = self._normalize_preprocessor_error(response)
|
|
729
|
+
if normalized_error is not None:
|
|
730
|
+
return None, normalized_error
|
|
731
|
+
|
|
732
|
+
raw_output = _extract_completion_content(response)
|
|
733
|
+
parsed = parse_preprocessor_output(raw_output)
|
|
734
|
+
if parsed is None:
|
|
735
|
+
return None, None
|
|
736
|
+
return parsed, None
|
|
737
|
+
|
|
738
|
+
async def _preprocess_user_input(
|
|
739
|
+
self,
|
|
740
|
+
message: str,
|
|
741
|
+
state: State,
|
|
742
|
+
*,
|
|
743
|
+
request: Request,
|
|
744
|
+
user_payload: dict[str, Any],
|
|
745
|
+
prompt_profile: str,
|
|
746
|
+
model_id: str | None,
|
|
747
|
+
) -> tuple[str | None, str | None]:
|
|
748
|
+
# Heuristic first for precision, determinism, and low latency.
|
|
749
|
+
# If heuristic does not produce a directive, try Open WebUI-native fallback.
|
|
750
|
+
heuristic_result = preprocess_heuristic(message)
|
|
751
|
+
|
|
752
|
+
if (
|
|
753
|
+
heuristic_result["outcome"] == PREPROCESS_OUTCOME_DIRECTIVE
|
|
754
|
+
and heuristic_result["directive"]
|
|
755
|
+
):
|
|
756
|
+
parsed = parse_preprocessor_output(heuristic_result["directive"])
|
|
757
|
+
if parsed is not None:
|
|
758
|
+
return parsed, None
|
|
759
|
+
|
|
760
|
+
if _is_directive_shaped_input(message):
|
|
761
|
+
return None, None
|
|
762
|
+
|
|
763
|
+
# In debug mode with missing base/preprocessor model ids, skip fallback
|
|
764
|
+
# preprocess entirely so we never attempt an empty-model LLM call.
|
|
765
|
+
model_id = _normalize_model_id(model_id)
|
|
766
|
+
if model_id is None:
|
|
767
|
+
return None, None
|
|
768
|
+
|
|
769
|
+
return await self._llm_fallback_preprocess(
|
|
770
|
+
message,
|
|
771
|
+
state,
|
|
772
|
+
request=request,
|
|
773
|
+
user_payload=user_payload,
|
|
774
|
+
prompt_profile=prompt_profile,
|
|
775
|
+
model_id=model_id,
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
async def _forward_passthrough(
|
|
779
|
+
self,
|
|
780
|
+
body: dict[str, Any],
|
|
781
|
+
user_payload: dict[str, Any],
|
|
782
|
+
request: Request,
|
|
783
|
+
*,
|
|
784
|
+
base_model_id: str | None,
|
|
785
|
+
state: State | None = None,
|
|
786
|
+
) -> Any:
|
|
787
|
+
if base_model_id is None:
|
|
788
|
+
if self._allow_missing_base_model_for_debug():
|
|
789
|
+
return (
|
|
790
|
+
"Context Compiler debug mode: BASE_MODEL_ID is empty; "
|
|
791
|
+
"skipping model passthrough."
|
|
792
|
+
)
|
|
793
|
+
return (
|
|
794
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID is required "
|
|
795
|
+
"(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)."
|
|
796
|
+
)
|
|
797
|
+
payload = {**body}
|
|
798
|
+
payload["model"] = base_model_id
|
|
799
|
+
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)
|
|
800
|
+
user = Users.get_user_by_id(user_payload["id"])
|
|
801
|
+
if inspect.isawaitable(user):
|
|
802
|
+
user = await user
|
|
803
|
+
try:
|
|
804
|
+
response = await generate_chat_completion(request, payload, user)
|
|
805
|
+
except Exception as exc:
|
|
806
|
+
normalized_exception = self._normalize_forward_exception(exc)
|
|
807
|
+
if normalized_exception is not None:
|
|
808
|
+
return normalized_exception
|
|
809
|
+
raise
|
|
810
|
+
normalized_error = self._normalize_forward_error(response)
|
|
811
|
+
if normalized_error is not None:
|
|
812
|
+
return normalized_error
|
|
813
|
+
return response
|
|
814
|
+
|
|
815
|
+
async def _forward_update(
|
|
816
|
+
self,
|
|
817
|
+
body: dict[str, Any],
|
|
818
|
+
user_payload: dict[str, Any],
|
|
819
|
+
request: Request,
|
|
820
|
+
state: State,
|
|
821
|
+
*,
|
|
822
|
+
base_model_id: str | None,
|
|
823
|
+
) -> Any:
|
|
824
|
+
if base_model_id is None:
|
|
825
|
+
if self._allow_missing_base_model_for_debug():
|
|
826
|
+
return (
|
|
827
|
+
"Context Compiler debug mode: BASE_MODEL_ID is empty; "
|
|
828
|
+
"skipping model passthrough."
|
|
829
|
+
)
|
|
830
|
+
return (
|
|
831
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID is required "
|
|
832
|
+
"(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)."
|
|
833
|
+
)
|
|
834
|
+
payload = {**body}
|
|
835
|
+
payload["model"] = base_model_id
|
|
836
|
+
|
|
837
|
+
payload["messages"] = _build_forward_messages(body.get("messages"), state=state)
|
|
838
|
+
|
|
839
|
+
user = Users.get_user_by_id(user_payload["id"])
|
|
840
|
+
if inspect.isawaitable(user):
|
|
841
|
+
user = await user
|
|
842
|
+
try:
|
|
843
|
+
response = await generate_chat_completion(request, payload, user)
|
|
844
|
+
except Exception as exc:
|
|
845
|
+
normalized_exception = self._normalize_forward_exception(exc)
|
|
846
|
+
if normalized_exception is not None:
|
|
847
|
+
return normalized_exception
|
|
848
|
+
raise
|
|
849
|
+
normalized_error = self._normalize_forward_error(response)
|
|
850
|
+
if normalized_error is not None:
|
|
851
|
+
return normalized_error
|
|
852
|
+
return response
|
|
853
|
+
|
|
854
|
+
async def pipe(
|
|
855
|
+
self,
|
|
856
|
+
body: dict[str, Any],
|
|
857
|
+
__user__: dict[str, Any],
|
|
858
|
+
__request__: Request,
|
|
859
|
+
__chat_id__: str | None = None,
|
|
860
|
+
__metadata__: dict[str, Any] | None = None,
|
|
861
|
+
) -> Any:
|
|
862
|
+
# Open WebUI integration entrypoint:
|
|
863
|
+
# 1) extract latest user input
|
|
864
|
+
# 2) run preprocess (heuristic -> LLM fallback)
|
|
865
|
+
# 3) pass directive or original input to engine.step(...)
|
|
866
|
+
# 4) map decision back to Open WebUI response behavior
|
|
867
|
+
raw_messages = body.get("messages")
|
|
868
|
+
messages = (
|
|
869
|
+
[msg for msg in raw_messages if isinstance(msg, dict)]
|
|
870
|
+
if isinstance(raw_messages, list)
|
|
871
|
+
else []
|
|
872
|
+
)
|
|
873
|
+
base_model_id = _normalize_model_id(self.valves.BASE_MODEL_ID)
|
|
874
|
+
preprocessor_model_id = _normalize_model_id(self.valves.PREPROCESSOR_MODEL_ID)
|
|
875
|
+
effective_preprocessor_model = preprocessor_model_id or base_model_id
|
|
876
|
+
current_model_id = str(body.get("model", "")).strip()
|
|
877
|
+
|
|
878
|
+
if not base_model_id and not self._allow_missing_base_model_for_debug():
|
|
879
|
+
return (
|
|
880
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID is required "
|
|
881
|
+
"(or set ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true for testing)."
|
|
882
|
+
)
|
|
883
|
+
if base_model_id and current_model_id and base_model_id == current_model_id:
|
|
884
|
+
return (
|
|
885
|
+
"Context Compiler pipe misconfigured: BASE_MODEL_ID must not match "
|
|
886
|
+
"the selected pipe model id to avoid recursive routing."
|
|
887
|
+
)
|
|
888
|
+
if (
|
|
889
|
+
effective_preprocessor_model
|
|
890
|
+
and current_model_id
|
|
891
|
+
and effective_preprocessor_model == current_model_id
|
|
892
|
+
):
|
|
893
|
+
return (
|
|
894
|
+
"Context Compiler pipe misconfigured: PREPROCESSOR_MODEL_ID must not "
|
|
895
|
+
"match the selected pipe model id to avoid recursive routing."
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
preflight_error = await self._validate_configured_model_ids(
|
|
899
|
+
__request__,
|
|
900
|
+
__user__,
|
|
901
|
+
base_model_id=base_model_id,
|
|
902
|
+
preprocessor_model_id=effective_preprocessor_model,
|
|
903
|
+
)
|
|
904
|
+
if preflight_error is not None:
|
|
905
|
+
return preflight_error
|
|
906
|
+
|
|
907
|
+
latest_user_text = _extract_latest_user_text(messages)
|
|
908
|
+
logger.debug("preprocessor: user_input_found=%s", latest_user_text is not None)
|
|
909
|
+
|
|
910
|
+
if latest_user_text is None:
|
|
911
|
+
return await self._forward_passthrough(
|
|
912
|
+
body,
|
|
913
|
+
__user__,
|
|
914
|
+
__request__,
|
|
915
|
+
base_model_id=base_model_id,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
chat_key = _resolve_chat_key(__user__, __chat_id__, __metadata__)
|
|
919
|
+
engine = _ENGINES_BY_CHAT_KEY.get(chat_key)
|
|
920
|
+
if engine is None:
|
|
921
|
+
engine = create_engine()
|
|
922
|
+
checkpoint = _CHECKPOINTS_BY_CHAT_KEY.get(chat_key)
|
|
923
|
+
if checkpoint is not None:
|
|
924
|
+
engine.import_checkpoint_json(checkpoint)
|
|
925
|
+
_ENGINES_BY_CHAT_KEY[chat_key] = engine
|
|
926
|
+
|
|
927
|
+
if latest_user_text.strip().lower() == "show state":
|
|
928
|
+
return _render_show_state_summary(engine)
|
|
929
|
+
|
|
930
|
+
state_before = engine.state
|
|
931
|
+
|
|
932
|
+
preprocessd: str | None = None
|
|
933
|
+
preprocess_error: str | None = None
|
|
934
|
+
if not _has_pending_clarification(engine):
|
|
935
|
+
preprocessd, preprocess_error = await self._preprocess_user_input(
|
|
936
|
+
latest_user_text,
|
|
937
|
+
engine.state,
|
|
938
|
+
request=__request__,
|
|
939
|
+
user_payload=__user__,
|
|
940
|
+
prompt_profile=self.valves.PREPROCESSOR_PROMPT_PROFILE,
|
|
941
|
+
model_id=effective_preprocessor_model,
|
|
942
|
+
)
|
|
943
|
+
if preprocess_error is not None:
|
|
944
|
+
return preprocess_error
|
|
945
|
+
|
|
946
|
+
logger.debug("preprocessor: preprocessd=%r", preprocessd)
|
|
947
|
+
# Preserve core behavior: if preprocess yields no directive, use raw user
|
|
948
|
+
# text so the compiler still decides clarify/passthrough/update.
|
|
949
|
+
compile_input = preprocessd if preprocessd is not None else latest_user_text
|
|
950
|
+
|
|
951
|
+
logger.debug("preprocessor: engine_input=%r", compile_input)
|
|
952
|
+
decision = engine.step(compile_input)
|
|
953
|
+
if is_clarify(decision):
|
|
954
|
+
kind = DECISION_CLARIFY
|
|
955
|
+
elif is_update(decision):
|
|
956
|
+
kind = DECISION_UPDATE
|
|
957
|
+
else:
|
|
958
|
+
kind = DECISION_PASSTHROUGH
|
|
959
|
+
logger.debug("preprocessor: decision=%s", kind)
|
|
960
|
+
near_miss_prompt = _near_miss_directive_clarify(latest_user_text)
|
|
961
|
+
state_after = get_decision_state(decision)
|
|
962
|
+
if state_after is None:
|
|
963
|
+
state_after = engine.state
|
|
964
|
+
|
|
965
|
+
if is_clarify(decision):
|
|
966
|
+
_CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json()
|
|
967
|
+
return self._with_trace(
|
|
968
|
+
near_miss_prompt or get_clarify_prompt(decision) or "",
|
|
969
|
+
original_input=latest_user_text,
|
|
970
|
+
compiler_input=compile_input,
|
|
971
|
+
decision=decision,
|
|
972
|
+
state_before=state_before,
|
|
973
|
+
state_after=state_after,
|
|
974
|
+
preprocessor_output=preprocessd,
|
|
975
|
+
llm_called=False,
|
|
976
|
+
)
|
|
977
|
+
if near_miss_prompt is not None and is_passthrough(decision):
|
|
978
|
+
return self._with_trace(
|
|
979
|
+
near_miss_prompt,
|
|
980
|
+
original_input=latest_user_text,
|
|
981
|
+
compiler_input=compile_input,
|
|
982
|
+
decision={"kind": DECISION_CLARIFY, "prompt_to_user": near_miss_prompt},
|
|
983
|
+
state_before=state_before,
|
|
984
|
+
state_after=state_after,
|
|
985
|
+
preprocessor_output=preprocessd,
|
|
986
|
+
llm_called=False,
|
|
987
|
+
)
|
|
988
|
+
if is_passthrough(decision):
|
|
989
|
+
compiled_state = _normalize_state(state_after)
|
|
990
|
+
state_injected = (
|
|
991
|
+
"yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
|
|
992
|
+
)
|
|
993
|
+
response = await self._forward_passthrough(
|
|
994
|
+
body,
|
|
995
|
+
__user__,
|
|
996
|
+
__request__,
|
|
997
|
+
base_model_id=base_model_id,
|
|
998
|
+
state=compiled_state,
|
|
999
|
+
)
|
|
1000
|
+
return self._with_trace(
|
|
1001
|
+
response,
|
|
1002
|
+
original_input=latest_user_text,
|
|
1003
|
+
compiler_input=compile_input,
|
|
1004
|
+
decision=decision,
|
|
1005
|
+
state_before=state_before,
|
|
1006
|
+
state_after=state_after,
|
|
1007
|
+
preprocessor_output=preprocessd,
|
|
1008
|
+
llm_called=base_model_id is not None,
|
|
1009
|
+
state_injected=state_injected,
|
|
1010
|
+
)
|
|
1011
|
+
if is_update(decision):
|
|
1012
|
+
_CHECKPOINTS_BY_CHAT_KEY[chat_key] = engine.export_checkpoint_json()
|
|
1013
|
+
return self._with_trace(
|
|
1014
|
+
_summarize_update_from_input(compile_input),
|
|
1015
|
+
original_input=latest_user_text,
|
|
1016
|
+
compiler_input=compile_input,
|
|
1017
|
+
decision=decision,
|
|
1018
|
+
state_before=state_before,
|
|
1019
|
+
state_after=state_after,
|
|
1020
|
+
preprocessor_output=preprocessd,
|
|
1021
|
+
llm_called=False,
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
compiled_state = _normalize_state(state_after)
|
|
1025
|
+
state_injected = (
|
|
1026
|
+
"yes" if _has_non_empty_authoritative_state(compiled_state) else "no"
|
|
1027
|
+
)
|
|
1028
|
+
response = await self._forward_passthrough(
|
|
1029
|
+
body,
|
|
1030
|
+
__user__,
|
|
1031
|
+
__request__,
|
|
1032
|
+
base_model_id=base_model_id,
|
|
1033
|
+
state=compiled_state,
|
|
1034
|
+
)
|
|
1035
|
+
return self._with_trace(
|
|
1036
|
+
response,
|
|
1037
|
+
original_input=latest_user_text,
|
|
1038
|
+
compiler_input=compile_input,
|
|
1039
|
+
decision=decision,
|
|
1040
|
+
state_before=state_before,
|
|
1041
|
+
state_after=state_after,
|
|
1042
|
+
preprocessor_output=preprocessd,
|
|
1043
|
+
llm_called=base_model_id is not None,
|
|
1044
|
+
state_injected=state_injected,
|
|
1045
|
+
)
|