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,312 @@
|
|
|
1
|
+
"""FastAPI comparison demo for execution authorization with expense approval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal, cast
|
|
10
|
+
|
|
11
|
+
from context_compiler import State, create_engine, get_decision_state, is_clarify
|
|
12
|
+
from fastapi import FastAPI, HTTPException
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from typing_extensions import TypedDict
|
|
15
|
+
|
|
16
|
+
from context_compiler_example_integrations.examples.execution_authorization.expense_approval.example import (
|
|
17
|
+
expense_execution_is_authorized,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .model_approval import ModelApproval, get_model_approval_claim
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExpenseRequest(BaseModel):
|
|
24
|
+
expense_id: str
|
|
25
|
+
employee_id: str
|
|
26
|
+
amount_usd: int
|
|
27
|
+
note: str
|
|
28
|
+
authoritative_state: dict[str, object] | None = None
|
|
29
|
+
compiler_input: str = ""
|
|
30
|
+
agent_claim: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SideEffectRecord(TypedDict):
|
|
34
|
+
expense_id: str
|
|
35
|
+
employee_id: str
|
|
36
|
+
amount_usd: int
|
|
37
|
+
note: str
|
|
38
|
+
path: Literal["baseline", "compiler"]
|
|
39
|
+
model_decision: str
|
|
40
|
+
authorization_source: Literal["model_claim", "context_compiler_state"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExpenseMutationResponse(TypedDict):
|
|
44
|
+
path: Literal["baseline", "compiler"]
|
|
45
|
+
decision_kind: Literal["clarify", "update", "passthrough"] | None
|
|
46
|
+
model_decision: str
|
|
47
|
+
model_message: str
|
|
48
|
+
agent_claim: str | None
|
|
49
|
+
authorization_state: Literal["authorized", "blocked"]
|
|
50
|
+
executed: bool
|
|
51
|
+
blocked_reason: str | None
|
|
52
|
+
prompt_to_user: str | None
|
|
53
|
+
submission: dict[str, str | int] | None
|
|
54
|
+
side_effect_path: str
|
|
55
|
+
side_effect_count: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _decision_kind_name(
|
|
59
|
+
decision: object,
|
|
60
|
+
) -> Literal["clarify", "update", "passthrough"]:
|
|
61
|
+
if not isinstance(decision, dict):
|
|
62
|
+
raise ValueError("unexpected decision shape")
|
|
63
|
+
|
|
64
|
+
kind = decision.get("kind")
|
|
65
|
+
kind_name = getattr(kind, "value", None)
|
|
66
|
+
if kind_name not in {"clarify", "update", "passthrough"}:
|
|
67
|
+
raise ValueError(f"unexpected decision kind: {kind_name}")
|
|
68
|
+
|
|
69
|
+
return kind_name
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _state_for_request(authoritative_state: dict[str, object] | None) -> State | None:
|
|
73
|
+
if authoritative_state is None:
|
|
74
|
+
return None
|
|
75
|
+
return cast(State, authoritative_state)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _expense_summary(request: ExpenseRequest) -> str:
|
|
79
|
+
return (
|
|
80
|
+
f"expense_id={request.expense_id}; "
|
|
81
|
+
f"employee_id={request.employee_id}; "
|
|
82
|
+
f"amount_usd={request.amount_usd}; "
|
|
83
|
+
f"note={request.note}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class ExpenseSideEffectStore:
|
|
89
|
+
"""Host-owned append-only side-effect artifact for the comparison demo."""
|
|
90
|
+
|
|
91
|
+
artifact_path: Path
|
|
92
|
+
|
|
93
|
+
def append(
|
|
94
|
+
self,
|
|
95
|
+
*,
|
|
96
|
+
request: ExpenseRequest,
|
|
97
|
+
path_name: Literal["baseline", "compiler"],
|
|
98
|
+
model_decision: str,
|
|
99
|
+
authorization_source: Literal["model_claim", "context_compiler_state"],
|
|
100
|
+
) -> dict[str, str | int]:
|
|
101
|
+
record: SideEffectRecord = {
|
|
102
|
+
"expense_id": request.expense_id,
|
|
103
|
+
"employee_id": request.employee_id,
|
|
104
|
+
"amount_usd": request.amount_usd,
|
|
105
|
+
"note": request.note,
|
|
106
|
+
"path": path_name,
|
|
107
|
+
"model_decision": model_decision,
|
|
108
|
+
"authorization_source": authorization_source,
|
|
109
|
+
}
|
|
110
|
+
self.artifact_path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
with self.artifact_path.open("a", encoding="utf-8") as artifact:
|
|
112
|
+
artifact.write(json.dumps(record, sort_keys=True) + "\n")
|
|
113
|
+
return {
|
|
114
|
+
"expense_id": request.expense_id,
|
|
115
|
+
"employee_id": request.employee_id,
|
|
116
|
+
"amount_usd": request.amount_usd,
|
|
117
|
+
"note": request.note,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def count(self) -> int:
|
|
121
|
+
if not self.artifact_path.exists():
|
|
122
|
+
return 0
|
|
123
|
+
with self.artifact_path.open(encoding="utf-8") as artifact:
|
|
124
|
+
return sum(1 for _ in artifact)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _blocked_response(
|
|
128
|
+
*,
|
|
129
|
+
path_name: Literal["baseline", "compiler"],
|
|
130
|
+
model_claim: ModelApproval,
|
|
131
|
+
side_effect_store: ExpenseSideEffectStore,
|
|
132
|
+
blocked_reason: str,
|
|
133
|
+
prompt_to_user: str | None,
|
|
134
|
+
decision_kind: Literal["clarify", "update", "passthrough"] | None,
|
|
135
|
+
request_agent_claim: str | None,
|
|
136
|
+
) -> ExpenseMutationResponse:
|
|
137
|
+
return {
|
|
138
|
+
"path": path_name,
|
|
139
|
+
"decision_kind": decision_kind,
|
|
140
|
+
"model_decision": model_claim.decision,
|
|
141
|
+
"model_message": model_claim.message,
|
|
142
|
+
"agent_claim": request_agent_claim,
|
|
143
|
+
"authorization_state": "blocked",
|
|
144
|
+
"executed": False,
|
|
145
|
+
"blocked_reason": blocked_reason,
|
|
146
|
+
"prompt_to_user": prompt_to_user,
|
|
147
|
+
"submission": None,
|
|
148
|
+
"side_effect_path": str(side_effect_store.artifact_path),
|
|
149
|
+
"side_effect_count": side_effect_store.count(),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _authorized_response(
|
|
154
|
+
*,
|
|
155
|
+
path_name: Literal["baseline", "compiler"],
|
|
156
|
+
model_claim: ModelApproval,
|
|
157
|
+
side_effect_store: ExpenseSideEffectStore,
|
|
158
|
+
submission: dict[str, str | int],
|
|
159
|
+
decision_kind: Literal["clarify", "update", "passthrough"] | None,
|
|
160
|
+
request_agent_claim: str | None,
|
|
161
|
+
) -> ExpenseMutationResponse:
|
|
162
|
+
return {
|
|
163
|
+
"path": path_name,
|
|
164
|
+
"decision_kind": decision_kind,
|
|
165
|
+
"model_decision": model_claim.decision,
|
|
166
|
+
"model_message": model_claim.message,
|
|
167
|
+
"agent_claim": request_agent_claim,
|
|
168
|
+
"authorization_state": "authorized",
|
|
169
|
+
"executed": True,
|
|
170
|
+
"blocked_reason": None,
|
|
171
|
+
"prompt_to_user": None,
|
|
172
|
+
"submission": submission,
|
|
173
|
+
"side_effect_path": str(side_effect_store.artifact_path),
|
|
174
|
+
"side_effect_count": side_effect_store.count(),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def create_app(
|
|
179
|
+
*,
|
|
180
|
+
artifact_path: Path | None = None,
|
|
181
|
+
model_approval_getter: Callable[[str], ModelApproval] = get_model_approval_claim,
|
|
182
|
+
) -> FastAPI:
|
|
183
|
+
if artifact_path is None:
|
|
184
|
+
artifact_path = Path(
|
|
185
|
+
"/tmp/context_compiler_expense_approval/submitted_expenses.jsonl"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
side_effect_store = ExpenseSideEffectStore(artifact_path=artifact_path)
|
|
189
|
+
app = FastAPI(title="expense-approval-fastapi-comparison")
|
|
190
|
+
app.state.side_effect_store = side_effect_store
|
|
191
|
+
|
|
192
|
+
@app.post("/baseline/expenses")
|
|
193
|
+
def submit_baseline_expense(request: ExpenseRequest) -> ExpenseMutationResponse:
|
|
194
|
+
model_claim = model_approval_getter(_expense_summary(request))
|
|
195
|
+
if not model_claim.is_approved:
|
|
196
|
+
raise HTTPException(
|
|
197
|
+
status_code=403,
|
|
198
|
+
detail=_blocked_response(
|
|
199
|
+
path_name="baseline",
|
|
200
|
+
model_claim=model_claim,
|
|
201
|
+
side_effect_store=side_effect_store,
|
|
202
|
+
blocked_reason="model claim did not approve expense",
|
|
203
|
+
prompt_to_user=None,
|
|
204
|
+
decision_kind=None,
|
|
205
|
+
request_agent_claim=request.agent_claim,
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
submission = side_effect_store.append(
|
|
210
|
+
request=request,
|
|
211
|
+
path_name="baseline",
|
|
212
|
+
model_decision=model_claim.decision,
|
|
213
|
+
authorization_source="model_claim",
|
|
214
|
+
)
|
|
215
|
+
return _authorized_response(
|
|
216
|
+
path_name="baseline",
|
|
217
|
+
model_claim=model_claim,
|
|
218
|
+
side_effect_store=side_effect_store,
|
|
219
|
+
submission=submission,
|
|
220
|
+
decision_kind=None,
|
|
221
|
+
request_agent_claim=request.agent_claim,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@app.post("/compiler/expenses")
|
|
225
|
+
def submit_compiler_mediated_expense(
|
|
226
|
+
request: ExpenseRequest,
|
|
227
|
+
) -> ExpenseMutationResponse:
|
|
228
|
+
model_claim = model_approval_getter(_expense_summary(request))
|
|
229
|
+
if not model_claim.is_approved:
|
|
230
|
+
raise HTTPException(
|
|
231
|
+
status_code=403,
|
|
232
|
+
detail=_blocked_response(
|
|
233
|
+
path_name="compiler",
|
|
234
|
+
model_claim=model_claim,
|
|
235
|
+
side_effect_store=side_effect_store,
|
|
236
|
+
blocked_reason="model claim did not approve expense",
|
|
237
|
+
prompt_to_user=None,
|
|
238
|
+
decision_kind=None,
|
|
239
|
+
request_agent_claim=request.agent_claim,
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
engine = create_engine(state=_state_for_request(request.authoritative_state))
|
|
244
|
+
decision_kind: Literal["clarify", "update", "passthrough"] | None = None
|
|
245
|
+
prompt_to_user: str | None = None
|
|
246
|
+
authoritative_state = engine.state
|
|
247
|
+
|
|
248
|
+
if request.compiler_input:
|
|
249
|
+
decision = engine.step(request.compiler_input)
|
|
250
|
+
decision_kind = _decision_kind_name(decision)
|
|
251
|
+
prompt_to_user = decision.get("prompt_to_user")
|
|
252
|
+
if is_clarify(decision):
|
|
253
|
+
raise HTTPException(
|
|
254
|
+
status_code=409,
|
|
255
|
+
detail=_blocked_response(
|
|
256
|
+
path_name="compiler",
|
|
257
|
+
model_claim=model_claim,
|
|
258
|
+
side_effect_store=side_effect_store,
|
|
259
|
+
blocked_reason=(
|
|
260
|
+
"clarification required before expense execution"
|
|
261
|
+
),
|
|
262
|
+
prompt_to_user=prompt_to_user,
|
|
263
|
+
decision_kind=decision_kind,
|
|
264
|
+
request_agent_claim=request.agent_claim,
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
decision_state = get_decision_state(decision)
|
|
269
|
+
authoritative_state = (
|
|
270
|
+
decision_state if decision_state is not None else engine.state
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if not expense_execution_is_authorized(authoritative_state):
|
|
274
|
+
raise HTTPException(
|
|
275
|
+
status_code=403,
|
|
276
|
+
detail=_blocked_response(
|
|
277
|
+
path_name="compiler",
|
|
278
|
+
model_claim=model_claim,
|
|
279
|
+
side_effect_store=side_effect_store,
|
|
280
|
+
blocked_reason="expense_approval state not authorized",
|
|
281
|
+
prompt_to_user=prompt_to_user,
|
|
282
|
+
decision_kind=decision_kind,
|
|
283
|
+
request_agent_claim=request.agent_claim,
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
submission = side_effect_store.append(
|
|
288
|
+
request=request,
|
|
289
|
+
path_name="compiler",
|
|
290
|
+
model_decision=model_claim.decision,
|
|
291
|
+
authorization_source="context_compiler_state",
|
|
292
|
+
)
|
|
293
|
+
return _authorized_response(
|
|
294
|
+
path_name="compiler",
|
|
295
|
+
model_claim=model_claim,
|
|
296
|
+
side_effect_store=side_effect_store,
|
|
297
|
+
submission=submission,
|
|
298
|
+
decision_kind=decision_kind,
|
|
299
|
+
request_agent_claim=request.agent_claim,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return app
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
app = create_app()
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
print(
|
|
310
|
+
"Run with: uv run fastapi dev "
|
|
311
|
+
"python/examples/execution_authorization/expense_approval/fastapi/app.py"
|
|
312
|
+
)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Minimal provider-backed approval claim helper for the FastAPI demo."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from context_compiler_example_integrations.examples._shared.litellm_request import (
|
|
11
|
+
build_litellm_provider_kwargs,
|
|
12
|
+
)
|
|
13
|
+
from context_compiler_example_integrations.examples._shared.provider_mode import (
|
|
14
|
+
print_startup_config,
|
|
15
|
+
resolve_provider_config,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ModelApproval:
|
|
21
|
+
decision: str
|
|
22
|
+
message: str
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def is_approved(self) -> bool:
|
|
26
|
+
return self.decision == "approved"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_litellm_completion() -> Callable[..., object]:
|
|
30
|
+
litellm_module = import_module("litellm")
|
|
31
|
+
return cast(Callable[..., object], litellm_module.completion)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_response_content(response: object) -> str | None:
|
|
35
|
+
if isinstance(response, Mapping):
|
|
36
|
+
choices = response.get("choices")
|
|
37
|
+
if isinstance(choices, list) and choices:
|
|
38
|
+
first = choices[0]
|
|
39
|
+
if isinstance(first, Mapping):
|
|
40
|
+
message = first.get("message")
|
|
41
|
+
if isinstance(message, Mapping):
|
|
42
|
+
content = message.get("content")
|
|
43
|
+
if isinstance(content, str):
|
|
44
|
+
return content
|
|
45
|
+
|
|
46
|
+
choices_attr = getattr(response, "choices", None)
|
|
47
|
+
if isinstance(choices_attr, list) and choices_attr:
|
|
48
|
+
first = choices_attr[0]
|
|
49
|
+
message_attr = getattr(first, "message", None)
|
|
50
|
+
content_attr = getattr(message_attr, "content", None)
|
|
51
|
+
if isinstance(content_attr, str):
|
|
52
|
+
return content_attr
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def classify_model_approval(message: str) -> str:
|
|
58
|
+
normalized = message.strip().lower()
|
|
59
|
+
if normalized.startswith("approved"):
|
|
60
|
+
return "approved"
|
|
61
|
+
if " approved" in f" {normalized} ":
|
|
62
|
+
return "approved"
|
|
63
|
+
return "not_approved"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_model_approval_claim(expense_summary: str) -> ModelApproval:
|
|
67
|
+
"""Return an approval-class claim from a live provider."""
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
completion = _get_litellm_completion()
|
|
71
|
+
except ModuleNotFoundError as exc:
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
"litellm is required. Install with: pip install litellm"
|
|
74
|
+
) from exc
|
|
75
|
+
|
|
76
|
+
config = resolve_provider_config(default_model="openai/gpt-4o-mini")
|
|
77
|
+
print_startup_config(config)
|
|
78
|
+
|
|
79
|
+
prompt = (
|
|
80
|
+
"You are an expense approver.\n"
|
|
81
|
+
"Reply with a short decision line that starts with APPROVED or DENIED.\n"
|
|
82
|
+
"Expense:\n"
|
|
83
|
+
f"{expense_summary}"
|
|
84
|
+
)
|
|
85
|
+
kwargs: dict[str, object] = {
|
|
86
|
+
**build_litellm_provider_kwargs(config),
|
|
87
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
response = completion(**kwargs)
|
|
91
|
+
message = _extract_response_content(response)
|
|
92
|
+
if message is None:
|
|
93
|
+
raise RuntimeError("LiteLLM response missing choices[0].message.content")
|
|
94
|
+
|
|
95
|
+
return ModelApproval(
|
|
96
|
+
decision=classify_model_approval(message),
|
|
97
|
+
message=message,
|
|
98
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Gateway middleware
|
|
2
|
+
|
|
3
|
+
These examples show a host-owned gateway making an allow / block / route
|
|
4
|
+
decision before any downstream service call happens.
|
|
5
|
+
|
|
6
|
+
Context Compiler owns the authoritative policy state.
|
|
7
|
+
|
|
8
|
+
The host owns the gateway boundary, the default route, and the downstream
|
|
9
|
+
handler invocation.
|
|
10
|
+
|
|
11
|
+
Adversarial wording does not bypass the gateway or mutate authoritative state.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### `customer_support_routing`
|
|
16
|
+
|
|
17
|
+
Routes a host-owned customer support request to `billing_support` only when
|
|
18
|
+
authoritative state contains:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
use billing_support
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If state is absent, the gateway blocks billing-routed requests and still allows
|
|
25
|
+
non-billing requests to follow the documented default path,
|
|
26
|
+
`general_support`.
|
|
27
|
+
|
|
28
|
+
If state contains:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
prohibit billing_support
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
the gateway blocks billing-routed requests before the downstream handler is
|
|
35
|
+
called.
|
|
36
|
+
|
|
37
|
+
The tests cover default-path behavior, authorized routing, blocked routing,
|
|
38
|
+
adversarial text, downstream non-invocation when blocked, and contradiction /
|
|
39
|
+
clarification behavior.
|
|
40
|
+
|
|
41
|
+
## Related integrations
|
|
42
|
+
|
|
43
|
+
The generic example teaches the gateway-middleware enforcement point in a small
|
|
44
|
+
host-owned flow.
|
|
45
|
+
|
|
46
|
+
For a concrete proxy runtime surface, see the LiteLLM Proxy reference
|
|
47
|
+
integration:
|
|
48
|
+
|
|
49
|
+
- [python/reference_integrations/litellm_proxy/README.md](../../reference_integrations/litellm_proxy/README.md)
|
context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Customer support routing
|
|
2
|
+
|
|
3
|
+
Authoritative state changes whether the gateway lets a billing request reach
|
|
4
|
+
the downstream support handler. This example shows that gateway behavior in a
|
|
5
|
+
host-owned customer support routing flow.
|
|
6
|
+
|
|
7
|
+
The gateway calls the downstream support service only after it allows the
|
|
8
|
+
request.
|
|
9
|
+
|
|
10
|
+
## Policy mapping
|
|
11
|
+
|
|
12
|
+
The gateway routes a billing request to `billing_support` only when state
|
|
13
|
+
contains:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
use billing_support
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If state is absent, the gateway blocks billing requests.
|
|
20
|
+
|
|
21
|
+
If state contains:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
prohibit billing_support
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
the gateway also blocks billing requests.
|
|
28
|
+
|
|
29
|
+
Requests that are not asking for billing support follow the host's documented
|
|
30
|
+
default path, `general_support`.
|
|
31
|
+
|
|
32
|
+
## What this example demonstrates
|
|
33
|
+
|
|
34
|
+
- Context Compiler owns authoritative policy state.
|
|
35
|
+
- The host owns the gateway middleware boundary and the downstream call.
|
|
36
|
+
- Adversarial request text does not bypass the gateway decision.
|
|
37
|
+
- Contradictory `use billing_support` and `prohibit billing_support` inputs
|
|
38
|
+
produce clarification behavior instead of a silent overwrite.
|
|
39
|
+
- The example does not call an LLM, does not use directive drafter, and does
|
|
40
|
+
not derive state from model output.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Generic gateway middleware example for customer support routing."""
|