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
29 changes: 16 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,24 +181,27 @@ uv run pytest

## FAQ

**Is this just prompt reinjection?**
Reinjection helps with persistence, and it remains useful. Context Compiler
handles a different problem: rules for when state is allowed to change.

Examples:
- replacement semantics (`use X instead of Y`) when `Y` may not exist
- contradiction detection before applying a mutation
- lifecycle enforcement (for example, you cannot change an unset premise)
- pending clarification flows that must be resolved before other mutations

In short: reinjection carries state forward; Context Compiler decides when your
app should change state.

**Isn’t this just prompt engineering?**
It complements prompt engineering, but solves a different problem. Prompting
shapes model behavior. Context Compiler enforces state rules and updates state
only through explicit directives.

**Why not just use a plain dict?**
A plain dict is enough to drive prompt construction, schema selection, and
other host behavior.

Context Compiler solves a different problem: who updates that state, under what
rules, and what happens when instructions conflict.

```text
User: use python_script
User: prohibit python_script
```

With a plain dict, the application must invent conflict-resolution rules.
Context Compiler applies deterministic state-transition rules and can return
clarification instead of silently overwriting state.

---

## 10-Second Example
Expand Down
7 changes: 7 additions & 0 deletions examples/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ Gateway-level integration using a LiteLLM pre-call hook.

See: [LiteLLM Proxy README](litellm_proxy/README.md)

## Ollama structured output (host-side schema selection)

Minimal example showing host-side JSON Schema selection for Ollama `format` based on
Context Compiler policy state.

See: [ollama_structured_output/README.md](ollama_structured_output/README.md)

## Open WebUI Pipe Function

Tested target: Open WebUI `v0.8.12`.
Expand Down
58 changes: 58 additions & 0 deletions examples/integrations/ollama_structured_output/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Ollama structured output (host-side selection)

This example shows a visible host behavior change that is different from prompt reinjection.

Flow:

`Context Compiler state -> host schema decision -> Ollama format request -> model call`

The host reads compiled policy state, picks a JSON Schema (or none), and sends that choice through Ollama's `format` field.

## What this example guarantees

- Context Compiler provides deterministic state transitions.
- The host integration decides whether to request a schema.
- Ollama structured output is a runtime request made by the host.
- If policy state is unknown or insufficient, the host requests no schema.

## What `prohibit shell_command` means here

- The host will not request the `shell_command` schema.
- The host may still request a different schema when policy supports it (for example, `python_script`).
- This does not block normal language discussion about shell commands.

## Observable behavior

Given policy state:

```text
use python_script
prohibit shell_command
```

this host selects `python_script` schema and does not request `shell_command` schema.

## Test boundary

Tests verify schema selection behavior only:

- compiler state -> selected schema (or no schema)
- contradiction handling stays in compiler `clarify`

Tests do not assert exact model wording.

## Run without Ollama

```shell
uv run python examples/integrations/ollama_structured_output/example.py
```

## Optional Ollama smoke run

```shell
export RUN_OLLAMA_SMOKE=1
export OLLAMA_MODEL=llama3.1
uv run python examples/integrations/ollama_structured_output/example.py
```

When smoke mode is enabled, the host sends the selected JSON Schema through Ollama `format`.
169 changes: 169 additions & 0 deletions examples/integrations/ollama_structured_output/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""Minimal host-side Ollama structured-output schema selection.

Flow:
Context Compiler state -> host selection logic -> Ollama `format` JSON Schema.

This example keeps model execution optional so tests can validate behavior without Ollama.
"""

import json
import os
import urllib.error
import urllib.request
from collections.abc import Mapping
from typing import Any, TypedDict, cast

from context_compiler import (
POLICY_PROHIBIT,
POLICY_USE,
State,
create_engine,
get_clarify_prompt,
get_decision_state,
get_policy_items,
is_clarify,
)
from context_compiler.engine import Engine

PYTHON_SCRIPT_SCHEMA: dict[str, Any] = {
"type": "object",
"properties": {
"python_script": {
"type": "string",
"description": "A complete Python script.",
}
},
"required": ["python_script"],
"additionalProperties": False,
}

SHELL_COMMAND_SCHEMA: dict[str, Any] = {
"type": "object",
"properties": {
"shell_command": {
"type": "string",
"description": "A single shell command.",
}
},
"required": ["shell_command"],
"additionalProperties": False,
}

# Small, explicit mapping from policy item -> Ollama `format` schema.
_SCHEMA_BY_ITEM: dict[str, dict[str, Any]] = {
"python_script": PYTHON_SCRIPT_SCHEMA,
"shell_command": SHELL_COMMAND_SCHEMA,
}


class TurnPlan(TypedDict):
decision_kind: str
clarify_prompt: str | None
selected_schema_item: str | None
format_schema: dict[str, Any] | None


def select_ollama_format_schema(state: State) -> tuple[str | None, dict[str, Any] | None]:
"""Return (policy_item, schema) or (None, None) when no safe match exists.

