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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions PROVIDER_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export PROVIDER=openai
export MODEL=openai/gpt-4o-mini
export PREPROCESSOR_MODEL=openai/gpt-4o-mini
export OPENAI_BASE_URL=...
export PREPROCESSOR_PROMPT_PROFILE=default
```

Provider mode contract (`PROVIDER`) is strict:
Expand Down Expand Up @@ -52,5 +51,5 @@ Startup emits one concise config line showing resolved `mode`, `base_url`,
`MODEL` and `PREPROCESSOR_MODEL` use LiteLLM format: `<provider>/<model>`.
`PREPROCESSOR_MODEL` is optional and defaults to `MODEL`.

For heuristic-first usage, keep `PREPROCESSOR_PROMPT_PROFILE=default`.
Use `llama` only for LLM-only preprocessing with Llama-family models.
The directive-drafter integration always uses heuristic-first processing with
the configured fallback model when needed.
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ Issues = "https://github.com/rlippmann/context-compiler-example-integrations/iss
[project.optional-dependencies]
all = [
"chromadb",
"context-compiler-directive-drafter==0.2.0dev4",
"context-compiler-directive-drafter==0.2.0dev5",
"fastapi",
"litellm",
]
drafter = [
"context-compiler-directive-drafter==0.2.0dev4",
"context-compiler-directive-drafter==0.2.0dev5",
]
fastapi = [
"fastapi",
Expand All @@ -67,7 +67,7 @@ retrieval = [
[dependency-groups]
dev = [
"chromadb",
"context-compiler-directive-drafter==0.2.0dev4",
"context-compiler-directive-drafter==0.2.0dev5",
"fastapi",
"httpx2>=2.5.0",
"httpx>=0.28.1",
Expand Down
2 changes: 1 addition & 1 deletion python/examples/prompt_construction/litellm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ For `with_directive_drafter.py`:
pip install "context-compiler-example-integrations[all]"
```

That variant requires `context-compiler-directive-drafter>=0.2.0dev4`.
That variant requires `context-compiler-directive-drafter>=0.2.0dev5`.

## Quickstart (copy/paste)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import logging
import os
from collections.abc import Callable, Mapping, Sequence
from functools import lru_cache
from importlib import import_module
from typing import TypedDict, cast

Expand All @@ -37,7 +38,9 @@
RejectedDirective,
UnknownDirective,
)
from context_compiler_directive_drafter.fallbacks import get_converter_prompt
from context_compiler_directive_drafter.fallbacks.litellm import (
create_litellm_fallback,
)

