posthoganalytics 7.6.0__py3-none-any.whl → 7.8.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.
- posthoganalytics/ai/__init__.py +3 -0
- posthoganalytics/ai/anthropic/anthropic_converter.py +18 -0
- posthoganalytics/ai/gemini/gemini_converter.py +7 -0
- posthoganalytics/ai/openai/openai_converter.py +19 -0
- posthoganalytics/ai/openai_agents/__init__.py +76 -0
- posthoganalytics/ai/openai_agents/processor.py +863 -0
- posthoganalytics/ai/prompts.py +271 -0
- posthoganalytics/ai/types.py +1 -0
- posthoganalytics/ai/utils.py +78 -0
- posthoganalytics/test/ai/__init__.py +0 -0
- posthoganalytics/test/ai/openai_agents/__init__.py +1 -0
- posthoganalytics/test/ai/openai_agents/test_processor.py +810 -0
- posthoganalytics/test/ai/test_prompts.py +577 -0
- posthoganalytics/test/ai/test_sanitization.py +522 -0
- posthoganalytics/test/ai/test_system_prompts.py +363 -0
- posthoganalytics/version.py +1 -1
- {posthoganalytics-7.6.0.dist-info → posthoganalytics-7.8.0.dist-info}/METADATA +1 -1
- {posthoganalytics-7.6.0.dist-info → posthoganalytics-7.8.0.dist-info}/RECORD +21 -12
- {posthoganalytics-7.6.0.dist-info → posthoganalytics-7.8.0.dist-info}/WHEEL +0 -0
- {posthoganalytics-7.6.0.dist-info → posthoganalytics-7.8.0.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-7.6.0.dist-info → posthoganalytics-7.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from posthoganalytics.ai.prompts import Prompts
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MockResponse:
|
|
8
|
+
"""Mock HTTP response for testing."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, json_data=None, status_code=200, ok=True):
|
|
11
|
+
self._json_data = json_data
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
self.ok = ok
|
|
14
|
+
|
|
15
|
+
def json(self):
|
|
16
|
+
if self._json_data is None:
|
|
17
|
+
raise ValueError("No JSON data")
|
|
18
|
+
return self._json_data
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestPrompts(unittest.TestCase):
|
|
22
|
+
"""Tests for the Prompts class."""
|
|
23
|
+
|
|
24
|
+
mock_prompt_response = {
|
|
25
|
+
"id": 1,
|
|
26
|
+
"name": "test-prompt",
|
|
27
|
+
"prompt": "Hello, {{name}}! You are a helpful assistant for {{company}}.",
|
|
28
|
+
"version": 1,
|
|
29
|
+
"created_by": "user@example.com",
|
|
30
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
31
|
+
"updated_at": "2024-01-01T00:00:00Z",
|
|
32
|
+
"deleted": False,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def create_mock_posthog(
|
|
36
|
+
self, personal_api_key="phx_test_key", host="https://us.i.posthog.com"
|
|
37
|
+
):
|
|
38
|
+
"""Create a mock PostHog client."""
|
|
39
|
+
mock = MagicMock()
|
|
40
|
+
mock.personal_api_key = personal_api_key
|
|
41
|
+
mock.raw_host = host
|
|
42
|
+
return mock
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestPromptsGet(TestPrompts):
|
|
46
|
+
"""Tests for the Prompts.get() method."""
|
|
47
|
+
|
|
48
|
+
@patch("posthog.ai.prompts._get_session")
|
|
49
|
+
def test_successfully_fetch_a_prompt(self, mock_get_session):
|
|
50
|
+
"""Should successfully fetch a prompt."""
|
|
51
|
+
mock_get = mock_get_session.return_value.get
|
|
52
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
53
|
+
|
|
54
|
+
posthog = self.create_mock_posthog()
|
|
55
|
+
prompts = Prompts(posthog)
|
|
56
|
+
|
|
57
|
+
result = prompts.get("test-prompt")
|
|
58
|
+
|
|
59
|
+
self.assertEqual(result, self.mock_prompt_response["prompt"])
|
|
60
|
+
mock_get.assert_called_once()
|
|
61
|
+
call_args = mock_get.call_args
|
|
62
|
+
self.assertEqual(
|
|
63
|
+
call_args[0][0],
|
|
64
|
+
"https://us.i.posthog.com/api/projects/@current/llm_prompts/name/test-prompt/",
|
|
65
|
+
)
|
|
66
|
+
self.assertIn("Authorization", call_args[1]["headers"])
|
|
67
|
+
self.assertEqual(
|
|
68
|
+
call_args[1]["headers"]["Authorization"], "Bearer phx_test_key"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@patch("posthog.ai.prompts._get_session")
|
|
72
|
+
@patch("posthog.ai.prompts.time.time")
|
|
73
|
+
def test_return_cached_prompt_when_fresh(self, mock_time, mock_get_session):
|
|
74
|
+
"""Should return cached prompt when fresh (no API call)."""
|
|
75
|
+
mock_get = mock_get_session.return_value.get
|
|
76
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
77
|
+
mock_time.return_value = 1000.0
|
|
78
|
+
|
|
79
|
+
posthog = self.create_mock_posthog()
|
|
80
|
+
prompts = Prompts(posthog)
|
|
81
|
+
|
|
82
|
+
# First call - fetches from API
|
|
83
|
+
result1 = prompts.get("test-prompt", cache_ttl_seconds=300)
|
|
84
|
+
self.assertEqual(result1, self.mock_prompt_response["prompt"])
|
|
85
|
+
self.assertEqual(mock_get.call_count, 1)
|
|
86
|
+
|
|
87
|
+
# Advance time by 60 seconds (still within TTL)
|
|
88
|
+
mock_time.return_value = 1060.0
|
|
89
|
+
|
|
90
|
+
# Second call - should use cache
|
|
91
|
+
result2 = prompts.get("test-prompt", cache_ttl_seconds=300)
|
|
92
|
+
self.assertEqual(result2, self.mock_prompt_response["prompt"])
|
|
93
|
+
self.assertEqual(mock_get.call_count, 1) # No additional fetch
|
|
94
|
+
|
|
95
|
+
@patch("posthog.ai.prompts._get_session")
|
|
96
|
+
@patch("posthog.ai.prompts.time.time")
|
|
97
|
+
def test_refetch_when_cache_is_stale(self, mock_time, mock_get_session):
|
|
98
|
+
"""Should refetch when cache is stale."""
|
|
99
|
+
mock_get = mock_get_session.return_value.get
|
|
100
|
+
updated_prompt_response = {
|
|
101
|
+
**self.mock_prompt_response,
|
|
102
|
+
"prompt": "Updated prompt: Hello, {{name}}!",
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
mock_get.side_effect = [
|
|
106
|
+
MockResponse(json_data=self.mock_prompt_response),
|
|
107
|
+
MockResponse(json_data=updated_prompt_response),
|
|
108
|
+
]
|
|
109
|
+
mock_time.return_value = 1000.0
|
|
110
|
+
|
|
111
|
+
posthog = self.create_mock_posthog()
|
|
112
|
+
prompts = Prompts(posthog)
|
|
113
|
+
|
|
114
|
+
# First call - fetches from API
|
|
115
|
+
result1 = prompts.get("test-prompt", cache_ttl_seconds=60)
|
|
116
|
+
self.assertEqual(result1, self.mock_prompt_response["prompt"])
|
|
117
|
+
self.assertEqual(mock_get.call_count, 1)
|
|
118
|
+
|
|
119
|
+
# Advance time past TTL
|
|
120
|
+
mock_time.return_value = 1061.0
|
|
121
|
+
|
|
122
|
+
# Second call - should refetch
|
|
123
|
+
result2 = prompts.get("test-prompt", cache_ttl_seconds=60)
|
|
124
|
+
self.assertEqual(result2, updated_prompt_response["prompt"])
|
|
125
|
+
self.assertEqual(mock_get.call_count, 2)
|
|
126
|
+
|
|
127
|
+
@patch("posthog.ai.prompts._get_session")
|
|
128
|
+
@patch("posthog.ai.prompts.time.time")
|
|
129
|
+
@patch("posthog.ai.prompts.log")
|
|
130
|
+
def test_use_stale_cache_on_fetch_failure_with_warning(
|
|
131
|
+
self, mock_log, mock_time, mock_get_session
|
|
132
|
+
):
|
|
133
|
+
"""Should use stale cache on fetch failure with warning."""
|
|
134
|
+
mock_get = mock_get_session.return_value.get
|
|
135
|
+
mock_get.side_effect = [
|
|
136
|
+
MockResponse(json_data=self.mock_prompt_response),
|
|
137
|
+
Exception("Network error"),
|
|
138
|
+
]
|
|
139
|
+
mock_time.return_value = 1000.0
|
|
140
|
+
|
|
141
|
+
posthog = self.create_mock_posthog()
|
|
142
|
+
prompts = Prompts(posthog)
|
|
143
|
+
|
|
144
|
+
# First call - populates cache
|
|
145
|
+
result1 = prompts.get("test-prompt", cache_ttl_seconds=60)
|
|
146
|
+
self.assertEqual(result1, self.mock_prompt_response["prompt"])
|
|
147
|
+
|
|
148
|
+
# Advance time past TTL
|
|
149
|
+
mock_time.return_value = 1061.0
|
|
150
|
+
|
|
151
|
+
# Second call - should use stale cache
|
|
152
|
+
result2 = prompts.get("test-prompt", cache_ttl_seconds=60)
|
|
153
|
+
self.assertEqual(result2, self.mock_prompt_response["prompt"])
|
|
154
|
+
|
|
155
|
+
# Check warning was logged
|
|
156
|
+
mock_log.warning.assert_called()
|
|
157
|
+
warning_call = mock_log.warning.call_args
|
|
158
|
+
self.assertIn("using stale cache", warning_call[0][0])
|
|
159
|
+
|
|
160
|
+
@patch("posthog.ai.prompts._get_session")
|
|
161
|
+
@patch("posthog.ai.prompts.log")
|
|
162
|
+
def test_use_fallback_when_no_cache_and_fetch_fails_with_warning(
|
|
163
|
+
self, mock_log, mock_get_session
|
|
164
|
+
):
|
|
165
|
+
"""Should use fallback when no cache and fetch fails with warning."""
|
|
166
|
+
mock_get = mock_get_session.return_value.get
|
|
167
|
+
mock_get.side_effect = Exception("Network error")
|
|
168
|
+
|
|
169
|
+
posthog = self.create_mock_posthog()
|
|
170
|
+
prompts = Prompts(posthog)
|
|
171
|
+
|
|
172
|
+
fallback = "Default system prompt."
|
|
173
|
+
result = prompts.get("test-prompt", fallback=fallback)
|
|
174
|
+
|
|
175
|
+
self.assertEqual(result, fallback)
|
|
176
|
+
|
|
177
|
+
# Check warning was logged
|
|
178
|
+
mock_log.warning.assert_called()
|
|
179
|
+
warning_call = mock_log.warning.call_args
|
|
180
|
+
self.assertIn("using fallback", warning_call[0][0])
|
|
181
|
+
|
|
182
|
+
@patch("posthog.ai.prompts._get_session")
|
|
183
|
+
def test_throw_when_no_cache_no_fallback_and_fetch_fails(self, mock_get_session):
|
|
184
|
+
"""Should throw when no cache, no fallback, and fetch fails."""
|
|
185
|
+
mock_get = mock_get_session.return_value.get
|
|
186
|
+
mock_get.side_effect = Exception("Network error")
|
|
187
|
+
|
|
188
|
+
posthog = self.create_mock_posthog()
|
|
189
|
+
prompts = Prompts(posthog)
|
|
190
|
+
|
|
191
|
+
with self.assertRaises(Exception) as context:
|
|
192
|
+
prompts.get("test-prompt")
|
|
193
|
+
|
|
194
|
+
self.assertIn("Network error", str(context.exception))
|
|
195
|
+
|
|
196
|
+
@patch("posthog.ai.prompts._get_session")
|
|
197
|
+
def test_handle_404_response(self, mock_get_session):
|
|
198
|
+
"""Should handle 404 response."""
|
|
199
|
+
mock_get = mock_get_session.return_value.get
|
|
200
|
+
mock_get.return_value = MockResponse(status_code=404, ok=False)
|
|
201
|
+
|
|
202
|
+
posthog = self.create_mock_posthog()
|
|
203
|
+
prompts = Prompts(posthog)
|
|
204
|
+
|
|
205
|
+
with self.assertRaises(Exception) as context:
|
|
206
|
+
prompts.get("nonexistent-prompt")
|
|
207
|
+
|
|
208
|
+
self.assertIn('Prompt "nonexistent-prompt" not found', str(context.exception))
|
|
209
|
+
|
|
210
|
+
@patch("posthog.ai.prompts._get_session")
|
|
211
|
+
def test_handle_403_response(self, mock_get_session):
|
|
212
|
+
"""Should handle 403 response."""
|
|
213
|
+
mock_get = mock_get_session.return_value.get
|
|
214
|
+
mock_get.return_value = MockResponse(status_code=403, ok=False)
|
|
215
|
+
|
|
216
|
+
posthog = self.create_mock_posthog()
|
|
217
|
+
prompts = Prompts(posthog)
|
|
218
|
+
|
|
219
|
+
with self.assertRaises(Exception) as context:
|
|
220
|
+
prompts.get("restricted-prompt")
|
|
221
|
+
|
|
222
|
+
self.assertIn(
|
|
223
|
+
'Access denied for prompt "restricted-prompt"', str(context.exception)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def test_throw_when_no_personal_api_key_configured(self):
|
|
227
|
+
"""Should throw when no personal_api_key is configured."""
|
|
228
|
+
posthog = self.create_mock_posthog(personal_api_key=None)
|
|
229
|
+
prompts = Prompts(posthog)
|
|
230
|
+
|
|
231
|
+
with self.assertRaises(Exception) as context:
|
|
232
|
+
prompts.get("test-prompt")
|
|
233
|
+
|
|
234
|
+
self.assertIn(
|
|
235
|
+
"personal_api_key is required to fetch prompts", str(context.exception)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
@patch("posthog.ai.prompts._get_session")
|
|
239
|
+
def test_throw_when_api_returns_invalid_response_format(self, mock_get_session):
|
|
240
|
+
"""Should throw when API returns invalid response format."""
|
|
241
|
+
mock_get = mock_get_session.return_value.get
|
|
242
|
+
mock_get.return_value = MockResponse(json_data={"invalid": "response"})
|
|
243
|
+
|
|
244
|
+
posthog = self.create_mock_posthog()
|
|
245
|
+
prompts = Prompts(posthog)
|
|
246
|
+
|
|
247
|
+
with self.assertRaises(Exception) as context:
|
|
248
|
+
prompts.get("test-prompt")
|
|
249
|
+
|
|
250
|
+
self.assertIn("Invalid response format", str(context.exception))
|
|
251
|
+
|
|
252
|
+
@patch("posthog.ai.prompts._get_session")
|
|
253
|
+
def test_use_custom_host_from_posthog_options(self, mock_get_session):
|
|
254
|
+
"""Should use custom host from PostHog options."""
|
|
255
|
+
mock_get = mock_get_session.return_value.get
|
|
256
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
257
|
+
|
|
258
|
+
posthog = self.create_mock_posthog(host="https://eu.i.posthog.com")
|
|
259
|
+
prompts = Prompts(posthog)
|
|
260
|
+
|
|
261
|
+
prompts.get("test-prompt")
|
|
262
|
+
|
|
263
|
+
call_args = mock_get.call_args
|
|
264
|
+
self.assertTrue(
|
|
265
|
+
call_args[0][0].startswith("https://eu.i.posthog.com/"),
|
|
266
|
+
f"Expected URL to start with 'https://eu.i.posthog.com/', got {call_args[0][0]}",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
@patch("posthog.ai.prompts._get_session")
|
|
270
|
+
@patch("posthog.ai.prompts.time.time")
|
|
271
|
+
def test_use_default_cache_ttl_5_minutes(self, mock_time, mock_get_session):
|
|
272
|
+
"""Should use default cache TTL (5 minutes) when not specified."""
|
|
273
|
+
mock_get = mock_get_session.return_value.get
|
|
274
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
275
|
+
mock_time.return_value = 1000.0
|
|
276
|
+
|
|
277
|
+
posthog = self.create_mock_posthog()
|
|
278
|
+
prompts = Prompts(posthog)
|
|
279
|
+
|
|
280
|
+
# First call
|
|
281
|
+
prompts.get("test-prompt")
|
|
282
|
+
self.assertEqual(mock_get.call_count, 1)
|
|
283
|
+
|
|
284
|
+
# Advance time by 4 minutes (within default 5-minute TTL)
|
|
285
|
+
mock_time.return_value = 1000.0 + (4 * 60)
|
|
286
|
+
|
|
287
|
+
# Second call - should use cache
|
|
288
|
+
prompts.get("test-prompt")
|
|
289
|
+
self.assertEqual(mock_get.call_count, 1)
|
|
290
|
+
|
|
291
|
+
# Advance time past 5-minute TTL
|
|
292
|
+
mock_time.return_value = 1000.0 + (6 * 60)
|
|
293
|
+
|
|
294
|
+
# Third call - should refetch
|
|
295
|
+
prompts.get("test-prompt")
|
|
296
|
+
self.assertEqual(mock_get.call_count, 2)
|
|
297
|
+
|
|
298
|
+
@patch("posthog.ai.prompts._get_session")
|
|
299
|
+
@patch("posthog.ai.prompts.time.time")
|
|
300
|
+
def test_use_custom_default_cache_ttl_from_constructor(
|
|
301
|
+
self, mock_time, mock_get_session
|
|
302
|
+
):
|
|
303
|
+
"""Should use custom default cache TTL from constructor."""
|
|
304
|
+
mock_get = mock_get_session.return_value.get
|
|
305
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
306
|
+
mock_time.return_value = 1000.0
|
|
307
|
+
|
|
308
|
+
posthog = self.create_mock_posthog()
|
|
309
|
+
prompts = Prompts(posthog, default_cache_ttl_seconds=60)
|
|
310
|
+
|
|
311
|
+
# First call
|
|
312
|
+
prompts.get("test-prompt")
|
|
313
|
+
self.assertEqual(mock_get.call_count, 1)
|
|
314
|
+
|
|
315
|
+
# Advance time past custom TTL
|
|
316
|
+
mock_time.return_value = 1061.0
|
|
317
|
+
|
|
318
|
+
# Second call - should refetch
|
|
319
|
+
prompts.get("test-prompt")
|
|
320
|
+
self.assertEqual(mock_get.call_count, 2)
|
|
321
|
+
|
|
322
|
+
@patch("posthog.ai.prompts._get_session")
|
|
323
|
+
def test_url_encode_prompt_names_with_special_characters(self, mock_get_session):
|
|
324
|
+
"""Should URL-encode prompt names with special characters."""
|
|
325
|
+
mock_get = mock_get_session.return_value.get
|
|
326
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
327
|
+
|
|
328
|
+
posthog = self.create_mock_posthog()
|
|
329
|
+
prompts = Prompts(posthog)
|
|
330
|
+
|
|
331
|
+
prompts.get("prompt with spaces/and/slashes")
|
|
332
|
+
|
|
333
|
+
call_args = mock_get.call_args
|
|
334
|
+
self.assertEqual(
|
|
335
|
+
call_args[0][0],
|
|
336
|
+
"https://us.i.posthog.com/api/projects/@current/llm_prompts/name/prompt%20with%20spaces%2Fand%2Fslashes/",
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
@patch("posthog.ai.prompts._get_session")
|
|
340
|
+
def test_work_with_direct_options_no_posthog_client(self, mock_get_session):
|
|
341
|
+
"""Should work with direct options (no PostHog client)."""
|
|
342
|
+
mock_get = mock_get_session.return_value.get
|
|
343
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
344
|
+
|
|
345
|
+
prompts = Prompts(personal_api_key="phx_direct_key")
|
|
346
|
+
|
|
347
|
+
result = prompts.get("test-prompt")
|
|
348
|
+
|
|
349
|
+
self.assertEqual(result, self.mock_prompt_response["prompt"])
|
|
350
|
+
call_args = mock_get.call_args
|
|
351
|
+
self.assertEqual(
|
|
352
|
+
call_args[0][0],
|
|
353
|
+
"https://us.i.posthog.com/api/projects/@current/llm_prompts/name/test-prompt/",
|
|
354
|
+
)
|
|
355
|
+
self.assertEqual(
|
|
356
|
+
call_args[1]["headers"]["Authorization"], "Bearer phx_direct_key"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
@patch("posthog.ai.prompts._get_session")
|
|
360
|
+
def test_use_custom_host_from_direct_options(self, mock_get_session):
|
|
361
|
+
"""Should use custom host from direct options."""
|
|
362
|
+
mock_get = mock_get_session.return_value.get
|
|
363
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
364
|
+
|
|
365
|
+
prompts = Prompts(
|
|
366
|
+
personal_api_key="phx_direct_key", host="https://eu.i.posthog.com"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
prompts.get("test-prompt")
|
|
370
|
+
|
|
371
|
+
call_args = mock_get.call_args
|
|
372
|
+
self.assertEqual(
|
|
373
|
+
call_args[0][0],
|
|
374
|
+
"https://eu.i.posthog.com/api/projects/@current/llm_prompts/name/test-prompt/",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
@patch("posthog.ai.prompts._get_session")
|
|
378
|
+
@patch("posthog.ai.prompts.time.time")
|
|
379
|
+
def test_use_custom_default_cache_ttl_from_direct_options(
|
|
380
|
+
self, mock_time, mock_get_session
|
|
381
|
+
):
|
|
382
|
+
"""Should use custom default cache TTL from direct options."""
|
|
383
|
+
mock_get = mock_get_session.return_value.get
|
|
384
|
+
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
|
|
385
|
+
mock_time.return_value = 1000.0
|
|
386
|
+
|
|
387
|
+
prompts = Prompts(
|
|
388
|
+
personal_api_key="phx_direct_key", default_cache_ttl_seconds=60
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# First call
|
|
392
|
+
prompts.get("test-prompt")
|
|
393
|
+
self.assertEqual(mock_get.call_count, 1)
|
|
394
|
+
|
|
395
|
+
# Advance time past custom TTL
|
|
396
|
+
mock_time.return_value = 1061.0
|
|
397
|
+
|
|
398
|
+
# Second call - should refetch
|
|
399
|
+
prompts.get("test-prompt")
|
|
400
|
+
self.assertEqual(mock_get.call_count, 2)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestPromptsCompile(TestPrompts):
|
|
404
|
+
"""Tests for the Prompts.compile() method."""
|
|
405
|
+
|
|
406
|
+
def test_replace_a_single_variable(self):
|
|
407
|
+
"""Should replace a single variable."""
|
|
408
|
+
posthog = self.create_mock_posthog()
|
|
409
|
+
prompts = Prompts(posthog)
|
|
410
|
+
|
|
411
|
+
result = prompts.compile("Hello, {{name}}!", {"name": "World"})
|
|
412
|
+
|
|
413
|
+
self.assertEqual(result, "Hello, World!")
|
|
414
|
+
|
|
415
|
+
def test_replace_multiple_variables(self):
|
|
416
|
+
"""Should replace multiple variables."""
|
|
417
|
+
posthog = self.create_mock_posthog()
|
|
418
|
+
prompts = Prompts(posthog)
|
|
419
|
+
|
|
420
|
+
result = prompts.compile(
|
|
421
|
+
"Hello, {{name}}! Welcome to {{company}}. Your tier is {{tier}}.",
|
|
422
|
+
{"name": "John", "company": "Acme Corp", "tier": "premium"},
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
self.assertEqual(
|
|
426
|
+
result, "Hello, John! Welcome to Acme Corp. Your tier is premium."
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def test_handle_numbers(self):
|
|
430
|
+
"""Should handle numbers."""
|
|
431
|
+
posthog = self.create_mock_posthog()
|
|
432
|
+
prompts = Prompts(posthog)
|
|
433
|
+
|
|
434
|
+
result = prompts.compile("You have {{count}} items.", {"count": 42})
|
|
435
|
+
|
|
436
|
+
self.assertEqual(result, "You have 42 items.")
|
|
437
|
+
|
|
438
|
+
def test_handle_booleans(self):
|
|
439
|
+
"""Should handle booleans."""
|
|
440
|
+
posthog = self.create_mock_posthog()
|
|
441
|
+
prompts = Prompts(posthog)
|
|
442
|
+
|
|
443
|
+
result = prompts.compile("Feature enabled: {{enabled}}", {"enabled": True})
|
|
444
|
+
|
|
445
|
+
self.assertEqual(result, "Feature enabled: True")
|
|
446
|
+
|
|
447
|
+
def test_leave_unmatched_variables_unchanged(self):
|
|
448
|
+
"""Should leave unmatched variables unchanged."""
|
|
449
|
+
posthog = self.create_mock_posthog()
|
|
450
|
+
prompts = Prompts(posthog)
|
|
451
|
+
|
|
452
|
+
result = prompts.compile(
|
|
453
|
+
"Hello, {{name}}! Your {{unknown}} is ready.", {"name": "World"}
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
self.assertEqual(result, "Hello, World! Your {{unknown}} is ready.")
|
|
457
|
+
|
|
458
|
+
def test_handle_prompts_with_no_variables(self):
|
|
459
|
+
"""Should handle prompts with no variables."""
|
|
460
|
+
posthog = self.create_mock_posthog()
|
|
461
|
+
prompts = Prompts(posthog)
|
|
462
|
+
|
|
463
|
+
result = prompts.compile("You are a helpful assistant.", {})
|
|
464
|
+
|
|
465
|
+
self.assertEqual(result, "You are a helpful assistant.")
|
|
466
|
+
|
|
467
|
+
def test_handle_empty_variables_dict(self):
|
|
468
|
+
"""Should handle empty variables dict."""
|
|
469
|
+
posthog = self.create_mock_posthog()
|
|
470
|
+
prompts = Prompts(posthog)
|
|
471
|
+
|
|
472
|
+
result = prompts.compile("Hello, {{name}}!", {})
|
|
473
|
+
|
|
474
|
+
self.assertEqual(result, "Hello, {{name}}!")
|
|
475
|
+
|
|
476
|
+
def test_handle_multiple_occurrences_of_same_variable(self):
|
|
477
|
+
"""Should handle multiple occurrences of the same variable."""
|
|
478
|
+
posthog = self.create_mock_posthog()
|
|
479
|
+
prompts = Prompts(posthog)
|
|
480
|
+
|
|
481
|
+
result = prompts.compile(
|
|
482
|
+
"Hello, {{name}}! Goodbye, {{name}}!", {"name": "World"}
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
self.assertEqual(result, "Hello, World! Goodbye, World!")
|
|
486
|
+
|
|
487
|
+
def test_work_with_direct_options_initialization(self):
|
|
488
|
+
"""Should work with direct options initialization."""
|
|
489
|
+
prompts = Prompts(personal_api_key="phx_test_key")
|
|
490
|
+
|
|
491
|
+
result = prompts.compile("Hello, {{name}}!", {"name": "World"})
|
|
492
|
+
|
|
493
|
+
self.assertEqual(result, "Hello, World!")
|
|
494
|
+
|
|
495
|
+
def test_handle_variables_with_hyphens(self):
|
|
496
|
+
"""Should handle variables with hyphens."""
|
|
497
|
+
prompts = Prompts(personal_api_key="phx_test_key")
|
|
498
|
+
|
|
499
|
+
result = prompts.compile("User ID: {{user-id}}", {"user-id": "12345"})
|
|
500
|
+
|
|
501
|
+
self.assertEqual(result, "User ID: 12345")
|
|
502
|
+
|
|
503
|
+
def test_handle_variables_with_dots(self):
|
|
504
|
+
"""Should handle variables with dots."""
|
|
505
|
+
prompts = Prompts(personal_api_key="phx_test_key")
|
|
506
|
+
|
|
507
|
+
result = prompts.compile("Company: {{company.name}}", {"company.name": "Acme"})
|
|
508
|
+
|
|
509
|
+
self.assertEqual(result, "Company: Acme")
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
class TestPromptsClearCache(TestPrompts):
|
|
513
|
+
"""Tests for the Prompts.clear_cache() method."""
|
|
514
|
+
|
|
515
|
+
@patch("posthog.ai.prompts._get_session")
|
|
516
|
+
def test_clear_a_specific_prompt_from_cache(self, mock_get_session):
|
|
517
|
+
"""Should clear a specific prompt from cache."""
|
|
518
|
+
mock_get = mock_get_session.return_value.get
|
|
519
|
+
other_prompt_response = {**self.mock_prompt_response, "name": "other-prompt"}
|
|
520
|
+
|
|
521
|
+
mock_get.side_effect = [
|
|
522
|
+
MockResponse(json_data=self.mock_prompt_response),
|
|
523
|
+
MockResponse(json_data=other_prompt_response),
|
|
524
|
+
MockResponse(json_data=self.mock_prompt_response),
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
posthog = self.create_mock_posthog()
|
|
528
|
+
prompts = Prompts(posthog)
|
|
529
|
+
|
|
530
|
+
# Populate cache with two prompts
|
|
531
|
+
prompts.get("test-prompt")
|
|
532
|
+
prompts.get("other-prompt")
|
|
533
|
+
self.assertEqual(mock_get.call_count, 2)
|
|
534
|
+
|
|
535
|
+
# Clear only test-prompt
|
|
536
|
+
prompts.clear_cache("test-prompt")
|
|
537
|
+
|
|
538
|
+
# test-prompt should be refetched
|
|
539
|
+
prompts.get("test-prompt")
|
|
540
|
+
self.assertEqual(mock_get.call_count, 3)
|
|
541
|
+
|
|
542
|
+
# other-prompt should still be cached
|
|
543
|
+
prompts.get("other-prompt")
|
|
544
|
+
self.assertEqual(mock_get.call_count, 3)
|
|
545
|
+
|
|
546
|
+
@patch("posthog.ai.prompts._get_session")
|
|
547
|
+
def test_clear_all_prompts_from_cache(self, mock_get_session):
|
|
548
|
+
"""Should clear all prompts from cache when no name is provided."""
|
|
549
|
+
mock_get = mock_get_session.return_value.get
|
|
550
|
+
other_prompt_response = {**self.mock_prompt_response, "name": "other-prompt"}
|
|
551
|
+
|
|
552
|
+
mock_get.side_effect = [
|
|
553
|
+
MockResponse(json_data=self.mock_prompt_response),
|
|
554
|
+
MockResponse(json_data=other_prompt_response),
|
|
555
|
+
MockResponse(json_data=self.mock_prompt_response),
|
|
556
|
+
MockResponse(json_data=other_prompt_response),
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
posthog = self.create_mock_posthog()
|
|
560
|
+
prompts = Prompts(posthog)
|
|
561
|
+
|
|
562
|
+
# Populate cache with two prompts
|
|
563
|
+
prompts.get("test-prompt")
|
|
564
|
+
prompts.get("other-prompt")
|
|
565
|
+
self.assertEqual(mock_get.call_count, 2)
|
|
566
|
+
|
|
567
|
+
# Clear all cache
|
|
568
|
+
prompts.clear_cache()
|
|
569
|
+
|
|
570
|
+
# Both prompts should be refetched
|
|
571
|
+
prompts.get("test-prompt")
|
|
572
|
+
prompts.get("other-prompt")
|
|
573
|
+
self.assertEqual(mock_get.call_count, 4)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
if __name__ == "__main__":
|
|
577
|
+
unittest.main()
|