-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathprompts.py
More file actions
291 lines (228 loc) · 9.11 KB
/
prompts.py
File metadata and controls
291 lines (228 loc) · 9.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
"""
Prompt management for PostHog AI SDK.
Fetch and compile LLM prompts from PostHog with caching and fallback support.
"""
import logging
import re
import time
import urllib.parse
from typing import Any, Dict, Optional, Union
from posthog.request import USER_AGENT, _get_session
from posthog.utils import remove_trailing_slash
log = logging.getLogger("posthog")
APP_ENDPOINT = "https://us.posthog.com"
DEFAULT_CACHE_TTL_SECONDS = 300 # 5 minutes
PromptVariables = Dict[str, Union[str, int, float, bool]]
class CachedPrompt:
"""Cached prompt with metadata."""
def __init__(self, prompt: str, fetched_at: float):
self.prompt = prompt
self.fetched_at = fetched_at
def _is_prompt_api_response(data: Any) -> bool:
"""Check if the response is a valid prompt API response."""
return (
isinstance(data, dict)
and "prompt" in data
and isinstance(data.get("prompt"), str)
)
class Prompts:
"""
Fetch and compile LLM prompts from PostHog.
Can be initialized with a PostHog client or with direct options.
Examples:
```python
from posthog import Posthog
from posthog.ai.prompts import Prompts
# With PostHog client
posthog = Posthog('phc_xxx', host='https://us.posthog.com', personal_api_key='phx_xxx')
prompts = Prompts(posthog)
# Or with direct options (no PostHog client needed)
prompts = Prompts(personal_api_key='phx_xxx', host='https://us.posthog.com')
# With error tracking: prompt fetch failures are reported to PostHog
prompts = Prompts(posthog, capture_errors=True)
# Fetch with caching and fallback
template = prompts.get('support-system-prompt', fallback='You are a helpful assistant.')
# Compile with variables
system_prompt = prompts.compile(template, {
'company': 'Acme Corp',
'tier': 'premium',
})
```
"""
def __init__(
self,
posthog: Optional[Any] = None,
*,
personal_api_key: Optional[str] = None,
host: Optional[str] = None,
default_cache_ttl_seconds: Optional[int] = None,
capture_errors: bool = False,
):
"""
Initialize Prompts.
Args:
posthog: PostHog client instance (optional if personal_api_key provided)
personal_api_key: Direct API key (optional if posthog provided)
host: PostHog host (defaults to app endpoint)
default_cache_ttl_seconds: Default cache TTL (defaults to 300)
capture_errors: If True and a PostHog client is provided, prompt fetch
failures are reported to PostHog error tracking via capture_exception().
"""
self._default_cache_ttl_seconds = (
default_cache_ttl_seconds or DEFAULT_CACHE_TTL_SECONDS
)
self._cache: Dict[str, CachedPrompt] = {}
self._client = posthog if posthog is not None else None
self._capture_errors = capture_errors
if posthog is not None:
self._personal_api_key = getattr(posthog, "personal_api_key", None) or ""
self._host = remove_trailing_slash(
getattr(posthog, "raw_host", None) or APP_ENDPOINT
)
else:
self._personal_api_key = personal_api_key or ""
self._host = remove_trailing_slash(host or APP_ENDPOINT)
def get(
self,
name: str,
*,
cache_ttl_seconds: Optional[int] = None,
fallback: Optional[str] = None,
) -> str:
"""
Fetch a prompt by name from the PostHog API.
Caching behavior:
1. If cache is fresh, return cached value
2. If fetch fails and cache exists (stale), return stale cache with warning
3. If fetch fails and fallback provided, return fallback with warning
4. If fetch fails with no cache/fallback, raise exception
Args:
name: The name of the prompt to fetch
cache_ttl_seconds: Cache TTL in seconds (defaults to instance default)
fallback: Fallback prompt to use if fetch fails and no cache available
Returns:
The prompt string
Raises:
Exception: If the prompt cannot be fetched and no fallback is available
"""
ttl = (
cache_ttl_seconds
if cache_ttl_seconds is not None
else self._default_cache_ttl_seconds
)
# Check cache first
cached = self._cache.get(name)
now = time.time()
if cached is not None:
is_fresh = (now - cached.fetched_at) < ttl
if is_fresh:
return cached.prompt
# Try to fetch from API
try:
prompt = self._fetch_prompt_from_api(name)
fetched_at = time.time()
# Update cache
self._cache[name] = CachedPrompt(prompt=prompt, fetched_at=fetched_at)
return prompt
except Exception as error:
self._maybe_capture_error(error)
# Fallback order:
# 1. Return stale cache (with warning)
if cached is not None:
log.warning(
'[PostHog Prompts] Failed to fetch prompt "%s", using stale cache: %s',
name,
error,
)
return cached.prompt
# 2. Return fallback (with warning)
if fallback is not None:
log.warning(
'[PostHog Prompts] Failed to fetch prompt "%s", using fallback: %s',
name,
error,
)
return fallback
# 3. Raise error
raise
def compile(self, prompt: str, variables: PromptVariables) -> str:
"""
Replace {{variableName}} placeholders with values.
Unmatched variables are left unchanged.
Supports variable names with hyphens and dots (e.g., user-id, company.name).
Args:
prompt: The prompt template string
variables: Object containing variable values
Returns:
The compiled prompt string
"""
def replace_variable(match: re.Match) -> str:
variable_name = match.group(1)
if variable_name in variables:
return str(variables[variable_name])
return match.group(0)
return re.sub(r"\{\{([\w.-]+)\}\}", replace_variable, prompt)
def clear_cache(self, name: Optional[str] = None) -> None:
"""
Clear cached prompts.
Args:
name: Specific prompt to clear. If None, clears all cached prompts.
"""
if name is not None:
self._cache.pop(name, None)
else:
self._cache.clear()
def _maybe_capture_error(self, error: Exception) -> None:
"""Report a prompt fetch error to PostHog error tracking if enabled."""
if not self._capture_errors or self._client is None:
return
try:
self._client.capture_exception(error)
except Exception:
log.debug("[PostHog Prompts] Failed to capture exception to error tracking")
def _fetch_prompt_from_api(self, name: str) -> str:
"""
Fetch prompt from PostHog API.
Endpoint: {host}/api/environments/@current/llm_prompts/name/{encoded_name}/
Auth: Bearer {personal_api_key}
Args:
name: The name of the prompt to fetch
Returns:
The prompt string
Raises:
Exception: If the prompt cannot be fetched
"""
if not self._personal_api_key:
raise Exception(
"[PostHog Prompts] personal_api_key is required to fetch prompts. "
"Please provide it when initializing the Prompts instance."
)
encoded_name = urllib.parse.quote(name, safe="")
url = f"{self._host}/api/environments/@current/llm_prompts/name/{encoded_name}/"
headers = {
"Authorization": f"Bearer {self._personal_api_key}",
"User-Agent": USER_AGENT,
}
response = _get_session().get(url, headers=headers, timeout=10)
if not response.ok:
if response.status_code == 404:
raise Exception(f'[PostHog Prompts] Prompt "{name}" not found')
if response.status_code == 403:
raise Exception(
f'[PostHog Prompts] Access denied for prompt "{name}". '
"Check that your personal_api_key has the correct permissions and the LLM prompts feature is enabled."
)
raise Exception(
f'[PostHog Prompts] Failed to fetch prompt "{name}": HTTP {response.status_code}'
)
try:
data = response.json()
except Exception:
raise Exception(
f'[PostHog Prompts] Invalid response format for prompt "{name}"'
)
if not _is_prompt_api_response(data):
raise Exception(
f'[PostHog Prompts] Invalid response format for prompt "{name}"'
)
return data["prompt"]