from context_compiler_example_integrations.examples._shared.provider_mode import (
print_startup_config,
Expand Down Expand Up @@ -82,12 +85,6 @@ def _extract_response_content(response: object) -> str | None:
return None


_DIRECTIVE_DRAFTER = DirectiveDrafter(
fallback=lambda message: _llm_fallback_candidate(message),
fallback_source="litellm_fallback",
)


def _render_state_lines(
premise: str | None, policies: Mapping[str, PolicyValue]
) -> list[str]:
Expand Down Expand Up @@ -211,44 +208,31 @@ def _call_litellm(messages: list[dict[str, str]]) -> str:
return content


def _llm_fallback_candidate(message: str) -> str | None:
try:
completion = _get_litellm_completion()
except ModuleNotFoundError:
return None
@lru_cache(maxsize=8)
def _create_directive_drafter(
model: str, api_key: str | None, api_base: str
) -> DirectiveDrafter:
return DirectiveDrafter(
fallback=create_litellm_fallback(
model=model,
api_key=api_key,
api_base=api_base,
),
fallback_source="litellm_fallback",
)

try:
config = resolve_provider_config(default_model="openai/gpt-4o-mini")
except RuntimeError:
return None
if config.mode == "openai" and not config.api_key:
return None
preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip()
if not preprocessor_model:
preprocessor_model = os.getenv("MODEL", "openai/gpt-4o-mini")

kwargs: _LiteLLMCallKwargs = {
"model": preprocessor_model,
"messages": [
{"role": "system", "content": get_converter_prompt()},
{"role": "user", "content": message},
],
"temperature": 0,
"api_base": config.base_url,
}
if config.api_key:
kwargs["api_key"] = config.api_key

try:
response = completion(**kwargs)
return _extract_response_content(response)
except Exception:
return None
def _get_directive_drafter() -> DirectiveDrafter:
config = resolve_provider_config(default_model="openai/gpt-4o-mini")
preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip() or config.model
return _create_directive_drafter(
preprocessor_model, config.api_key, config.base_url
)


def _preprocess_user_input(message: str) -> str | None:
try:
drafted_result = _DIRECTIVE_DRAFTER.draft_directive(message)
drafted_result = _get_directive_drafter().draft_directive(message)
logger.debug("preprocessor: drafted_result=%r", drafted_result)
return _extract_drafted_text(drafted_result)
except Exception:
Expand Down
7 changes: 3 additions & 4 deletions python/reference_integrations/litellm_proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ For `context_compiler_precall_hook_with_directive_drafter.py`:
pip install "context-compiler-example-integrations[all]"
```

That variant requires `context-compiler-directive-drafter>=0.2.0dev4`.
That variant requires `context-compiler-directive-drafter>=0.2.0dev5`.

For the opt-in runtime smoke test, install the proxy runtime extras:

Expand Down Expand Up @@ -202,13 +202,12 @@ Optional env vars for directive-drafter fallback:

```shell
export PREPROCESSOR_MODEL=openai/gpt-4o-mini
export PREPROCESSOR_PROMPT_PROFILE=default
```

`PREPROCESSOR_MODEL` is optional and defaults to `MODEL`.

For heuristic-first usage, keep `PREPROCESSOR_PROMPT_PROFILE=default`.
Use `llama` only for LLM-only fallback drafting with Llama-family models.
The directive-drafter integration always uses heuristic-first processing with
the configured fallback model when needed.

## Notes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@

import logging
import os
from collections.abc import Callable, Mapping, Sequence
from importlib import import_module
from typing import Any, cast
from functools import lru_cache
from typing import Any

try:
from litellm.integrations.custom_logger import CustomLogger
Expand All @@ -33,7 +32,9 @@ class CustomLogger: # type: ignore[no-redef]
DirectiveDrafter,
DraftResult,
)
from context_compiler_directive_drafter.fallbacks import get_converter_prompt
from context_compiler_directive_drafter.fallbacks.litellm import (
create_litellm_fallback,
)
from context_compiler_example_integrations.reference_integrations.litellm_proxy._checkpoint_support import (
MODE_PERSISTENT,
CheckpointStore,
Expand All @@ -60,75 +61,41 @@ class CustomLogger: # type: ignore[no-redef]
CHECKPOINT_STORE: CheckpointStore = InMemoryCheckpointStore()


def _extract_response_content(response: object) -> str | None:
if isinstance(response, Mapping):
choices = response.get("choices")
if isinstance(choices, Sequence) and choices:
first = choices[0]
if isinstance(first, Mapping):
message = first.get("message")
if isinstance(message, Mapping):
content = message.get("content")
if isinstance(content, str):
return content

choices_attr = getattr(response, "choices", None)
if isinstance(choices_attr, Sequence) and choices_attr:
first = choices_attr[0]
message_attr = getattr(first, "message", None)
content_attr = getattr(message_attr, "content", None)
if isinstance(content_attr, str):
return content_attr

return None


def _get_litellm_completion() -> Callable[..., object]:
litellm_module = import_module("litellm")
return cast(Callable[..., object], litellm_module.completion)
@lru_cache(maxsize=8)
def _create_directive_drafter(
model: str, api_key: str, api_base: str | None
) -> DirectiveDrafter:
return DirectiveDrafter(
fallback=create_litellm_fallback(
model=model,
api_key=api_key,
api_base=api_base,
),
fallback_source="litellm_fallback",
)


def _llm_fallback_candidate(message: str) -> str | None:
def _get_directive_drafter() -> DirectiveDrafter:
preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip()
if not preprocessor_model:
preprocessor_model = os.getenv("MODEL", "").strip()
if not preprocessor_model:
return None
return DirectiveDrafter()

api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
return None
return DirectiveDrafter()
return _create_directive_drafter(
preprocessor_model, api_key, os.getenv("OPENAI_BASE_URL") or None
)

try:
completion = _get_litellm_completion()
except ModuleNotFoundError:
return None

kwargs: dict[str, object] = {
"model": preprocessor_model,
"messages": [
{"role": "system", "content": get_converter_prompt()},
{"role": "user", "content": message},
],
"api_key": api_key,
"temperature": 0,
}
api_base = os.getenv("OPENAI_BASE_URL")
if api_base:
kwargs["api_base"] = api_base

def _draft_last_user_message(message: str) -> DraftResult:
try:
response = completion(**kwargs)
return _extract_response_content(response)
return _get_directive_drafter().draft_directive(message)
except Exception:
return None


def _draft_last_user_message(message: str) -> DraftResult:
drafter = DirectiveDrafter(
fallback=_llm_fallback_candidate, fallback_source="litellm_fallback"
)
return drafter.draft_directive(message)
logger.debug("litellm_proxy: drafter_exception", exc_info=True)
return DirectiveDrafter().draft_directive(message)


class ContextCompilerPreCallHookWithPreprocessor(CustomLogger):
Expand Down
8 changes: 3 additions & 5 deletions python/reference_integrations/openwebui_pipe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,9 @@ These examples require `context-compiler>=0.9.0dev13`.
If using `open_webui_pipe_with_directive_drafter.py`:

- Install directive-drafter support if needed:
`pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev4"`
- Set `PREPROCESSOR_PROMPT_PROFILE=default` for heuristic-first behavior
`pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev5"`
- Optionally set `PREPROCESSOR_MODEL_ID` to use a separate fallback drafting model
- If `PREPROCESSOR_MODEL_ID` is unset, fallback uses `BASE_MODEL_ID`
- Use `llama` only for LLM-only fallback drafting with Llama-family models

Model fallback output is structurally validated before handoff. This does not prove that the model interpreted the user correctly. The automated fallback path is experimental pending a separate source-aware acceptance policy and reviewed drafting workflow.

Expand All @@ -86,7 +84,7 @@ If frontmatter dependency installs are disabled, offline, or unavailable:
1. Install the package manually:

- Minimal pipe: `pip install "context-compiler>=0.9.0dev13"`
- Directive Drafter pipe: `pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev4"`
- Directive Drafter pipe: `pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev5"`

1. Import and enable the function in Open WebUI, then configure valves.

Expand Down Expand Up @@ -246,7 +244,7 @@ rejection flows.
- `PREPROCESSOR_MODEL_ID must not match the selected pipe model id`: choose a real backend model id, not the pipe model id itself.
- `PREPROCESSOR_MODEL_ID is invalid or not configured in Open WebUI`: the fallback route hit a missing model; fix the configured fallback model or unset it to reuse `BASE_MODEL_ID`.
- `ALLOW_MISSING_BASE_MODEL_FOR_DEBUG=true`: directive-only updates still run locally, but passthrough returns a deterministic debug message instead of calling a downstream model.
- imports fail after function upload: install `context-compiler>=0.9.0dev13` in the Open WebUI runtime, and add `context-compiler-directive-drafter>=0.2.0dev4` only for the Directive Drafter pipe, because the copied function runs from a temp/cached location.
- imports fail after function upload: install `context-compiler>=0.9.0dev13` in the Open WebUI runtime, and add `context-compiler-directive-drafter>=0.2.0dev5` only for the Directive Drafter pipe, because the copied function runs from a temp/cached location.

## Fallback notes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
author: rlippmann
author_url: https://github.com/rlippmann/context-compiler-example-integrations
version: 0.10.1
requirements: context-compiler>=0.9.0dev13, context-compiler-directive-drafter>=0.2.0dev4
requirements: context-compiler>=0.9.0dev13, context-compiler-directive-drafter>=0.2.0dev5

Open WebUI integration with Context Compiler directive drafter.

Expand All @@ -24,7 +24,7 @@
import logging
import re
from collections.abc import AsyncIterator
from typing import Any, Literal, TypedDict, cast
from typing import Any, TypedDict, cast

from fastapi import Request # type: ignore[import-not-found]
from open_webui.models.users import Users # type: ignore[import-not-found]
Expand Down Expand Up @@ -62,10 +62,12 @@ def Field(*, default: Any, description: str = "") -> Any: # type: ignore[no-red
RejectedDirective,
UnknownDirective,
)
from context_compiler_directive_drafter.fallbacks import get_converter_prompt
from context_compiler_directive_drafter.fallbacks import get_fallback_profile

