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.
Files changed (65) hide show
  1. context_compiler_example_integrations/README.md +71 -0
  2. context_compiler_example_integrations/__init__.py +1 -0
  3. context_compiler_example_integrations/examples/__init__.py +12 -0
  4. context_compiler_example_integrations/examples/_shared/__init__.py +1 -0
  5. context_compiler_example_integrations/examples/_shared/litellm_request.py +61 -0
  6. context_compiler_example_integrations/examples/_shared/provider_mode.py +91 -0
  7. context_compiler_example_integrations/examples/checkpoint_continuation/README.md +77 -0
  8. context_compiler_example_integrations/examples/checkpoint_continuation/example.py +169 -0
  9. context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/README.md +85 -0
  10. context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/app.py +205 -0
  11. context_compiler_example_integrations/examples/execution_authorization/README.md +31 -0
  12. context_compiler_example_integrations/examples/execution_authorization/expense_approval/README.md +77 -0
  13. context_compiler_example_integrations/examples/execution_authorization/expense_approval/__init__.py +1 -0
  14. context_compiler_example_integrations/examples/execution_authorization/expense_approval/example.py +168 -0
  15. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/README.md +97 -0
  16. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/__init__.py +1 -0
  17. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/app.py +312 -0
  18. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/model_approval.py +98 -0
  19. context_compiler_example_integrations/examples/gateway_middleware/README.md +49 -0
  20. context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/README.md +40 -0
  21. context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/__init__.py +1 -0
  22. context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/example.py +220 -0
  23. context_compiler_example_integrations/examples/prompt_construction/README.md +27 -0
  24. context_compiler_example_integrations/examples/prompt_construction/litellm/README.md +197 -0
  25. context_compiler_example_integrations/examples/prompt_construction/litellm/basic.py +392 -0
  26. context_compiler_example_integrations/examples/prompt_construction/litellm/confirmation_helper.py +91 -0
  27. context_compiler_example_integrations/examples/prompt_construction/litellm/with_directive_drafter.py +524 -0
  28. context_compiler_example_integrations/examples/prompt_construction/writing_assistant/README.md +92 -0
  29. context_compiler_example_integrations/examples/prompt_construction/writing_assistant/__init__.py +1 -0
  30. context_compiler_example_integrations/examples/prompt_construction/writing_assistant/example.py +205 -0
  31. context_compiler_example_integrations/examples/retrieval_filtering/README.md +65 -0
  32. context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/README.md +120 -0
  33. context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/__init__.py +1 -0
  34. context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/example.py +320 -0
  35. context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/README.md +123 -0
  36. context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/__init__.py +1 -0
  37. context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/example.py +300 -0
  38. context_compiler_example_integrations/examples/schema_selection/README.md +41 -0
  39. context_compiler_example_integrations/examples/schema_selection/litellm_response_format/response_format.py +218 -0
  40. context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/README.md +60 -0
  41. context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/example.py +171 -0
  42. context_compiler_example_integrations/examples/schema_selection/refund_intake/README.md +95 -0
  43. context_compiler_example_integrations/examples/schema_selection/refund_intake/__init__.py +1 -0
  44. context_compiler_example_integrations/examples/schema_selection/refund_intake/example.py +180 -0
  45. context_compiler_example_integrations/examples/tool_gating/README.md +55 -0
  46. context_compiler_example_integrations/examples/tool_gating/calendar_admin/README.md +34 -0
  47. context_compiler_example_integrations/examples/tool_gating/calendar_admin/__init__.py +1 -0
  48. context_compiler_example_integrations/examples/tool_gating/calendar_admin/example.py +195 -0
  49. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/README.md +93 -0
  50. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/__init__.py +1 -0
  51. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/example.py +250 -0
  52. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/live_model.py +369 -0
  53. context_compiler_example_integrations/reference_integrations/__init__.py +12 -0
  54. context_compiler_example_integrations/reference_integrations/litellm_proxy/README.md +251 -0
  55. context_compiler_example_integrations/reference_integrations/litellm_proxy/_checkpoint_support.py +146 -0
  56. context_compiler_example_integrations/reference_integrations/litellm_proxy/config.example.yaml +23 -0
  57. context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook.py +163 -0
  58. context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +289 -0
  59. context_compiler_example_integrations/reference_integrations/openwebui_pipe/README.md +233 -0
  60. context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe.py +750 -0
  61. context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +1045 -0
  62. context_compiler_example_integrations-0.1.0.dist-info/METADATA +110 -0
  63. context_compiler_example_integrations-0.1.0.dist-info/RECORD +65 -0
  64. context_compiler_example_integrations-0.1.0.dist-info/WHEEL +4 -0
  65. context_compiler_example_integrations-0.1.0.dist-info/licenses/LICENSE +173 -0
@@ -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
+ )