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.
- context_compiler_example_integrations/README.md +71 -0
- context_compiler_example_integrations/__init__.py +1 -0
- context_compiler_example_integrations/examples/__init__.py +12 -0
- context_compiler_example_integrations/examples/_shared/__init__.py +1 -0
- context_compiler_example_integrations/examples/_shared/litellm_request.py +61 -0
- context_compiler_example_integrations/examples/_shared/provider_mode.py +91 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/README.md +77 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/example.py +169 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/README.md +85 -0
- context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/app.py +205 -0
- context_compiler_example_integrations/examples/execution_authorization/README.md +31 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/README.md +77 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/__init__.py +1 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/example.py +168 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/README.md +97 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/__init__.py +1 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/app.py +312 -0
- context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/model_approval.py +98 -0
- context_compiler_example_integrations/examples/gateway_middleware/README.md +49 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/README.md +40 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/__init__.py +1 -0
- context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/example.py +220 -0
- context_compiler_example_integrations/examples/prompt_construction/README.md +27 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/README.md +197 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/basic.py +392 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/confirmation_helper.py +91 -0
- context_compiler_example_integrations/examples/prompt_construction/litellm/with_directive_drafter.py +524 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/README.md +92 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/__init__.py +1 -0
- context_compiler_example_integrations/examples/prompt_construction/writing_assistant/example.py +205 -0
- context_compiler_example_integrations/examples/retrieval_filtering/README.md +65 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/README.md +120 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/__init__.py +1 -0
- context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/example.py +320 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/README.md +123 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/__init__.py +1 -0
- context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/example.py +300 -0
- context_compiler_example_integrations/examples/schema_selection/README.md +41 -0
- context_compiler_example_integrations/examples/schema_selection/litellm_response_format/response_format.py +218 -0
- context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/README.md +60 -0
- context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/example.py +171 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/README.md +95 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/__init__.py +1 -0
- context_compiler_example_integrations/examples/schema_selection/refund_intake/example.py +180 -0
- context_compiler_example_integrations/examples/tool_gating/README.md +55 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/README.md +34 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/__init__.py +1 -0
- context_compiler_example_integrations/examples/tool_gating/calendar_admin/example.py +195 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/README.md +93 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/__init__.py +1 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/example.py +250 -0
- context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/live_model.py +369 -0
- context_compiler_example_integrations/reference_integrations/__init__.py +12 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/README.md +251 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/_checkpoint_support.py +146 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/config.example.yaml +23 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook.py +163 -0
- context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +289 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/README.md +233 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe.py +750 -0
- context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +1045 -0
- context_compiler_example_integrations-0.1.0.dist-info/METADATA +110 -0
- context_compiler_example_integrations-0.1.0.dist-info/RECORD +65 -0
- context_compiler_example_integrations-0.1.0.dist-info/WHEEL +4 -0
- context_compiler_example_integrations-0.1.0.dist-info/licenses/LICENSE +173 -0
context_compiler_example_integrations/reference_integrations/litellm_proxy/_checkpoint_support.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Narrow checkpoint/session support for LiteLLM Proxy reference hooks.
|
|
2
|
+
|
|
3
|
+
This module is intentionally repo-local and small in scope:
|
|
4
|
+
- explicit persistent vs stateless mode selection
|
|
5
|
+
- explicit session-key resolution
|
|
6
|
+
- storage-neutral checkpoint store contract
|
|
7
|
+
- latest-user-turn extraction helpers
|
|
8
|
+
|
|
9
|
+
The in-memory store is suitable only for tests and single-process examples.
|
|
10
|
+
It does not provide durability, multi-worker coordination, or atomic updates.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import Mapping
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Protocol
|
|
19
|
+
|
|
20
|
+
MODE_PERSISTENT = "persistent"
|
|
21
|
+
MODE_STATELESS = "stateless"
|
|
22
|
+
SESSION_MODE_ENV_VAR = "CONTEXT_COMPILER_SESSION_MODE"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CheckpointStore(Protocol):
|
|
26
|
+
def load(self, session_key: str) -> Mapping[str, object] | None: ...
|
|
27
|
+
|
|
28
|
+
def save(self, session_key: str, checkpoint: Mapping[str, object]) -> None: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InMemoryCheckpointStore:
|
|
32
|
+
"""Single-process example checkpoint store for tests and local reference use."""
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
self._checkpoints: dict[str, dict[str, object]] = {}
|
|
36
|
+
|
|
37
|
+
def load(self, session_key: str) -> Mapping[str, object] | None:
|
|
38
|
+
checkpoint = self._checkpoints.get(session_key)
|
|
39
|
+
if checkpoint is None:
|
|
40
|
+
return None
|
|
41
|
+
return dict(checkpoint)
|
|
42
|
+
|
|
43
|
+
def save(self, session_key: str, checkpoint: Mapping[str, object]) -> None:
|
|
44
|
+
self._checkpoints[session_key] = dict(checkpoint)
|
|
45
|
+
|
|
46
|
+
def clear(self) -> None:
|
|
47
|
+
self._checkpoints.clear()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class SessionContext:
|
|
52
|
+
mode: str
|
|
53
|
+
session_key: str | None
|
|
54
|
+
source: str | None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def resolve_session_context(data: Mapping[str, object]) -> SessionContext:
|
|
58
|
+
mode = _resolve_mode(data)
|
|
59
|
+
session_key, source = _resolve_session_key(data)
|
|
60
|
+
return SessionContext(mode=mode, session_key=session_key, source=source)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_latest_user_text(messages: list[dict[str, object]]) -> str | None:
|
|
64
|
+
for message in reversed(messages):
|
|
65
|
+
if message.get("role") != "user":
|
|
66
|
+
continue
|
|
67
|
+
content = extract_text_content(message.get("content"))
|
|
68
|
+
if content is not None:
|
|
69
|
+
return content
|
|
70
|
+
return None
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_text_content(content: object) -> str | None:
|
|
75
|
+
if isinstance(content, str):
|
|
76
|
+
return content
|
|
77
|
+
if isinstance(content, list):
|
|
78
|
+
text_parts: list[str] = []
|
|
79
|
+
for item in content:
|
|
80
|
+
if not isinstance(item, dict):
|
|
81
|
+
continue
|
|
82
|
+
if item.get("type") != "text":
|
|
83
|
+
continue
|
|
84
|
+
text = item.get("text")
|
|
85
|
+
if isinstance(text, str):
|
|
86
|
+
text_parts.append(text)
|
|
87
|
+
if text_parts:
|
|
88
|
+
return " ".join(text_parts)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def checkpoint_to_jsonable(checkpoint_json: str) -> dict[str, object]:
|
|
93
|
+
import json
|
|
94
|
+
|
|
95
|
+
raw = json.loads(checkpoint_json)
|
|
96
|
+
if not isinstance(raw, dict):
|
|
97
|
+
raise ValueError("Checkpoint JSON must decode to an object.")
|
|
98
|
+
return raw
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def checkpoint_from_jsonable(checkpoint: Mapping[str, object]) -> str:
|
|
102
|
+
import json
|
|
103
|
+
|
|
104
|
+
return json.dumps(checkpoint, separators=(",", ":"), sort_keys=True)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _resolve_mode(data: Mapping[str, object]) -> str:
|
|
108
|
+
candidates = [
|
|
109
|
+
data.get("context_compiler_mode"),
|
|
110
|
+
os.getenv(SESSION_MODE_ENV_VAR),
|
|
111
|
+
]
|
|
112
|
+
for candidate in candidates:
|
|
113
|
+
if not isinstance(candidate, str):
|
|
114
|
+
continue
|
|
115
|
+
normalized = candidate.strip().lower()
|
|
116
|
+
if normalized in {MODE_PERSISTENT, MODE_STATELESS}:
|
|
117
|
+
return normalized
|
|
118
|
+
return MODE_STATELESS
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_session_key(data: Mapping[str, object]) -> tuple[str | None, str | None]:
|
|
122
|
+
candidates: list[tuple[str, object]] = [
|
|
123
|
+
("context_compiler_session_key", data.get("context_compiler_session_key")),
|
|
124
|
+
(
|
|
125
|
+
"metadata.context_compiler_session_key",
|
|
126
|
+
_nested_lookup(data.get("metadata"), "context_compiler_session_key"),
|
|
127
|
+
),
|
|
128
|
+
]
|
|
129
|
+
for source, value in candidates:
|
|
130
|
+
normalized = _normalize_key(value)
|
|
131
|
+
if normalized is not None:
|
|
132
|
+
return normalized, source
|
|
133
|
+
return None, None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _nested_lookup(value: object, key: str) -> object:
|
|
137
|
+
if isinstance(value, Mapping):
|
|
138
|
+
return value.get(key)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _normalize_key(value: object) -> str | None:
|
|
143
|
+
if not isinstance(value, str):
|
|
144
|
+
return None
|
|
145
|
+
normalized = value.strip()
|
|
146
|
+
return normalized or None
|
context_compiler_example_integrations/reference_integrations/litellm_proxy/config.example.yaml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
model_list:
|
|
2
|
+
# `model_name` is the client-facing alias sent to the proxy.
|
|
3
|
+
# `litellm_params.model` is the upstream provider/model LiteLLM calls.
|
|
4
|
+
- model_name: gpt-4o-mini
|
|
5
|
+
litellm_params:
|
|
6
|
+
model: openai/gpt-4o-mini
|
|
7
|
+
api_key: os.environ/OPENAI_API_KEY
|
|
8
|
+
|
|
9
|
+
- model_name: llama3.1
|
|
10
|
+
litellm_params:
|
|
11
|
+
model: ollama/llama3.1:8b
|
|
12
|
+
api_base: http://localhost:11434
|
|
13
|
+
|
|
14
|
+
litellm_settings:
|
|
15
|
+
callbacks:
|
|
16
|
+
# Basic checkpoint-backed hook.
|
|
17
|
+
# Default mode is stateless unless the request or environment sets
|
|
18
|
+
# CONTEXT_COMPILER_SESSION_MODE=persistent.
|
|
19
|
+
- context_compiler_precall_hook.proxy_handler_instance
|
|
20
|
+
# Directive-drafter-enabled checkpoint-backed hook (use this instead).
|
|
21
|
+
# Persistent mode requires context_compiler_session_key or
|
|
22
|
+
# metadata.context_compiler_session_key on the request.
|
|
23
|
+
# - context_compiler_precall_hook_with_directive_drafter.proxy_handler_instance
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Minimal LiteLLM Proxy pre-call hook example.
|
|
2
|
+
|
|
3
|
+
Architecture:
|
|
4
|
+
- Resolve explicit persistent or stateless mode for the current request.
|
|
5
|
+
- In persistent mode, restore compiler checkpoint by session key.
|
|
6
|
+
- Process only the latest user turn exactly once.
|
|
7
|
+
- Save checkpoint after each decision, including clarify.
|
|
8
|
+
- If clarification is required, block upstream model call.
|
|
9
|
+
- Otherwise inject compiled state guidance into a system message.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from litellm.integrations.custom_logger import CustomLogger
|
|
17
|
+
except ModuleNotFoundError:
|
|
18
|
+
# Keep this import path optional: CI/tests run without integration extras.
|
|
19
|
+
# A tiny fallback base class keeps module imports deterministic so coverage
|
|
20
|
+
# validates behavior instead of failing or silently skipping on missing litellm.
|
|
21
|
+
class CustomLogger: # type: ignore[no-redef]
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from context_compiler import (
|
|
26
|
+
POLICY_PROHIBIT,
|
|
27
|
+
create_engine,
|
|
28
|
+
get_clarify_prompt,
|
|
29
|
+
State,
|
|
30
|
+
get_policy_items,
|
|
31
|
+
get_premise_value,
|
|
32
|
+
is_clarify,
|
|
33
|
+
)
|
|
34
|
+
from context_compiler.engine import DecisionKind
|
|
35
|
+
from context_compiler_example_integrations.reference_integrations.litellm_proxy._checkpoint_support import (
|
|
36
|
+
MODE_PERSISTENT,
|
|
37
|
+
CheckpointStore,
|
|
38
|
+
InMemoryCheckpointStore,
|
|
39
|
+
checkpoint_from_jsonable,
|
|
40
|
+
checkpoint_to_jsonable,
|
|
41
|
+
extract_latest_user_text,
|
|
42
|
+
resolve_session_context,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
_SUPPORTED_CALL_TYPES = {
|
|
48
|
+
"completion",
|
|
49
|
+
"acompletion",
|
|
50
|
+
"chat_completion",
|
|
51
|
+
"achat_completion",
|
|
52
|
+
}
|
|
53
|
+
CHECKPOINT_STORE: CheckpointStore = InMemoryCheckpointStore()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _render_compiled_state_contract(compiled_state: State) -> str:
|
|
57
|
+
prohibited = get_policy_items(compiled_state, POLICY_PROHIBIT)
|
|
58
|
+
premise = get_premise_value(compiled_state)
|
|
59
|
+
|
|
60
|
+
lines: list[str] = ["The following constraints are authoritative."]
|
|
61
|
+
if prohibited:
|
|
62
|
+
items = ", ".join(prohibited)
|
|
63
|
+
lines.append(f"Never recommend or use prohibited items: {items}.")
|
|
64
|
+
if premise:
|
|
65
|
+
lines.append(
|
|
66
|
+
"When the answer depends on user preference/style, "
|
|
67
|
+
f"treat the current premise as: {premise}."
|
|
68
|
+
)
|
|
69
|
+
lines.append(
|
|
70
|
+
"If the user message conflicts with these constraints, follow them exactly."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _extract_request_messages(data: dict[str, object]) -> list[dict[str, object]]:
|
|
77
|
+
raw_messages = data.get("messages")
|
|
78
|
+
if not isinstance(raw_messages, list):
|
|
79
|
+
return []
|
|
80
|
+
return [msg for msg in raw_messages if isinstance(msg, dict)]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ContextCompilerPreCallHook(CustomLogger):
|
|
84
|
+
async def async_pre_call_hook(
|
|
85
|
+
self,
|
|
86
|
+
user_api_key_dict: Any,
|
|
87
|
+
cache: Any,
|
|
88
|
+
data: dict[str, object],
|
|
89
|
+
call_type: str,
|
|
90
|
+
) -> dict[str, object] | str:
|
|
91
|
+
del user_api_key_dict, cache
|
|
92
|
+
logger.debug("litellm_proxy: call_type=%s", call_type)
|
|
93
|
+
if call_type not in _SUPPORTED_CALL_TYPES:
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
request_messages = _extract_request_messages(data)
|
|
97
|
+
logger.debug("litellm_proxy: message_count=%d", len(request_messages))
|
|
98
|
+
session = resolve_session_context(data)
|
|
99
|
+
logger.debug(
|
|
100
|
+
"litellm_proxy: mode=%s session_key_source=%s",
|
|
101
|
+
session.mode,
|
|
102
|
+
session.source,
|
|
103
|
+
)
|
|
104
|
+
if session.mode == MODE_PERSISTENT and session.session_key is None:
|
|
105
|
+
return (
|
|
106
|
+
"Context Compiler persistent mode requires a stable session key. "
|
|
107
|
+
"Set context_compiler_session_key or "
|
|
108
|
+
"metadata.context_compiler_session_key."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
latest_user_text = extract_latest_user_text(request_messages)
|
|
112
|
+
logger.debug(
|
|
113
|
+
"litellm_proxy: latest_user_text_present=%s", latest_user_text is not None
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
engine = create_engine()
|
|
117
|
+
if session.mode == MODE_PERSISTENT and session.session_key is not None:
|
|
118
|
+
checkpoint = CHECKPOINT_STORE.load(session.session_key)
|
|
119
|
+
if checkpoint is not None:
|
|
120
|
+
try:
|
|
121
|
+
engine.import_checkpoint_json(checkpoint_from_jsonable(checkpoint))
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
return (
|
|
124
|
+
"Context Compiler checkpoint load failed for session "
|
|
125
|
+
f"{session.session_key!r}: {exc}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if latest_user_text is not None:
|
|
129
|
+
decision = engine.step(latest_user_text)
|
|
130
|
+
else:
|
|
131
|
+
decision = {
|
|
132
|
+
"kind": DecisionKind.PASSTHROUGH,
|
|
133
|
+
"state": engine.state,
|
|
134
|
+
"prompt_to_user": None,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if session.mode == MODE_PERSISTENT and session.session_key is not None:
|
|
138
|
+
CHECKPOINT_STORE.save(
|
|
139
|
+
session.session_key,
|
|
140
|
+
checkpoint_to_jsonable(engine.export_checkpoint_json()),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger.debug("litellm_proxy: decision_kind=%s", decision["kind"])
|
|
144
|
+
|
|
145
|
+
if is_clarify(decision):
|
|
146
|
+
logger.debug("litellm_proxy: blocking_on_clarify=true")
|
|
147
|
+
return get_clarify_prompt(decision) or "Confirmation required."
|
|
148
|
+
|
|
149
|
+
compiled_state = engine.state
|
|
150
|
+
# For long-running conversations, you can optionally compact transcripts by removing user inputs that were compiled into state. See Demo 6. # noqa: E501
|
|
151
|
+
system_message: dict[str, object] = {
|
|
152
|
+
"role": "system",
|
|
153
|
+
"content": "You are a helpful assistant.\n"
|
|
154
|
+
+ _render_compiled_state_contract(compiled_state),
|
|
155
|
+
}
|
|
156
|
+
# Prepend one compiler contract system message, then forward the original
|
|
157
|
+
# request messages unchanged. Existing system messages are preserved.
|
|
158
|
+
logger.debug("litellm_proxy: inject_system_message=true")
|
|
159
|
+
data["messages"] = [system_message, *request_messages]
|
|
160
|
+
return data
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
proxy_handler_instance = ContextCompilerPreCallHook()
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""LiteLLM Proxy pre-call hook with optional directive drafter on latest user message.
|
|
2
|
+
|
|
3
|
+
Architecture:
|
|
4
|
+
- Resolve explicit persistent or stateless mode for the current request.
|
|
5
|
+
- In persistent mode, restore compiler checkpoint by session key.
|
|
6
|
+
- Draft only the latest user message after restore.
|
|
7
|
+
- Call ``engine.step(...)`` exactly once for the current turn.
|
|
8
|
+
- Save checkpoint after each decision, including clarify.
|
|
9
|
+
- If clarification is required, block upstream model call.
|
|
10
|
+
- Otherwise inject compiled state guidance into a system message.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
16
|
+
from importlib import import_module
|
|
17
|
+
from importlib.resources import as_file, files
|
|
18
|
+
from importlib.resources.abc import Traversable
|
|
19
|
+
from typing import Any, cast
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from litellm.integrations.custom_logger import CustomLogger
|
|
23
|
+
except ModuleNotFoundError:
|
|
24
|
+
# Keep this import path optional: CI/tests run without integration extras.
|
|
25
|
+
# A tiny fallback base class keeps module imports deterministic so coverage
|
|
26
|
+
# validates behavior instead of failing or silently skipping on missing litellm.
|
|
27
|
+
class CustomLogger: # type: ignore[no-redef]
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
from context_compiler import (
|
|
32
|
+
POLICY_PROHIBIT,
|
|
33
|
+
State,
|
|
34
|
+
create_engine,
|
|
35
|
+
get_clarify_prompt,
|
|
36
|
+
get_policy_items,
|
|
37
|
+
get_premise_value,
|
|
38
|
+
is_clarify,
|
|
39
|
+
)
|
|
40
|
+
from context_compiler.engine import DecisionKind
|
|
41
|
+
from context_compiler_directive_drafter import (
|
|
42
|
+
PREPROCESS_OUTCOME_DIRECTIVE,
|
|
43
|
+
parse_preprocessor_output,
|
|
44
|
+
preprocess_heuristic,
|
|
45
|
+
render_prompt,
|
|
46
|
+
)
|
|
47
|
+
from context_compiler_example_integrations.reference_integrations.litellm_proxy._checkpoint_support import (
|
|
48
|
+
MODE_PERSISTENT,
|
|
49
|
+
CheckpointStore,
|
|
50
|
+
InMemoryCheckpointStore,
|
|
51
|
+
checkpoint_from_jsonable,
|
|
52
|
+
checkpoint_to_jsonable,
|
|
53
|
+
extract_latest_user_text,
|
|
54
|
+
resolve_session_context,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
_SUPPORTED_CALL_TYPES = {
|
|
60
|
+
"completion",
|
|
61
|
+
"acompletion",
|
|
62
|
+
"chat_completion",
|
|
63
|
+
"achat_completion",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_PROMPTS_DIR = files("context_compiler_directive_drafter").joinpath("prompts")
|
|
67
|
+
CHECKPOINT_STORE: CheckpointStore = InMemoryCheckpointStore()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _render_compiled_state_contract(compiled_state: State) -> str:
|
|
71
|
+
prohibited = get_policy_items(compiled_state, POLICY_PROHIBIT)
|
|
72
|
+
premise = get_premise_value(compiled_state)
|
|
73
|
+
|
|
74
|
+
lines: list[str] = ["The following constraints are authoritative."]
|
|
75
|
+
if prohibited:
|
|
76
|
+
items = ", ".join(prohibited)
|
|
77
|
+
lines.append(f"Never recommend or use prohibited items: {items}.")
|
|
78
|
+
if premise:
|
|
79
|
+
lines.append(
|
|
80
|
+
"When the answer depends on user preference/style, "
|
|
81
|
+
f"treat the current premise as: {premise}."
|
|
82
|
+
)
|
|
83
|
+
lines.append(
|
|
84
|
+
"If the user message conflicts with these constraints, follow them exactly."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return "Host policy contract:\n" + "\n".join(f"- {line}" for line in lines)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _extract_request_messages(data: dict[str, object]) -> list[dict[str, object]]:
|
|
91
|
+
raw_messages = data.get("messages")
|
|
92
|
+
if not isinstance(raw_messages, list):
|
|
93
|
+
return []
|
|
94
|
+
return [msg for msg in raw_messages if isinstance(msg, dict)]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _extract_response_content(response: object) -> str | None:
|
|
98
|
+
if isinstance(response, Mapping):
|
|
99
|
+
choices = response.get("choices")
|
|
100
|
+
if isinstance(choices, Sequence) and choices:
|
|
101
|
+
first = choices[0]
|
|
102
|
+
if isinstance(first, Mapping):
|
|
103
|
+
message = first.get("message")
|
|
104
|
+
if isinstance(message, Mapping):
|
|
105
|
+
content = message.get("content")
|
|
106
|
+
if isinstance(content, str):
|
|
107
|
+
return content
|
|
108
|
+
|
|
109
|
+
choices_attr = getattr(response, "choices", None)
|
|
110
|
+
if isinstance(choices_attr, Sequence) and choices_attr:
|
|
111
|
+
first = choices_attr[0]
|
|
112
|
+
message_attr = getattr(first, "message", None)
|
|
113
|
+
content_attr = getattr(message_attr, "content", None)
|
|
114
|
+
if isinstance(content_attr, str):
|
|
115
|
+
return content_attr
|
|
116
|
+
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _prompt_file_path() -> Traversable:
|
|
121
|
+
profile = os.getenv("PREPROCESSOR_PROMPT_PROFILE", "default").strip().lower()
|
|
122
|
+
if profile == "llama":
|
|
123
|
+
return _PROMPTS_DIR.joinpath("llama.txt")
|
|
124
|
+
return _PROMPTS_DIR.joinpath("default.txt")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _get_litellm_completion() -> Callable[..., object]:
|
|
128
|
+
litellm_module = import_module("litellm")
|
|
129
|
+
return cast(Callable[..., object], litellm_module.completion)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _llm_fallback_preprocess(message: str, state: State) -> str | None:
|
|
133
|
+
with as_file(_prompt_file_path()) as prompt_path:
|
|
134
|
+
prompt = render_prompt(prompt_path, state)
|
|
135
|
+
if prompt is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
preprocessor_model = os.getenv("PREPROCESSOR_MODEL", "").strip()
|
|
139
|
+
if not preprocessor_model:
|
|
140
|
+
preprocessor_model = os.getenv("MODEL", "").strip()
|
|
141
|
+
if not preprocessor_model:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
145
|
+
if not api_key:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
completion = _get_litellm_completion()
|
|
150
|
+
except ModuleNotFoundError:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
kwargs: dict[str, object] = {
|
|
154
|
+
"model": preprocessor_model,
|
|
155
|
+
"messages": [
|
|
156
|
+
{"role": "system", "content": prompt},
|
|
157
|
+
{"role": "user", "content": message},
|
|
158
|
+
],
|
|
159
|
+
"api_key": api_key,
|
|
160
|
+
"temperature": 0,
|
|
161
|
+
}
|
|
162
|
+
api_base = os.getenv("OPENAI_BASE_URL")
|
|
163
|
+
if api_base:
|
|
164
|
+
kwargs["api_base"] = api_base
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
response = completion(**kwargs)
|
|
168
|
+
raw_output = _extract_response_content(response)
|
|
169
|
+
except Exception:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
parsed = parse_preprocessor_output(raw_output)
|
|
173
|
+
if parsed is None:
|
|
174
|
+
return None
|
|
175
|
+
return parsed
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _preprocess_last_user_message(message: str, state: State | None) -> str | None:
|
|
179
|
+
try:
|
|
180
|
+
heuristic_result = preprocess_heuristic(message)
|
|
181
|
+
if (
|
|
182
|
+
heuristic_result["outcome"] == PREPROCESS_OUTCOME_DIRECTIVE
|
|
183
|
+
and heuristic_result["directive"]
|
|
184
|
+
):
|
|
185
|
+
parsed = parse_preprocessor_output(heuristic_result["directive"])
|
|
186
|
+
if parsed is not None:
|
|
187
|
+
return parsed
|
|
188
|
+
except Exception:
|
|
189
|
+
logger.debug("litellm_proxy: heuristic_exception", exc_info=True)
|
|
190
|
+
|
|
191
|
+
if state is None:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
return _llm_fallback_preprocess(message, state)
|
|
196
|
+
except Exception:
|
|
197
|
+
logger.debug("litellm_proxy: fallback_exception", exc_info=True)
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class ContextCompilerPreCallHookWithPreprocessor(CustomLogger):
|
|
202
|
+
async def async_pre_call_hook(
|
|
203
|
+
self,
|
|
204
|
+
user_api_key_dict: Any,
|
|
205
|
+
cache: Any,
|
|
206
|
+
data: dict[str, object],
|
|
207
|
+
call_type: str,
|
|
208
|
+
) -> dict[str, object] | str:
|
|
209
|
+
del user_api_key_dict, cache
|
|
210
|
+
logger.debug("litellm_proxy: call_type=%s", call_type)
|
|
211
|
+
if call_type not in _SUPPORTED_CALL_TYPES:
|
|
212
|
+
return data
|
|
213
|
+
|
|
214
|
+
request_messages = _extract_request_messages(data)
|
|
215
|
+
logger.debug("litellm_proxy: message_count=%d", len(request_messages))
|
|
216
|
+
session = resolve_session_context(data)
|
|
217
|
+
logger.debug(
|
|
218
|
+
"litellm_proxy: mode=%s session_key_source=%s",
|
|
219
|
+
session.mode,
|
|
220
|
+
session.source,
|
|
221
|
+
)
|
|
222
|
+
if session.mode == MODE_PERSISTENT and session.session_key is None:
|
|
223
|
+
return (
|
|
224
|
+
"Context Compiler persistent mode requires a stable session key. "
|
|
225
|
+
"Set context_compiler_session_key or "
|
|
226
|
+
"metadata.context_compiler_session_key."
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
engine = create_engine()
|
|
230
|
+
if session.mode == MODE_PERSISTENT and session.session_key is not None:
|
|
231
|
+
checkpoint = CHECKPOINT_STORE.load(session.session_key)
|
|
232
|
+
if checkpoint is not None:
|
|
233
|
+
try:
|
|
234
|
+
engine.import_checkpoint_json(checkpoint_from_jsonable(checkpoint))
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
return (
|
|
237
|
+
"Context Compiler checkpoint load failed for session "
|
|
238
|
+
f"{session.session_key!r}: {exc}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
latest_user_text = extract_latest_user_text(request_messages)
|
|
242
|
+
logger.debug(
|
|
243
|
+
"litellm_proxy: latest_user_text_present=%s", latest_user_text is not None
|
|
244
|
+
)
|
|
245
|
+
engine_input = latest_user_text
|
|
246
|
+
drafted_input: str | None = None
|
|
247
|
+
|
|
248
|
+
if latest_user_text is not None and not engine.has_pending_clarification():
|
|
249
|
+
drafted_input = _preprocess_last_user_message(
|
|
250
|
+
latest_user_text, engine.state
|
|
251
|
+
)
|
|
252
|
+
logger.debug("litellm_proxy: drafted_input=%r", drafted_input)
|
|
253
|
+
if drafted_input is not None:
|
|
254
|
+
engine_input = drafted_input
|
|
255
|
+
|
|
256
|
+
if engine_input is not None:
|
|
257
|
+
decision = engine.step(engine_input)
|
|
258
|
+
else:
|
|
259
|
+
decision = {
|
|
260
|
+
"kind": DecisionKind.PASSTHROUGH,
|
|
261
|
+
"state": engine.state,
|
|
262
|
+
"prompt_to_user": None,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if session.mode == MODE_PERSISTENT and session.session_key is not None:
|
|
266
|
+
CHECKPOINT_STORE.save(
|
|
267
|
+
session.session_key,
|
|
268
|
+
checkpoint_to_jsonable(engine.export_checkpoint_json()),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
logger.debug("litellm_proxy: decision_kind=%s", decision["kind"])
|
|
272
|
+
|
|
273
|
+
if is_clarify(decision):
|
|
274
|
+
logger.debug("litellm_proxy: blocking_on_clarify=true")
|
|
275
|
+
return get_clarify_prompt(decision) or "Confirmation required."
|
|
276
|
+
|
|
277
|
+
compiled_state = engine.state
|
|
278
|
+
system_message: dict[str, object] = {
|
|
279
|
+
"role": "system",
|
|
280
|
+
"content": "You are a helpful assistant.\n"
|
|
281
|
+
+ _render_compiled_state_contract(compiled_state),
|
|
282
|
+
}
|
|
283
|
+
logger.debug("litellm_proxy: inject_system_message=true")
|
|
284
|
+
# Preserve original request messages; drafting changes only compiler input.
|
|
285
|
+
data["messages"] = [system_message, *request_messages]
|
|
286
|
+
return data
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
proxy_handler_instance = ContextCompilerPreCallHookWithPreprocessor()
|