logger = logging.getLogger(__name__)

_FALLBACK_PROFILE = get_fallback_profile()

_CC_MARKER = "[[cc_state]]"
_ENGINES_BY_CHAT_KEY: dict[str, Engine] = {}

Expand Down Expand Up @@ -381,10 +383,6 @@ class Valves(BaseModel):
"Optional model id for fallback drafting (defaults to BASE_MODEL_ID)."
),
)
PREPROCESSOR_PROMPT_PROFILE: Literal["default", "llama"] = Field(
default="default",
description="Prompt profile for LLM fallback drafting.",
)
ALLOW_MISSING_BASE_MODEL_FOR_DEBUG: bool = Field(
default=False,
description="Allow missing BASE_MODEL_ID for debug/testing only.",
Expand Down Expand Up @@ -634,7 +632,7 @@ async def _llm_fallback_candidate(
"model": model_id,
"stream": False,
"messages": [
{"role": "system", "content": get_converter_prompt()},
{"role": "system", "content": _FALLBACK_PROFILE.system_prompt},
{"role": "user", "content": message},
],
}
Expand Down Expand Up @@ -695,10 +693,8 @@ async def _preprocess_user_input(
*,
request: Request,
user_payload: dict[str, Any],
prompt_profile: str,
model_id: str | None,
) -> tuple[DraftResult, str | None]:
del prompt_profile
self._last_preprocessor_error = None
drafted_result = await self._draft_user_input(
message,
Expand Down Expand Up @@ -892,7 +888,6 @@ async def pipe(
latest_user_text,
request=__request__,
user_payload=__user__,
prompt_profile=self.valves.PREPROCESSOR_PROMPT_PROFILE,
model_id=effective_preprocessor_model,
)
if preprocess_error is not None:
Expand Down
Loading