Unknown/insufficient policy state intentionally selects no schema.
"""

use_items = set(get_policy_items(state, POLICY_USE))
prohibit_items = set(get_policy_items(state, POLICY_PROHIBIT))

for item, schema in _SCHEMA_BY_ITEM.items():
if item in use_items and item not in prohibit_items:
return item, schema

return None, None


def plan_turn(user_input: str, engine: Engine) -> TurnPlan:
"""Run compiler step and decide whether to request Ollama structured output."""

decision = engine.step(user_input)
if is_clarify(decision):
return {
"decision_kind": "clarify",
"clarify_prompt": get_clarify_prompt(decision),
"selected_schema_item": None,
"format_schema": None,
}

decision_state = get_decision_state(decision)
compiled_state = decision_state if decision_state is not None else engine.state
selected_item, format_schema = select_ollama_format_schema(compiled_state)

return {
"decision_kind": str(decision["kind"]),
"clarify_prompt": None,
"selected_schema_item": selected_item,
"format_schema": format_schema,
}


def optional_ollama_call(
*,
user_input: str,
model: str,
format_schema: Mapping[str, Any] | None,
host: str | None = None,
) -> dict[str, Any]:
"""Optional smoke call to Ollama's /api/chat.

If `format_schema` is provided, it is passed through `format` exactly.
"""

base_url = host or os.getenv("OLLAMA_BASE_URL") or "http://localhost:11434"
payload: dict[str, Any] = {
"model": model,
"messages": [{"role": "user", "content": user_input}],
"stream": False,
}
if format_schema is not None:
payload["format"] = dict(format_schema)

request = urllib.request.Request(
url=f"{base_url.rstrip('/')}/api/chat",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)

try:
with urllib.request.urlopen(request, timeout=30) as response:
raw = response.read().decode("utf-8")
except urllib.error.URLError as exc:
raise RuntimeError(f"Ollama call failed: {exc}") from exc

decoded = cast(object, json.loads(raw))
if not isinstance(decoded, dict):
raise RuntimeError("Ollama response must be a JSON object")
return cast(dict[str, Any], decoded)


def main() -> None:
engine = create_engine()

# Demonstration setup.
engine.step("use python_script")
engine.step("prohibit shell_command")

plan = plan_turn("Write a helper script to parse CSV files.", engine)
print("decision_kind:", plan["decision_kind"])
print("selected_schema_item:", plan["selected_schema_item"])
print("format_schema_selected:", plan["format_schema"] is not None)

# Optional model execution path; disabled by default.
if os.getenv("RUN_OLLAMA_SMOKE") == "1":
response = optional_ollama_call(
user_input="Write a helper script to parse CSV files.",
model=os.getenv("OLLAMA_MODEL", "llama3.1"),
format_schema=plan["format_schema"],
)
print("ollama_response_keys:", sorted(response.keys()))


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "context-compiler"
version = "0.7.5"
version = "0.7.6"
description = "Deterministic conversational state engine for LLM applications."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
5 changes: 5 additions & 0 deletions tests/test_example_integrations_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ def _guarded_import(
[
(INTEGRATIONS_DIR / "litellm" / "basic.py", ("litellm",), False),
(INTEGRATIONS_DIR / "litellm" / "with_preprocessor.py", ("litellm",), False),
(
INTEGRATIONS_DIR / "ollama_structured_output" / "example.py",
(),
False,
),
(
INTEGRATIONS_DIR / "litellm_proxy" / "context_compiler_precall_hook.py",
("litellm",),
Expand Down
60 changes: 60 additions & 0 deletions tests/test_ollama_structured_output_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import importlib.util
from pathlib import Path

from context_compiler import create_engine

REPO_ROOT = Path(__file__).resolve().parents[1]
EXAMPLE_PATH = REPO_ROOT / "examples" / "integrations" / "ollama_structured_output" / "example.py"


def _load_module():
spec = importlib.util.spec_from_file_location("ollama_structured_output_example", EXAMPLE_PATH)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def test_use_python_script_selects_python_schema() -> None:
module = _load_module()
engine = create_engine()

first = engine.step("use python_script")
assert first["kind"] == "update"

plan = module.plan_turn("write script", engine)
assert plan["selected_schema_item"] == "python_script"
assert plan["format_schema"] == module.PYTHON_SCRIPT_SCHEMA


def test_prohibit_shell_command_does_not_select_shell_schema() -> None:
module = _load_module()
engine = create_engine()

assert engine.step("use python_script")["kind"] == "update"
assert engine.step("prohibit shell_command")["kind"] == "update"

plan = module.plan_turn("do task", engine)
assert plan["selected_schema_item"] == "python_script"
assert plan["format_schema"] != module.SHELL_COMMAND_SCHEMA


def test_unknown_or_no_matching_state_selects_no_schema() -> None:
module = _load_module()
engine = create_engine()

plan = module.plan_turn("hello", engine)
assert plan["decision_kind"] == "passthrough"
assert plan["selected_schema_item"] is None
assert plan["format_schema"] is None


def test_contradictory_input_is_compiler_clarify_not_host_resolution() -> None:
module = _load_module()
engine = create_engine()

assert engine.step("use python_script")["kind"] == "update"
conflict_plan = module.plan_turn("prohibit python_script", engine)
assert conflict_plan["decision_kind"] == "clarify"
assert conflict_plan["selected_schema_item"] is None
assert conflict_plan["format_schema"] is None
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading