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
12 changes: 6 additions & 6 deletions python/reference_integrations/litellm_proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ Available hook files:
turn with a fresh engine.
- In explicit persistent mode, the hook resolves a session key, loads a saved
checkpoint, restores the engine, processes the latest user turn once, and
saves the resulting checkpoint after every decision, including `clarify`.
saves the resulting checkpoint only after non-error decisions.
- In stateless mode, no continuity is preserved across requests.
- If result is `clarify`, the proxy does not call the downstream model and
LiteLLM surfaces the clarification as an HTTP 400 response.
- If directive application fails, the proxy does not call the downstream model
and LiteLLM surfaces the rejection as an HTTP 400 response.
- If result is `passthrough`, the proxy forwards the request normally.
- If result is `update`, the proxy injects compiler state as a system message
and then calls the model.
Expand All @@ -49,7 +49,7 @@ The reference hooks support two explicit modes:
- `persistent`
- explicit mode
- requires a stable session key
- preserves saved state and pending clarification across requests
- preserves saved authoritative state across requests
- `stateless`
- default mode
- processes only the latest user turn
Expand Down Expand Up @@ -217,8 +217,8 @@ Use `llama` only for LLM-only fallback drafting with Llama-family models.
- In the directive-drafter hook, drafter state context now comes from restored
checkpoint state rather than transcript-prefix reconstruction.
- Compound directive-shaped input such as `use docker and prohibit peanuts`
should produce a local clarify response telling the user to submit each
directive separately, without mutating saved state or forwarding upstream.
should produce a local rejection telling the user to submit each directive
separately, without mutating saved state or forwarding upstream.

## Troubleshooting

Expand Down
59 changes: 59 additions & 0 deletions python/reference_integrations/litellm_proxy/_litellm_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Shared LiteLLM hook plumbing for request parsing and state rendering."""

from __future__ import annotations

from typing import TypedDict

from context_compiler import POLICY_PROHIBIT, PolicyValue


class EngineSnapshot(TypedDict):
premise: str | None
policies: dict[str, PolicyValue]


def snapshot_engine_state(engine: object) -> EngineSnapshot:
premise = getattr(engine, "premise", None)
policies = getattr(engine, "policies", {})
normalized_policies = (
dict(policies)
if isinstance(policies, dict)
else dict(policies)
if hasattr(policies, "items")
else {}
)
return {
"premise": premise if isinstance(premise, str) else None,
"policies": normalized_policies,
}


def render_compiled_state_contract(compiled_state: EngineSnapshot) -> str:
prohibited = sorted(
key
for key, value in compiled_state["policies"].items()
if value == POLICY_PROHIBIT
)
premise = compiled_state["premise"]

lines: list[str] = ["The following constraints are authoritative."]
if prohibited:
items = ", ".join(prohibited)
lines.append(f"Never recommend or use prohibited items: {items}.")
if premise:
lines.append(
"When the answer depends on user preference/style, "
f"treat the current premise as: {premise}."
)
lines.append(
"If the user message conflicts with these constraints, follow them exactly."
)

return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines)


def extract_request_messages(data: dict[str, object]) -> list[dict[str, object]]:
raw_messages = data.get("messages")
if not isinstance(raw_messages, list):
return []
return [msg for msg in raw_messages if isinstance(msg, dict)]
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
- Resolve explicit persistent or stateless mode for the current request.
- In persistent mode, restore compiler checkpoint by session key.
- Process only the latest user turn exactly once.
- Save checkpoint after each decision, including clarify.
- If clarification is required, block upstream model call.
- Save checkpoints only after successful authoritative state transitions.
- If directive application fails, reject the current request without persisting
failed-turn engine state.
- Otherwise inject compiled state guidance into a system message.
"""

import logging
from typing import Any, TypedDict
from typing import Any

try:
from litellm.integrations.custom_logger import CustomLogger
Expand All @@ -24,8 +25,6 @@ class CustomLogger: # type: ignore[no-redef]

