diff --git a/pyproject.toml b/pyproject.toml index e9fb6eb..2d9fbe0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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.0dev2", + "context-compiler-directive-drafter==0.2.0dev4", "fastapi", "litellm", ] drafter = [ - "context-compiler-directive-drafter==0.2.0dev2", + "context-compiler-directive-drafter==0.2.0dev4", ] fastapi = [ "fastapi", @@ -67,7 +67,7 @@ retrieval = [ [dependency-groups] dev = [ "chromadb", - "context-compiler-directive-drafter==0.2.0dev2", + "context-compiler-directive-drafter==0.2.0dev4", "fastapi", "httpx2>=2.5.0", "httpx>=0.28.1", diff --git a/python/examples/prompt_construction/litellm/README.md b/python/examples/prompt_construction/litellm/README.md index 0cfeca2..b323bad 100644 --- a/python/examples/prompt_construction/litellm/README.md +++ b/python/examples/prompt_construction/litellm/README.md @@ -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.0dev2`. +That variant requires `context-compiler-directive-drafter>=0.2.0dev4`. ## Quickstart (copy/paste) diff --git a/python/examples/prompt_construction/litellm/with_directive_drafter.py b/python/examples/prompt_construction/litellm/with_directive_drafter.py index f05a18f..59b79ec 100644 --- a/python/examples/prompt_construction/litellm/with_directive_drafter.py +++ b/python/examples/prompt_construction/litellm/with_directive_drafter.py @@ -34,10 +34,10 @@ from context_compiler_directive_drafter import ( DraftResult, DirectiveDrafter, - NoDirective, + RejectedDirective, UnknownDirective, - get_converter_prompt, ) +from context_compiler_directive_drafter.fallbacks import get_converter_prompt from context_compiler_example_integrations.examples._shared.provider_mode import ( print_startup_config, @@ -262,7 +262,7 @@ def _extract_drafted_text(drafted_result: DraftResult) -> str | None: result = drafted_result.result if isinstance(result, CanonicalDirective): return result.text - if isinstance(result, NoDirective): + if isinstance(result, RejectedDirective): return None if isinstance(result, UnknownDirective): return None diff --git a/python/reference_integrations/litellm_proxy/README.md b/python/reference_integrations/litellm_proxy/README.md index 39d374b..4143e39 100644 --- a/python/reference_integrations/litellm_proxy/README.md +++ b/python/reference_integrations/litellm_proxy/README.md @@ -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.0dev2`. +That variant requires `context-compiler-directive-drafter>=0.2.0dev4`. For the opt-in runtime smoke test, install the proxy runtime extras: diff --git a/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py b/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py index 7db163a..9a6af30 100644 --- a/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +++ b/python/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py @@ -32,8 +32,8 @@ class CustomLogger: # type: ignore[no-redef] from context_compiler_directive_drafter import ( DirectiveDrafter, DraftResult, - get_converter_prompt, ) +from context_compiler_directive_drafter.fallbacks import get_converter_prompt from context_compiler_example_integrations.reference_integrations.litellm_proxy._checkpoint_support import ( MODE_PERSISTENT, CheckpointStore, diff --git a/python/reference_integrations/openwebui_pipe/README.md b/python/reference_integrations/openwebui_pipe/README.md index bef080c..33b7390 100644 --- a/python/reference_integrations/openwebui_pipe/README.md +++ b/python/reference_integrations/openwebui_pipe/README.md @@ -68,7 +68,7 @@ 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.0dev2"` + `pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev4"` - Set `PREPROCESSOR_PROMPT_PROFILE=default` for heuristic-first behavior - Optionally set `PREPROCESSOR_MODEL_ID` to use a separate fallback drafting model - If `PREPROCESSOR_MODEL_ID` is unset, fallback uses `BASE_MODEL_ID` @@ -86,7 +86,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.0dev2"` +- Directive Drafter pipe: `pip install "context-compiler>=0.9.0dev13" "context-compiler-directive-drafter>=0.2.0dev4"` 1. Import and enable the function in Open WebUI, then configure valves. @@ -246,7 +246,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.0dev2` 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.0dev4` only for the Directive Drafter pipe, because the copied function runs from a temp/cached location. ## Fallback notes diff --git a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py index 42c89eb..12b636d 100644 --- a/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +++ b/python/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py @@ -2,8 +2,8 @@ title: Context Compiler Open WebUI Pipe (Directive Drafter) author: rlippmann author_url: https://github.com/rlippmann/context-compiler-example-integrations -version: 0.10.0 -requirements: context-compiler>=0.9.0dev13, context-compiler-directive-drafter>=0.2.0dev2 +version: 0.10.1 +requirements: context-compiler>=0.9.0dev13, context-compiler-directive-drafter>=0.2.0dev4 Open WebUI integration with Context Compiler directive drafter. @@ -59,10 +59,10 @@ def Field(*, default: Any, description: str = "") -> Any: # type: ignore[no-red from context_compiler_directive_drafter import ( DirectiveDrafter, DraftResult, - NoDirective, + RejectedDirective, UnknownDirective, - get_converter_prompt, ) +from context_compiler_directive_drafter.fallbacks import get_converter_prompt logger = logging.getLogger(__name__) @@ -683,7 +683,7 @@ async def fallback(candidate_message: str) -> str | None: def _extract_drafted_text(self, drafted_result: DraftResult) -> str | None: if isinstance(drafted_result.result, CanonicalDirective): return drafted_result.result.text - if isinstance(drafted_result.result, NoDirective): + if isinstance(drafted_result.result, RejectedDirective): return None if isinstance(drafted_result.result, UnknownDirective): return None diff --git a/python/tests/test_litellm_proxy_with_directive_drafter.py b/python/tests/test_litellm_proxy_with_directive_drafter.py index d5909b2..4769b58 100644 --- a/python/tests/test_litellm_proxy_with_directive_drafter.py +++ b/python/tests/test_litellm_proxy_with_directive_drafter.py @@ -8,7 +8,11 @@ import pytest from context_compiler.grammar import decompose_directive -from context_compiler_directive_drafter import NoDirective, UnknownDirective +from context_compiler_directive_drafter import ( + REASON_MULTIPLE_DIRECTIVES, + REASON_NON_DIRECTIVE, + RejectedDirective, +) REPO_ROOT = Path(__file__).resolve().parents[2] MODULE_PATH = ( @@ -51,7 +55,7 @@ def fake_draft(message: str) -> object: drafted_calls.append((message, {})) return module.DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module, "_draft_last_user_message", fake_draft) @@ -156,7 +160,7 @@ def test_default_mode_is_stateless_and_requires_no_session_key(monkeypatch) -> N "_draft_last_user_message", lambda message: module.DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ), ) data = { @@ -177,7 +181,7 @@ def test_stateless_mode_has_no_cross_call_continuity(monkeypatch) -> None: "_draft_last_user_message", lambda message: module.DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ), ) first = { @@ -307,7 +311,7 @@ def fake_draft(message: str) -> object: seen_messages.append(message) return module.DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module, "_draft_last_user_message", fake_draft) @@ -381,7 +385,7 @@ def test_compound_directives_fall_through_to_normal_forwarding_when_not_applied( "_draft_last_user_message", lambda _message: module.DraftResult( source="test", - result=UnknownDirective(reason="reject.multi_candidate_directive"), + result=RejectedDirective(reason=REASON_MULTIPLE_DIRECTIVES), ), ) data = { diff --git a/python/tests/test_litellm_with_directive_drafter.py b/python/tests/test_litellm_with_directive_drafter.py index 08f6f32..91620cc 100644 --- a/python/tests/test_litellm_with_directive_drafter.py +++ b/python/tests/test_litellm_with_directive_drafter.py @@ -7,7 +7,9 @@ from context_compiler_directive_drafter import ( DirectiveDrafter, DraftResult, - NoDirective, + REASON_INCOMPLETE, + REASON_NON_DIRECTIVE, + RejectedDirective, UnknownDirective, ) @@ -136,7 +138,7 @@ def downstream(messages: list[dict[str, str]]) -> str: monkeypatch.setattr(module, "_call_litellm", downstream) - result = module.handle_turn("please use docker", engine) + result = module.handle_turn("maybe use docker", engine) assert compile_inputs == [] assert result == "stubbed reply" @@ -150,7 +152,7 @@ def test_extract_drafted_text_observes_draft_result_behavior() -> None: assert module._extract_drafted_text(drafted_result) == "use docker" no_directive_result = DraftResult( - source="test", result=NoDirective("not a directive") + source="test", result=RejectedDirective(reason=REASON_NON_DIRECTIVE) ) assert module._extract_drafted_text(no_directive_result) is None @@ -222,7 +224,7 @@ def downstream(messages: list[dict[str, str]]) -> str: ) clarify_engine = Engine() - clarify = module.handle_turn("set premise to concise replies", clarify_engine) + clarify = module.handle_turn("set premise to", clarify_engine) assert clarify == "downstream reply" assert llm_calls @@ -393,7 +395,10 @@ def fallback(_message: str) -> str | None: monkeypatch.setattr(module, "_call_litellm", lambda _messages: "downstream reply") assert module.handle_turn("use docker instead of", Engine()) == "downstream reply" - assert fallback_calls == 1 + drafted_result = module._DIRECTIVE_DRAFTER.draft_directive("use docker instead of") + assert isinstance(drafted_result.result, RejectedDirective) + assert drafted_result.result.reason == REASON_INCOMPLETE + assert fallback_calls == 0 def test_compound_directives_fall_through_when_not_applied(monkeypatch) -> None: diff --git a/python/tests/test_openwebui_pipe_with_directive_drafter.py b/python/tests/test_openwebui_pipe_with_directive_drafter.py index 46671b3..97b5232 100644 --- a/python/tests/test_openwebui_pipe_with_directive_drafter.py +++ b/python/tests/test_openwebui_pipe_with_directive_drafter.py @@ -10,7 +10,9 @@ from context_compiler.grammar import CanonicalDirective, DirectiveKind from context_compiler_directive_drafter import ( DraftResult, - NoDirective, + REASON_MULTIPLE_DIRECTIVES, + REASON_NON_DIRECTIVE, + RejectedDirective, UnknownDirective, ) @@ -489,7 +491,7 @@ async def update_draft(*args, **kwargs): async def no_draft(*args, **kwargs): return DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", update_draft) @@ -575,7 +577,7 @@ async def update_draft(*args, **kwargs): async def no_draft(*args, **kwargs): return DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", update_draft) @@ -651,7 +653,7 @@ async def forward( async def no_draft(*args, **kwargs): return DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", no_draft) @@ -662,7 +664,10 @@ async def no_draft(*args, **kwargs): result = asyncio.run( pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, + { + "model": "pipe-model", + "messages": [{"role": "user", "content": "hello"}], + }, __user__={"id": "u1"}, __request__=object(), __chat_id__="chat-raw", @@ -708,7 +713,7 @@ async def confirm(event: dict[str, object]) -> bool: pipe.pipe( { "model": "pipe-model", - "messages": [{"role": "user", "content": "please use docker"}], + "messages": [{"role": "user", "content": "maybe use docker"}], }, __user__={"id": "u1"}, __request__=object(), @@ -720,7 +725,7 @@ async def confirm(event: dict[str, object]) -> bool: async def no_draft(*args, **kwargs): return DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", no_draft) @@ -763,7 +768,7 @@ async def forward( async def no_draft(*args, **kwargs): return DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", no_draft) @@ -826,7 +831,7 @@ async def forward( async def compound_draft(*args, **kwargs): return DraftResult( source="test", - result=UnknownDirective(reason="reject.multi_candidate_directive"), + result=RejectedDirective(reason=REASON_MULTIPLE_DIRECTIVES), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", compound_draft) @@ -900,7 +905,7 @@ async def confirm(event: dict[str, object]) -> bool: async def no_draft(*args, **kwargs): return DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", no_draft) @@ -1005,14 +1010,17 @@ def test_debug_mode_missing_base_model_returns_deterministic_message( async def no_draft(*args, **kwargs): return DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", no_draft) result = asyncio.run( pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, + { + "model": "pipe-model", + "messages": [{"role": "user", "content": "maybe use docker"}], + }, __user__={"id": "u1"}, __request__=object(), __chat_id__="chat-debug-missing-base", @@ -1041,7 +1049,10 @@ async def generate( module.generate_chat_completion = generate result = asyncio.run( pipe.pipe( - {"model": "pipe-model", "messages": [{"role": "user", "content": "hello"}]}, + { + "model": "pipe-model", + "messages": [{"role": "user", "content": "maybe use docker"}], + }, __user__={"id": "u1"}, __request__=object(), __chat_id__="chat-preprocessor-not-found", @@ -1069,7 +1080,7 @@ async def generate( ) -> dict[str, object]: calls.append(str(payload.get("model", ""))) if len(calls) == 1: - return {"choices": [{"message": {"content": "no_directive"}}]} + return {"choices": [{"message": {"content": ""}}]} return {"choices": [{"message": {"content": "downstream"}}]} module.generate_chat_completion = generate @@ -1077,7 +1088,7 @@ async def generate( pipe.pipe( { "model": "pipe-model", - "messages": [{"role": "user", "content": "please use docker"}], + "messages": [{"role": "user", "content": "maybe use docker"}], }, __user__={"id": "u1"}, __request__=object(), @@ -1102,11 +1113,11 @@ def test_extract_drafted_text_only_applies_canonical_directive(monkeypatch) -> N ) no_directive = DraftResult( source="test", - result=NoDirective(reason="reject.confident_non_directive"), + result=RejectedDirective(reason=REASON_NON_DIRECTIVE), ) unknown = DraftResult( source="test", - result=UnknownDirective(reason="reject.multi_candidate_directive"), + result=UnknownDirective(reason="semantic_uncertainty"), ) assert pipe._extract_drafted_text(canonical) == "use docker" @@ -1132,7 +1143,7 @@ async def forward( async def unknown_draft(*args, **kwargs): return DraftResult( source="test", - result=UnknownDirective(reason="reject.multi_candidate_directive"), + result=UnknownDirective(reason="semantic_uncertainty"), ) monkeypatch.setattr(module.Pipe, "_draft_user_input", unknown_draft) diff --git a/uv.lock b/uv.lock index bfe6ab5..7f496b6 100644 --- a/uv.lock +++ b/uv.lock @@ -667,14 +667,14 @@ wheels = [ [[package]] name = "context-compiler-directive-drafter" -version = "0.2.0.dev2" +version = "0.2.0.dev4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "context-compiler" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/29/7bebef5ebe92b89e38c9e4aba22b30526400be68d68c176e29514faba7e5/context_compiler_directive_drafter-0.2.0.dev2.tar.gz", hash = "sha256:89acc90936bb1ecc62705b3e51733e1164fae9599cd11a9fca72b52f922fe05c", size = 88929, upload-time = "2026-08-17T06:59:35.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/33/797dd6d527d6f0513f6cff613ec8bae3b9332e62f6b772863a5aeb16ebd4/context_compiler_directive_drafter-0.2.0.dev4.tar.gz", hash = "sha256:80d89fc004966282e592fa757e17e5bdd1171440f6f5d8606766dead8eaa74da", size = 159341, upload-time = "2026-09-01T15:41:57.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/77/04bc383764d79b0bc459c4478d6bb31f18f40a79b0a850964c8d4b5cdccd/context_compiler_directive_drafter-0.2.0.dev2-py3-none-any.whl", hash = "sha256:cdb7940627a43dd82f0c975500d0009b9d2972e3bbd820042c13b61e4d566802", size = 20066, upload-time = "2026-08-17T06:59:33.825Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8e/5800ed38b3924dfa08e81cec5470c7e88bb376fa04c50bfaf0d21df0f340/context_compiler_directive_drafter-0.2.0.dev4-py3-none-any.whl", hash = "sha256:9ea45cf88b08763039a229a938285c6e6d5f38bbe2df573d1181ddd3570208f2", size = 29735, upload-time = "2026-09-01T15:41:55.794Z" }, ] [[package]] @@ -727,8 +727,8 @@ requires-dist = [ { name = "chromadb", marker = "extra == 'all'" }, { name = "chromadb", marker = "extra == 'retrieval'" }, { name = "context-compiler", specifier = "==0.9.0.dev13" }, - { name = "context-compiler-directive-drafter", marker = "extra == 'all'", specifier = "==0.2.0.dev2" }, - { name = "context-compiler-directive-drafter", marker = "extra == 'drafter'", specifier = "==0.2.0.dev2" }, + { name = "context-compiler-directive-drafter", marker = "extra == 'all'", specifier = "==0.2.0.dev4" }, + { name = "context-compiler-directive-drafter", marker = "extra == 'drafter'", specifier = "==0.2.0.dev4" }, { name = "fastapi", marker = "extra == 'all'" }, { name = "fastapi", marker = "extra == 'fastapi'" }, { name = "litellm", marker = "extra == 'all'" }, @@ -739,7 +739,7 @@ provides-extras = ["all", "drafter", "fastapi", "litellm", "retrieval"] [package.metadata.requires-dev] dev = [ { name = "chromadb" }, - { name = "context-compiler-directive-drafter", specifier = "==0.2.0.dev2" }, + { name = "context-compiler-directive-drafter", specifier = "==0.2.0.dev4" }, { name = "fastapi" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "httpx2", specifier = ">=2.5.0" },