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
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Opt-in live-model tool-gating comparison for MCP calendar admin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from importlib import import_module
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal, TypedDict, cast
|
|
11
|
+
|
|
12
|
+
from context_compiler import State, create_engine, get_decision_state, is_clarify
|
|
13
|
+
|
|
14
|
+
from context_compiler_example_integrations.examples._shared.litellm_request import (
|
|
15
|
+
build_litellm_provider_kwargs,
|
|
16
|
+
)
|
|
17
|
+
from context_compiler_example_integrations.examples.tool_gating.mcp_calendar_admin.example import (
|
|
18
|
+
CalendarAdminMcpHost,
|
|
19
|
+
McpToolCall,
|
|
20
|
+
)
|
|
21
|
+
from context_compiler_example_integrations.examples._shared.provider_mode import (
|
|
22
|
+
print_startup_config,
|
|
23
|
+
resolve_provider_config,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SideEffectRecord(TypedDict):
|
|
28
|
+
tool_name: str
|
|
29
|
+
calendar_id: str
|
|
30
|
+
event_title: str
|
|
31
|
+
authorization_source: Literal["context_compiler_state"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LiveModelResult(TypedDict):
|
|
35
|
+
decision_kind: Literal["clarify", "update", "passthrough"] | None
|
|
36
|
+
prompt_to_user: str | None
|
|
37
|
+
exposed_tool_names: list[str]
|
|
38
|
+
hidden_tool_names: list[str]
|
|
39
|
+
protected_tool_exposed: bool
|
|
40
|
+
model_selected_tool_name: str | None
|
|
41
|
+
executed: bool
|
|
42
|
+
blocked_reason: str | None
|
|
43
|
+
tool_result: str | None
|
|
44
|
+
execution_log: list[str]
|
|
45
|
+
side_effect_path: str
|
|
46
|
+
side_effect_count: int
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _LiteLLMCallKwargs(TypedDict, total=False):
|
|
50
|
+
model: str
|
|
51
|
+
messages: list[dict[str, str]]
|
|
52
|
+
tools: list[dict[str, object]]
|
|
53
|
+
tool_choice: str
|
|
54
|
+
temperature: float
|
|
55
|
+
drop_params: bool
|
|
56
|
+
api_base: str
|
|
57
|
+
api_key: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class _SelectedToolCall:
|
|
62
|
+
name: str | None
|
|
63
|
+
arguments: dict[str, str]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class CalendarAdminSideEffectStore:
|
|
68
|
+
"""Host-owned append-only artifact for successful protected execution."""
|
|
69
|
+
|
|
70
|
+
artifact_path: Path
|
|
71
|
+
|
|
72
|
+
def append(self, *, tool_call: McpToolCall) -> None:
|
|
73
|
+
record: SideEffectRecord = {
|
|
74
|
+
"tool_name": tool_call["tool_name"],
|
|
75
|
+
"calendar_id": tool_call["arguments"]["calendar_id"],
|
|
76
|
+
"event_title": tool_call["arguments"]["event_title"],
|
|
77
|
+
"authorization_source": "context_compiler_state",
|
|
78
|
+
}
|
|
79
|
+
self.artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
with self.artifact_path.open("a", encoding="utf-8") as artifact:
|
|
81
|
+
artifact.write(json.dumps(record, sort_keys=True) + "\n")
|
|
82
|
+
|
|
83
|
+
def count(self) -> int:
|
|
84
|
+
if not self.artifact_path.exists():
|
|
85
|
+
return 0
|
|
86
|
+
with self.artifact_path.open(encoding="utf-8") as artifact:
|
|
87
|
+
return sum(1 for _ in artifact)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_litellm_completion() -> Callable[..., object]:
|
|
91
|
+
litellm_module = import_module("litellm")
|
|
92
|
+
return cast(Callable[..., object], litellm_module.completion)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _state_for_request(authoritative_state: State | None) -> State | None:
|
|
96
|
+
return authoritative_state
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _decision_kind_name(
|
|
100
|
+
decision: object,
|
|
101
|
+
) -> Literal["clarify", "update", "passthrough"]:
|
|
102
|
+
if not isinstance(decision, dict):
|
|
103
|
+
raise ValueError("unexpected decision shape")
|
|
104
|
+
|
|
105
|
+
kind = decision.get("kind")
|
|
106
|
+
kind_name = getattr(kind, "value", None)
|
|
107
|
+
if kind_name not in {"clarify", "update", "passthrough"}:
|
|
108
|
+
raise ValueError(f"unexpected decision kind: {kind_name}")
|
|
109
|
+
|
|
110
|
+
return kind_name
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _exposed_tool_names(
|
|
114
|
+
host: CalendarAdminMcpHost, state: State
|
|
115
|
+
) -> tuple[list[str], list[str]]:
|
|
116
|
+
exposed_tools = host.exposed_mcp_tools(state)
|
|
117
|
+
return (
|
|
118
|
+
[tool["name"] for tool in exposed_tools["tools"]],
|
|
119
|
+
exposed_tools["hidden_tool_names"],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_openai_tools(
|
|
124
|
+
host: CalendarAdminMcpHost, state: State
|
|
125
|
+
) -> list[dict[str, object]]:
|
|
126
|
+
exposed_tools = host.exposed_mcp_tools(state)["tools"]
|
|
127
|
+
tools: list[dict[str, object]] = []
|
|
128
|
+
for tool in exposed_tools:
|
|
129
|
+
if tool["name"] == "calendar_view_events":
|
|
130
|
+
input_schema: dict[str, object] = {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"calendar_id": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "Calendar identifier to inspect.",
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"required": ["calendar_id"],
|
|
139
|
+
"additionalProperties": False,
|
|
140
|
+
}
|
|
141
|
+
elif tool["name"] == "calendar_admin_create_event":
|
|
142
|
+
input_schema = {
|
|
143
|
+
"type": "object",
|
|
144
|
+
"properties": {
|
|
145
|
+
"calendar_id": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"description": "Administrative calendar identifier.",
|
|
148
|
+
},
|
|
149
|
+
"event_title": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"description": "Administrative event title to create.",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
"required": ["calendar_id", "event_title"],
|
|
155
|
+
"additionalProperties": False,
|
|
156
|
+
}
|
|
157
|
+
else:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
tools.append(
|
|
161
|
+
{
|
|
162
|
+
"type": "function",
|
|
163
|
+
"function": {
|
|
164
|
+
"name": tool["name"],
|
|
165
|
+
"description": tool["description"],
|
|
166
|
+
"parameters": input_schema,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
return tools
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _build_messages(user_intent: str) -> list[dict[str, str]]:
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
"role": "system",
|
|
177
|
+
"content": (
|
|
178
|
+
"You are a careful assistant.\n"
|
|
179
|
+
"Use a tool if one is available to complete the user's request.\n"
|
|
180
|
+
"Do not invent unavailable tools."
|
|
181
|
+
),
|
|
182
|
+
},
|
|
183
|
+
{"role": "user", "content": user_intent},
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _mapping_value(obj: object, key: str) -> object | None:
|
|
188
|
+
if isinstance(obj, Mapping):
|
|
189
|
+
return obj.get(key)
|
|
190
|
+
return getattr(obj, key, None)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _extract_selected_tool_call(response: object) -> _SelectedToolCall:
|
|
194
|
+
choices = _mapping_value(response, "choices")
|
|
195
|
+
if not isinstance(choices, list) or not choices:
|
|
196
|
+
choices = getattr(response, "choices", None)
|
|
197
|
+
if not isinstance(choices, list) or not choices:
|
|
198
|
+
return _SelectedToolCall(name=None, arguments={})
|
|
199
|
+
|
|
200
|
+
message = _mapping_value(choices[0], "message")
|
|
201
|
+
if message is None:
|
|
202
|
+
return _SelectedToolCall(name=None, arguments={})
|
|
203
|
+
|
|
204
|
+
tool_calls = _mapping_value(message, "tool_calls")
|
|
205
|
+
if not isinstance(tool_calls, list) or not tool_calls:
|
|
206
|
+
return _SelectedToolCall(name=None, arguments={})
|
|
207
|
+
|
|
208
|
+
first_tool_call = tool_calls[0]
|
|
209
|
+
function_payload = _mapping_value(first_tool_call, "function")
|
|
210
|
+
if function_payload is None:
|
|
211
|
+
return _SelectedToolCall(name=None, arguments={})
|
|
212
|
+
|
|
213
|
+
name = _mapping_value(function_payload, "name")
|
|
214
|
+
raw_arguments = _mapping_value(function_payload, "arguments")
|
|
215
|
+
if not isinstance(name, str):
|
|
216
|
+
return _SelectedToolCall(name=None, arguments={})
|
|
217
|
+
|
|
218
|
+
if isinstance(raw_arguments, str):
|
|
219
|
+
decoded_arguments = json.loads(raw_arguments)
|
|
220
|
+
elif isinstance(raw_arguments, Mapping):
|
|
221
|
+
decoded_arguments = dict(raw_arguments)
|
|
222
|
+
else:
|
|
223
|
+
decoded_arguments = {}
|
|
224
|
+
|
|
225
|
+
arguments = {
|
|
226
|
+
key: str(value)
|
|
227
|
+
for key, value in decoded_arguments.items()
|
|
228
|
+
if isinstance(key, str)
|
|
229
|
+
}
|
|
230
|
+
return _SelectedToolCall(name=name, arguments=arguments)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _call_live_model(
|
|
234
|
+
*, user_intent: str, tools: list[dict[str, object]]
|
|
235
|
+
) -> _SelectedToolCall:
|
|
236
|
+
try:
|
|
237
|
+
completion = _get_litellm_completion()
|
|
238
|
+
except ModuleNotFoundError as exc:
|
|
239
|
+
raise RuntimeError(
|
|
240
|
+
"litellm is required. Install with: pip install litellm"
|
|
241
|
+
) from exc
|
|
242
|
+
|
|
243
|
+
config = resolve_provider_config(default_model="openai/gpt-4o-mini")
|
|
244
|
+
print_startup_config(config)
|
|
245
|
+
|
|
246
|
+
kwargs: _LiteLLMCallKwargs = {
|
|
247
|
+
**build_litellm_provider_kwargs(config),
|
|
248
|
+
"messages": _build_messages(user_intent),
|
|
249
|
+
"tools": tools,
|
|
250
|
+
"tool_choice": "auto",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
response = completion(**kwargs)
|
|
254
|
+
return _extract_selected_tool_call(response)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def run_live_model_turn(
|
|
258
|
+
*,
|
|
259
|
+
user_intent: str,
|
|
260
|
+
authoritative_state: State | None = None,
|
|
261
|
+
compiler_input: str = "",
|
|
262
|
+
artifact_path: Path | None = None,
|
|
263
|
+
model_tool_selector: Callable[..., _SelectedToolCall] | None = None,
|
|
264
|
+
) -> LiveModelResult:
|
|
265
|
+
"""Run one tool-gated turn with a live model or injected selector."""
|
|
266
|
+
|
|
267
|
+
if artifact_path is None:
|
|
268
|
+
artifact_path = Path(
|
|
269
|
+
"/tmp/context_compiler_mcp_calendar_admin/tool_calls.jsonl"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
side_effect_store = CalendarAdminSideEffectStore(artifact_path=artifact_path)
|
|
273
|
+
host = CalendarAdminMcpHost()
|
|
274
|
+
engine = create_engine(state=_state_for_request(authoritative_state))
|
|
275
|
+
decision_kind: Literal["clarify", "update", "passthrough"] | None = None
|
|
276
|
+
prompt_to_user: str | None = None
|
|
277
|
+
effective_state = engine.state
|
|
278
|
+
|
|
279
|
+
if compiler_input:
|
|
280
|
+
decision = engine.step(compiler_input)
|
|
281
|
+
decision_kind = _decision_kind_name(decision)
|
|
282
|
+
prompt_to_user = decision.get("prompt_to_user")
|
|
283
|
+
if is_clarify(decision):
|
|
284
|
+
exposed_tool_names, hidden_tool_names = _exposed_tool_names(
|
|
285
|
+
host, engine.state
|
|
286
|
+
)
|
|
287
|
+
return {
|
|
288
|
+
"decision_kind": decision_kind,
|
|
289
|
+
"prompt_to_user": prompt_to_user,
|
|
290
|
+
"exposed_tool_names": exposed_tool_names,
|
|
291
|
+
"hidden_tool_names": hidden_tool_names,
|
|
292
|
+
"protected_tool_exposed": "calendar_admin_create_event"
|
|
293
|
+
in exposed_tool_names,
|
|
294
|
+
"model_selected_tool_name": None,
|
|
295
|
+
"executed": False,
|
|
296
|
+
"blocked_reason": (
|
|
297
|
+
"clarification required before exposing calendar admin MCP tools"
|
|
298
|
+
),
|
|
299
|
+
"tool_result": None,
|
|
300
|
+
"execution_log": host.execution_log.copy(),
|
|
301
|
+
"side_effect_path": str(side_effect_store.artifact_path),
|
|
302
|
+
"side_effect_count": side_effect_store.count(),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
decision_state = get_decision_state(decision)
|
|
306
|
+
effective_state = decision_state if decision_state is not None else engine.state
|
|
307
|
+
|
|
308
|
+
exposed_tool_names, hidden_tool_names = _exposed_tool_names(host, effective_state)
|
|
309
|
+
protected_tool_exposed = "calendar_admin_create_event" in exposed_tool_names
|
|
310
|
+
tools = _build_openai_tools(host, effective_state)
|
|
311
|
+
selector = model_tool_selector or _call_live_model
|
|
312
|
+
selected_tool_call = selector(user_intent=user_intent, tools=tools)
|
|
313
|
+
|
|
314
|
+
if selected_tool_call.name != "calendar_admin_create_event":
|
|
315
|
+
return {
|
|
316
|
+
"decision_kind": decision_kind,
|
|
317
|
+
"prompt_to_user": prompt_to_user,
|
|
318
|
+
"exposed_tool_names": exposed_tool_names,
|
|
319
|
+
"hidden_tool_names": hidden_tool_names,
|
|
320
|
+
"protected_tool_exposed": protected_tool_exposed,
|
|
321
|
+
"model_selected_tool_name": selected_tool_call.name,
|
|
322
|
+
"executed": False,
|
|
323
|
+
"blocked_reason": (
|
|
324
|
+
"model did not select protected admin tool"
|
|
325
|
+
if protected_tool_exposed
|
|
326
|
+
else "protected admin tool was not exposed to the model"
|
|
327
|
+
),
|
|
328
|
+
"tool_result": None,
|
|
329
|
+
"execution_log": host.execution_log.copy(),
|
|
330
|
+
"side_effect_path": str(side_effect_store.artifact_path),
|
|
331
|
+
"side_effect_count": side_effect_store.count(),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if not protected_tool_exposed:
|
|
335
|
+
return {
|
|
336
|
+
"decision_kind": decision_kind,
|
|
337
|
+
"prompt_to_user": prompt_to_user,
|
|
338
|
+
"exposed_tool_names": exposed_tool_names,
|
|
339
|
+
"hidden_tool_names": hidden_tool_names,
|
|
340
|
+
"protected_tool_exposed": False,
|
|
341
|
+
"model_selected_tool_name": selected_tool_call.name,
|
|
342
|
+
"executed": False,
|
|
343
|
+
"blocked_reason": "calendar_admin state not authorized",
|
|
344
|
+
"tool_result": None,
|
|
345
|
+
"execution_log": host.execution_log.copy(),
|
|
346
|
+
"side_effect_path": str(side_effect_store.artifact_path),
|
|
347
|
+
"side_effect_count": side_effect_store.count(),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
tool_call: McpToolCall = {
|
|
351
|
+
"tool_name": "calendar_admin_create_event",
|
|
352
|
+
"arguments": selected_tool_call.arguments,
|
|
353
|
+
}
|
|
354
|
+
tool_result = host.execute_mcp_tool(tool_call)
|
|
355
|
+
side_effect_store.append(tool_call=tool_call)
|
|
356
|
+
return {
|
|
357
|
+
"decision_kind": decision_kind,
|
|
358
|
+
"prompt_to_user": prompt_to_user,
|
|
359
|
+
"exposed_tool_names": exposed_tool_names,
|
|
360
|
+
"hidden_tool_names": hidden_tool_names,
|
|
361
|
+
"protected_tool_exposed": True,
|
|
362
|
+
"model_selected_tool_name": selected_tool_call.name,
|
|
363
|
+
"executed": True,
|
|
364
|
+
"blocked_reason": None,
|
|
365
|
+
"tool_result": tool_result,
|
|
366
|
+
"execution_log": host.execution_log.copy(),
|
|
367
|
+
"side_effect_path": str(side_effect_store.artifact_path),
|
|
368
|
+
"side_effect_count": side_effect_store.count(),
|
|
369
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Reference integrations for installed and editable package use."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_PACKAGE_DIR = Path(__file__).resolve().parent
|
|
8
|
+
_SOURCE_DIR = _PACKAGE_DIR.parents[1] / "python" / "reference_integrations"
|
|
9
|
+
|
|
10
|
+
__path__ = [str(_PACKAGE_DIR)]
|
|
11
|
+
if _SOURCE_DIR.is_dir():
|
|
12
|
+
__path__.append(str(_SOURCE_DIR))
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# LiteLLM Proxy (pre-call hook)
|
|
2
|
+
|
|
3
|
+
Saved compiler state changes whether LiteLLM Proxy blocks a request, forwards
|
|
4
|
+
it unchanged, or injects compiler state before the downstream call. This
|
|
5
|
+
example shows LiteLLM Proxy acting as the host-owned gateway surface.
|
|
6
|
+
|
|
7
|
+
Context Compiler is the authority layer for saved state. These hooks no longer
|
|
8
|
+
derive authority from transcript history. They process only the latest user
|
|
9
|
+
turn and rely on host-owned checkpoints for continuity.
|
|
10
|
+
|
|
11
|
+
Available hook files:
|
|
12
|
+
|
|
13
|
+
- Basic checkpoint-backed hook: `context_compiler_precall_hook.py`
|
|
14
|
+
- Directive-drafter-enabled checkpoint-backed hook:
|
|
15
|
+
`context_compiler_precall_hook_with_directive_drafter.py`
|
|
16
|
+
|
|
17
|
+
## Runtime behavior
|
|
18
|
+
|
|
19
|
+
- LiteLLM Proxy is the gateway surface; Context Compiler remains the authority
|
|
20
|
+
layer for saved state.
|
|
21
|
+
- By default, the hooks run in stateless mode and process only the latest user
|
|
22
|
+
turn with a fresh engine.
|
|
23
|
+
- In explicit persistent mode, the hook resolves a session key, loads a saved
|
|
24
|
+
checkpoint, restores the engine, processes the latest user turn once, and
|
|
25
|
+
saves the resulting checkpoint after every decision, including `clarify`.
|
|
26
|
+
- In stateless mode, no continuity is preserved across requests.
|
|
27
|
+
- If result is `clarify`, the proxy does not call the downstream model and
|
|
28
|
+
LiteLLM surfaces the clarification as an HTTP 400 response.
|
|
29
|
+
- If result is `passthrough`, the proxy forwards the request normally.
|
|
30
|
+
- If result is `update`, the proxy injects compiler state as a system message
|
|
31
|
+
and then calls the model.
|
|
32
|
+
- Unsupported LiteLLM callback `call_type` values return the original request
|
|
33
|
+
data unchanged.
|
|
34
|
+
|
|
35
|
+
Optional directive-drafter behavior:
|
|
36
|
+
|
|
37
|
+
- The drafter runs only on the current/latest user turn.
|
|
38
|
+
- The hook restores checkpoints before drafting.
|
|
39
|
+
- Heuristic runs first; if no directive is found, LLM fallback is attempted.
|
|
40
|
+
- Forwarded upstream request messages are not rewritten except for the injected
|
|
41
|
+
compiler system message.
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
## Session mode and session keys
|
|
46
|
+
|
|
47
|
+
The reference hooks support two explicit modes:
|
|
48
|
+
|
|
49
|
+
- `persistent`
|
|
50
|
+
- explicit mode
|
|
51
|
+
- requires a stable session key
|
|
52
|
+
- preserves saved state and pending clarification across requests
|
|
53
|
+
- `stateless`
|
|
54
|
+
- default mode
|
|
55
|
+
- processes only the latest user turn
|
|
56
|
+
- preserves no continuity
|
|
57
|
+
|
|
58
|
+
Set the mode with one of:
|
|
59
|
+
|
|
60
|
+
- top-level request field `context_compiler_mode`
|
|
61
|
+
- env var `CONTEXT_COMPILER_SESSION_MODE`
|
|
62
|
+
|
|
63
|
+
Persistent mode resolves session keys in this order:
|
|
64
|
+
|
|
65
|
+
1. `context_compiler_session_key`
|
|
66
|
+
2. `metadata.context_compiler_session_key`
|
|
67
|
+
|
|
68
|
+
If persistent mode cannot resolve a stable session key, the hook fails clearly
|
|
69
|
+
and does not fall back to transcript replay or implicit stateless behavior.
|
|
70
|
+
|
|
71
|
+
## Checkpoint store contract
|
|
72
|
+
|
|
73
|
+
The hooks share a small repo-local support module:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
class CheckpointStore(Protocol):
|
|
77
|
+
def load(self, session_key: str) -> Mapping[str, object] | None: ...
|
|
78
|
+
def save(self, session_key: str, checkpoint: Mapping[str, object]) -> None: ...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The reference implementation uses an in-memory store for tests and local demos.
|
|
82
|
+
It is single-process only:
|
|
83
|
+
|
|
84
|
+
- no durability across restarts
|
|
85
|
+
- no multi-worker coordination
|
|
86
|
+
- no atomic compare-and-swap
|
|
87
|
+
- no expiration/cleanup policy
|
|
88
|
+
|
|
89
|
+
Real deployments should replace it with a host-owned store such as Redis or a
|
|
90
|
+
database-backed implementation.
|
|
91
|
+
|
|
92
|
+
## Requirements
|
|
93
|
+
|
|
94
|
+
```shell
|
|
95
|
+
pip install "context-compiler-example-integrations[litellm]"
|
|
96
|
+
export OPENAI_API_KEY=...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Start with the compiler-only hook. Add `context-compiler-directive-drafter`
|
|
100
|
+
only if you want the optional directive-drafter variant.
|
|
101
|
+
|
|
102
|
+
For `context_compiler_precall_hook_with_directive_drafter.py`:
|
|
103
|
+
|
|
104
|
+
```shell
|
|
105
|
+
pip install "context-compiler-example-integrations[all]"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That variant requires `context-compiler-directive-drafter>=0.1.2`.
|
|
109
|
+
|
|
110
|
+
For the opt-in runtime smoke test, install the proxy runtime extras:
|
|
111
|
+
|
|
112
|
+
```shell
|
|
113
|
+
uv sync --group proxy_runtime --no-editable
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Quickstart (copy/paste)
|
|
117
|
+
|
|
118
|
+
From the repo root:
|
|
119
|
+
|
|
120
|
+
```shell
|
|
121
|
+
pip install "context-compiler-example-integrations[litellm]"
|
|
122
|
+
export OPENAI_API_KEY=...
|
|
123
|
+
litellm --config python/reference_integrations/litellm_proxy/config.example.yaml
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`config.example.yaml` includes both OpenAI and Ollama model definitions.
|
|
127
|
+
Use the Ollama model entry for local testing without API credentials.
|
|
128
|
+
|
|
129
|
+
## Make a request
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from openai import OpenAI
|
|
133
|
+
|
|
134
|
+
client = OpenAI(
|
|
135
|
+
api_key="anything",
|
|
136
|
+
base_url="http://localhost:4000",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
response = client.chat.completions.create(
|
|
140
|
+
model="gpt-4o-mini",
|
|
141
|
+
messages=[{"role": "user", "content": "prohibit peanuts"}],
|
|
142
|
+
extra_body={"context_compiler_session_key": "demo-chat"},
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Or with curl:
|
|
147
|
+
|
|
148
|
+
```shell
|
|
149
|
+
curl http://localhost:4000/v1/chat/completions \
|
|
150
|
+
-H "Content-Type: application/json" \
|
|
151
|
+
-H "Authorization: Bearer anything" \
|
|
152
|
+
-d '{
|
|
153
|
+
"model": "gpt-4o-mini",
|
|
154
|
+
"context_compiler_session_key": "demo-chat",
|
|
155
|
+
"messages": [{"role": "user", "content": "prohibit peanuts"}]
|
|
156
|
+
}'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For explicit stateless mode:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"model": "gpt-4o-mini",
|
|
164
|
+
"context_compiler_mode": "stateless",
|
|
165
|
+
"messages": [{"role": "user", "content": "prohibit peanuts"}]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
For explicit persistent mode:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"model": "gpt-4o-mini",
|
|
174
|
+
"context_compiler_mode": "persistent",
|
|
175
|
+
"context_compiler_session_key": "demo-chat",
|
|
176
|
+
"messages": [{"role": "user", "content": "prohibit peanuts"}]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Run proxy
|
|
181
|
+
|
|
182
|
+
The reference integration is covered by unit tests and an opt-in runtime smoke
|
|
183
|
+
test. See "Opt-in Runtime Smoke Test" below for details.
|
|
184
|
+
|
|
185
|
+
The proxy runs on `http://localhost:4000` by default. By default,
|
|
186
|
+
`config.example.yaml` points to the basic checkpoint-backed hook. To use the
|
|
187
|
+
directive-drafter variant, switch the callback path in the config. The callback
|
|
188
|
+
path must be importable by LiteLLM in the environment where the proxy process
|
|
189
|
+
starts.
|
|
190
|
+
|
|
191
|
+
When starting LiteLLM from the repo root, prefer fully qualified callback
|
|
192
|
+
imports in automated configs, for example:
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
python.reference_integrations.litellm_proxy.context_compiler_precall_hook.proxy_handler_instance
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Optional env vars for directive-drafter fallback:
|
|
199
|
+
|
|
200
|
+
```shell
|
|
201
|
+
export PREPROCESSOR_MODEL=openai/gpt-4o-mini
|
|
202
|
+
export PREPROCESSOR_PROMPT_PROFILE=default
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`PREPROCESSOR_MODEL` is optional and defaults to `MODEL`.
|
|
206
|
+
|
|
207
|
+
For heuristic-first usage, keep `PREPROCESSOR_PROMPT_PROFILE=default`.
|
|
208
|
+
Use `llama` only for LLM-only fallback drafting with Llama-family models.
|
|
209
|
+
|
|
210
|
+
## Notes
|
|
211
|
+
|
|
212
|
+
- Mixed-content user messages compile only text segments from the latest user
|
|
213
|
+
turn.
|
|
214
|
+
- `MODEL` and `PREPROCESSOR_MODEL` use LiteLLM format: `<provider>/<model>`.
|
|
215
|
+
- Corrupt or incompatible checkpoints fail clearly in persistent mode and do
|
|
216
|
+
not silently reset state.
|
|
217
|
+
- In the directive-drafter hook, drafter state context now comes from restored
|
|
218
|
+
checkpoint state rather than transcript-prefix reconstruction.
|
|
219
|
+
- Compound directive-shaped input such as `use docker and prohibit peanuts`
|
|
220
|
+
should produce a local clarify response telling the user to submit each
|
|
221
|
+
directive separately, without mutating saved state or forwarding upstream.
|
|
222
|
+
|
|
223
|
+
## Troubleshooting
|
|
224
|
+
|
|
225
|
+
- callback import failures: verify the callback path configured in
|
|
226
|
+
`config.example.yaml` is importable in the current LiteLLM environment
|
|
227
|
+
- persistent mode rejects requests: provide a stable session key or opt into
|
|
228
|
+
explicit `stateless` mode
|
|
229
|
+
- proxy starts but upstream calls fail: check `OPENAI_API_KEY` and upstream
|
|
230
|
+
model/provider config in `config.example.yaml`
|
|
231
|
+
- directive-drafter fallback issues: `PREPROCESSOR_MODEL` defaults to `MODEL`;
|
|
232
|
+
set it explicitly only when using a separate fallback model
|
|
233
|
+
|
|
234
|
+
## Opt-in Runtime Smoke Test
|
|
235
|
+
|
|
236
|
+
This repo includes an opt-in runtime smoke test for the LiteLLM Proxy
|
|
237
|
+
reference integration. The test starts a real LiteLLM Proxy process, runs the
|
|
238
|
+
basic hook and the directive-drafter hook in separate proxy launches, sends
|
|
239
|
+
local requests through the proxy with explicit session keys, verifies blocked
|
|
240
|
+
requests do not reach upstream, verifies allowed requests reach a local stub
|
|
241
|
+
upstream with the injected compiler contract, verifies the directive-drafter
|
|
242
|
+
path preserves the original forwarded user prompt text, and shuts each proxy
|
|
243
|
+
down cleanly.
|
|
244
|
+
|
|
245
|
+
It is intentionally not part of `./scripts/validate_python.sh`.
|
|
246
|
+
|
|
247
|
+
Run it from the repo root:
|
|
248
|
+
|
|
249
|
+
```shell
|
|
250
|
+
RUN_LITELLM_PROXY_RUNTIME=1 uv run --group proxy_runtime pytest python/tests/test_litellm_proxy_runtime.py
|
|
251
|
+
```
|