from context_compiler import (
DecisionKind,
POLICY_PROHIBIT,
PolicyValue,
create_engine,
)
from context_compiler_example_integrations.reference_integrations.litellm_proxy._checkpoint_support import (
Expand All @@ -37,6 +36,11 @@ class CustomLogger: # type: ignore[no-redef]
extract_latest_user_text,
resolve_session_context,
)
from context_compiler_example_integrations.reference_integrations.litellm_proxy._litellm_support import (
extract_request_messages,
render_compiled_state_contract,
snapshot_engine_state,
)

logger = logging.getLogger(__name__)

Expand All @@ -49,58 +53,6 @@ class CustomLogger: # type: ignore[no-redef]
CHECKPOINT_STORE: CheckpointStore = InMemoryCheckpointStore()


class _EngineSnapshot(TypedDict):
premise: str | None
policies: dict[str, PolicyValue]


def _snapshot_engine_state(engine: object) -> _EngineSnapshot:
premise = getattr(engine, "premise", None)
policies = getattr(engine, "policies", {})
normalized_policies = (
dict(policies)
if isinstance(policies, dict)
else dict(policies)
if hasattr(policies, "items")
else {}
)
return {
"premise": premise if isinstance(premise, str) else None,
"policies": normalized_policies,
}


def _render_compiled_state_contract(compiled_state: _EngineSnapshot) -> str:
prohibited = sorted(
key
for key, value in compiled_state["policies"].items()
if value == POLICY_PROHIBIT
)
premise = compiled_state["premise"]

lines: list[str] = ["The following constraints are authoritative."]
if prohibited:
items = ", ".join(prohibited)
lines.append(f"Never recommend or use prohibited items: {items}.")
if premise:
lines.append(
"When the answer depends on user preference/style, "
f"treat the current premise as: {premise}."
)
lines.append(
"If the user message conflicts with these constraints, follow them exactly."
)

return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines)


def _extract_request_messages(data: dict[str, object]) -> list[dict[str, object]]:
raw_messages = data.get("messages")
if not isinstance(raw_messages, list):
return []
return [msg for msg in raw_messages if isinstance(msg, dict)]


class ContextCompilerPreCallHook(CustomLogger):
async def async_pre_call_hook(
self,
Expand All @@ -114,7 +66,7 @@ async def async_pre_call_hook(
if call_type not in _SUPPORTED_CALL_TYPES:
return data

request_messages = _extract_request_messages(data)
request_messages = extract_request_messages(data)
logger.debug("litellm_proxy: message_count=%d", len(request_messages))
session = resolve_session_context(data)
logger.debug(
Expand Down Expand Up @@ -151,24 +103,24 @@ async def async_pre_call_hook(
else:
decision = {"kind": DecisionKind.NO_DIRECTIVE, "message": None}

logger.debug("litellm_proxy: decision_kind=%s", decision["kind"])

if decision["kind"] == DecisionKind.ERROR:
logger.debug("litellm_proxy: rejecting_failed_application=true")
return decision.get("message") or "Request rejected."

if session.mode == MODE_PERSISTENT and session.session_key is not None:
CHECKPOINT_STORE.save(
session.session_key,
checkpoint_to_jsonable(engine.export_json()),
)

logger.debug("litellm_proxy: decision_kind=%s", decision["kind"])

if decision["kind"] == DecisionKind.ERROR:
logger.debug("litellm_proxy: blocking_on_clarify=true")
return decision.get("message") or "Request rejected."

compiled_state = _snapshot_engine_state(engine)
compiled_state = snapshot_engine_state(engine)
# For long-running conversations, you can optionally compact transcripts by removing user inputs that were compiled into state. See Demo 6. # noqa: E501
system_message: dict[str, object] = {
"role": "system",
"content": "You are a helpful assistant.\n"
+ _render_compiled_state_contract(compiled_state),
+ render_compiled_state_contract(compiled_state),
}
# Prepend one compiler contract system message, then forward the original
# request messages unchanged. Existing system messages are preserved.
Expand Down
Loading