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.
Files changed (65) hide show
  1. context_compiler_example_integrations/README.md +71 -0
  2. context_compiler_example_integrations/__init__.py +1 -0
  3. context_compiler_example_integrations/examples/__init__.py +12 -0
  4. context_compiler_example_integrations/examples/_shared/__init__.py +1 -0
  5. context_compiler_example_integrations/examples/_shared/litellm_request.py +61 -0
  6. context_compiler_example_integrations/examples/_shared/provider_mode.py +91 -0
  7. context_compiler_example_integrations/examples/checkpoint_continuation/README.md +77 -0
  8. context_compiler_example_integrations/examples/checkpoint_continuation/example.py +169 -0
  9. context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/README.md +85 -0
  10. context_compiler_example_integrations/examples/checkpoint_continuation/fastapi/app.py +205 -0
  11. context_compiler_example_integrations/examples/execution_authorization/README.md +31 -0
  12. context_compiler_example_integrations/examples/execution_authorization/expense_approval/README.md +77 -0
  13. context_compiler_example_integrations/examples/execution_authorization/expense_approval/__init__.py +1 -0
  14. context_compiler_example_integrations/examples/execution_authorization/expense_approval/example.py +168 -0
  15. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/README.md +97 -0
  16. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/__init__.py +1 -0
  17. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/app.py +312 -0
  18. context_compiler_example_integrations/examples/execution_authorization/expense_approval/fastapi/model_approval.py +98 -0
  19. context_compiler_example_integrations/examples/gateway_middleware/README.md +49 -0
  20. context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/README.md +40 -0
  21. context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/__init__.py +1 -0
  22. context_compiler_example_integrations/examples/gateway_middleware/customer_support_routing/example.py +220 -0
  23. context_compiler_example_integrations/examples/prompt_construction/README.md +27 -0
  24. context_compiler_example_integrations/examples/prompt_construction/litellm/README.md +197 -0
  25. context_compiler_example_integrations/examples/prompt_construction/litellm/basic.py +392 -0
  26. context_compiler_example_integrations/examples/prompt_construction/litellm/confirmation_helper.py +91 -0
  27. context_compiler_example_integrations/examples/prompt_construction/litellm/with_directive_drafter.py +524 -0
  28. context_compiler_example_integrations/examples/prompt_construction/writing_assistant/README.md +92 -0
  29. context_compiler_example_integrations/examples/prompt_construction/writing_assistant/__init__.py +1 -0
  30. context_compiler_example_integrations/examples/prompt_construction/writing_assistant/example.py +205 -0
  31. context_compiler_example_integrations/examples/retrieval_filtering/README.md +65 -0
  32. context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/README.md +120 -0
  33. context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/__init__.py +1 -0
  34. context_compiler_example_integrations/examples/retrieval_filtering/chromadb_hr_policy_lookup/example.py +320 -0
  35. context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/README.md +123 -0
  36. context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/__init__.py +1 -0
  37. context_compiler_example_integrations/examples/retrieval_filtering/hr_policy_lookup/example.py +300 -0
  38. context_compiler_example_integrations/examples/schema_selection/README.md +41 -0
  39. context_compiler_example_integrations/examples/schema_selection/litellm_response_format/response_format.py +218 -0
  40. context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/README.md +60 -0
  41. context_compiler_example_integrations/examples/schema_selection/ollama_structured_output/example.py +171 -0
  42. context_compiler_example_integrations/examples/schema_selection/refund_intake/README.md +95 -0
  43. context_compiler_example_integrations/examples/schema_selection/refund_intake/__init__.py +1 -0
  44. context_compiler_example_integrations/examples/schema_selection/refund_intake/example.py +180 -0
  45. context_compiler_example_integrations/examples/tool_gating/README.md +55 -0
  46. context_compiler_example_integrations/examples/tool_gating/calendar_admin/README.md +34 -0
  47. context_compiler_example_integrations/examples/tool_gating/calendar_admin/__init__.py +1 -0
  48. context_compiler_example_integrations/examples/tool_gating/calendar_admin/example.py +195 -0
  49. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/README.md +93 -0
  50. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/__init__.py +1 -0
  51. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/example.py +250 -0
  52. context_compiler_example_integrations/examples/tool_gating/mcp_calendar_admin/live_model.py +369 -0
  53. context_compiler_example_integrations/reference_integrations/__init__.py +12 -0
  54. context_compiler_example_integrations/reference_integrations/litellm_proxy/README.md +251 -0
  55. context_compiler_example_integrations/reference_integrations/litellm_proxy/_checkpoint_support.py +146 -0
  56. context_compiler_example_integrations/reference_integrations/litellm_proxy/config.example.yaml +23 -0
  57. context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook.py +163 -0
  58. context_compiler_example_integrations/reference_integrations/litellm_proxy/context_compiler_precall_hook_with_directive_drafter.py +289 -0
  59. context_compiler_example_integrations/reference_integrations/openwebui_pipe/README.md +233 -0
  60. context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe.py +750 -0
  61. context_compiler_example_integrations/reference_integrations/openwebui_pipe/open_webui_pipe_with_directive_drafter.py +1045 -0
  62. context_compiler_example_integrations-0.1.0.dist-info/METADATA +110 -0
  63. context_compiler_example_integrations-0.1.0.dist-info/RECORD +65 -0
  64. context_compiler_example_integrations-0.1.0.dist-info/WHEEL +4 -0
  65. context_compiler_example_integrations-0.1.0.dist-info/licenses/LICENSE +173 -0
@@ -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
@@ -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()