posthoganalytics 7.6.0__py3-none-any.whl → 7.7.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.
@@ -0,0 +1,363 @@
1
+ """
2
+ Tests for system prompt capture across all LLM providers.
3
+
4
+ This test suite ensures that system prompts are correctly captured in analytics
5
+ regardless of how they're passed to the providers:
6
+ - As first message in messages/contents array (standard format)
7
+ - As separate system parameter (Anthropic, OpenAI)
8
+ - As instructions parameter (OpenAI Responses API)
9
+ - As system_instruction parameter (Gemini)
10
+ """
11
+
12
+ import time
13
+ import unittest
14
+ from unittest.mock import MagicMock, patch
15
+
16
+ from posthoganalytics.client import Client
17
+ from posthoganalytics.test.test_utils import FAKE_TEST_API_KEY
18
+
19
+
20
+ class TestSystemPromptCapture(unittest.TestCase):
21
+ """Test system prompt capture for all providers."""
22
+
23
+ def setUp(self):
24
+ super().setUp()
25
+ self.test_system_prompt = "You are a helpful AI assistant."
26
+ self.test_user_message = "Hello, how are you?"
27
+ self.test_response = "I'm doing well, thank you!"
28
+
29
+ # Create mock PostHog client
30
+ self.client = Client(FAKE_TEST_API_KEY)
31
+ self.client._enqueue = MagicMock()
32
+ self.client.privacy_mode = False
33
+
34
+ def _assert_system_prompt_captured(self, captured_input):
35
+ """Helper to assert system prompt is correctly captured."""
36
+ self.assertEqual(
37
+ len(captured_input), 2, "Should have 2 messages (system + user)"
38
+ )
39
+ self.assertEqual(
40
+ captured_input[0]["role"], "system", "First message should be system"
41
+ )
42
+ self.assertEqual(
43
+ captured_input[0]["content"],
44
+ self.test_system_prompt,
45
+ "System content should match",
46
+ )
47
+ self.assertEqual(
48
+ captured_input[1]["role"], "user", "Second message should be user"
49
+ )
50
+ self.assertEqual(
51
+ captured_input[1]["content"],
52
+ self.test_user_message,
53
+ "User content should match",
54
+ )
55
+
56
+ # OpenAI Tests
57
+ def test_openai_messages_array_system_prompt(self):
58
+ """Test OpenAI with system prompt in messages array."""
59
+ try:
60
+ from openai.types.chat import ChatCompletion, ChatCompletionMessage
61
+ from openai.types.chat.chat_completion import Choice
62
+ from openai.types.completion_usage import CompletionUsage
63
+
64
+ from posthoganalytics.ai.openai import OpenAI
65
+ except ImportError:
66
+ self.skipTest("OpenAI package not available")
67
+
68
+ mock_response = ChatCompletion(
69
+ id="test",
70
+ model="gpt-4",
71
+ object="chat.completion",
72
+ created=int(time.time()),
73
+ choices=[
74
+ Choice(
75
+ finish_reason="stop",
76
+ index=0,
77
+ message=ChatCompletionMessage(
78
+ content=self.test_response, role="assistant"
79
+ ),
80
+ )
81
+ ],
82
+ usage=CompletionUsage(
83
+ completion_tokens=10, prompt_tokens=20, total_tokens=30
84
+ ),
85
+ )
86
+
87
+ with patch(
88
+ "openai.resources.chat.completions.Completions.create",
89
+ return_value=mock_response,
90
+ ):
91
+ client = OpenAI(posthog_client=self.client, api_key="test")
92
+
93
+ messages = [
94
+ {"role": "system", "content": self.test_system_prompt},
95
+ {"role": "user", "content": self.test_user_message},
96
+ ]
97
+
98
+ client.chat.completions.create(
99
+ model="gpt-4", messages=messages, posthog_distinct_id="test-user"
100
+ )
101
+
102
+ self.assertEqual(len(self.client._enqueue.call_args_list), 1)
103
+ properties = self.client._enqueue.call_args_list[0][0][0]["properties"]
104
+ self._assert_system_prompt_captured(properties["$ai_input"])
105
+
106
+ def test_openai_separate_system_parameter(self):
107
+ """Test OpenAI with system prompt as separate parameter."""
108
+ try:
109
+ from openai.types.chat import ChatCompletion, ChatCompletionMessage
110
+ from openai.types.chat.chat_completion import Choice
111
+ from openai.types.completion_usage import CompletionUsage
112
+
113
+ from posthoganalytics.ai.openai import OpenAI
114
+ except ImportError:
115
+ self.skipTest("OpenAI package not available")
116
+
117
+ mock_response = ChatCompletion(
118
+ id="test",
119
+ model="gpt-4",
120
+ object="chat.completion",
121
+ created=int(time.time()),
122
+ choices=[
123
+ Choice(
124
+ finish_reason="stop",
125
+ index=0,
126
+ message=ChatCompletionMessage(
127
+ content=self.test_response, role="assistant"
128
+ ),
129
+ )
130
+ ],
131
+ usage=CompletionUsage(
132
+ completion_tokens=10, prompt_tokens=20, total_tokens=30
133
+ ),
134
+ )
135
+
136
+ with patch(
137
+ "openai.resources.chat.completions.Completions.create",
138
+ return_value=mock_response,
139
+ ):
140
+ client = OpenAI(posthog_client=self.client, api_key="test")
141
+
142
+ messages = [{"role": "user", "content": self.test_user_message}]
143
+
144
+ client.chat.completions.create(
145
+ model="gpt-4",
146
+ messages=messages,
147
+ system=self.test_system_prompt,
148
+ posthog_distinct_id="test-user",
149
+ )
150
+
151
+ self.assertEqual(len(self.client._enqueue.call_args_list), 1)
152
+ properties = self.client._enqueue.call_args_list[0][0][0]["properties"]
153
+ self._assert_system_prompt_captured(properties["$ai_input"])
154
+
155
+ def test_openai_streaming_system_parameter(self):
156
+ """Test OpenAI streaming with system parameter."""
157
+ try:
158
+ from openai.types.chat.chat_completion_chunk import (
159
+ ChatCompletionChunk,
160
+ ChoiceDelta,
161
+ )
162
+ from openai.types.chat.chat_completion_chunk import Choice as ChoiceChunk
163
+ from openai.types.completion_usage import CompletionUsage
164
+
165
+ from posthoganalytics.ai.openai import OpenAI
166
+ except ImportError:
167
+ self.skipTest("OpenAI package not available")
168
+
169
+ chunk1 = ChatCompletionChunk(
170
+ id="test",
171
+ model="gpt-4",
172
+ object="chat.completion.chunk",
173
+ created=int(time.time()),
174
+ choices=[
175
+ ChoiceChunk(
176
+ finish_reason=None,
177
+ index=0,
178
+ delta=ChoiceDelta(content="Hello", role="assistant"),
179
+ )
180
+ ],
181
+ )
182
+
183
+ chunk2 = ChatCompletionChunk(
184
+ id="test",
185
+ model="gpt-4",
186
+ object="chat.completion.chunk",
187
+ created=int(time.time()),
188
+ choices=[
189
+ ChoiceChunk(
190
+ finish_reason="stop",
191
+ index=0,
192
+ delta=ChoiceDelta(content=" there!", role=None),
193
+ )
194
+ ],
195
+ usage=CompletionUsage(
196
+ completion_tokens=10, prompt_tokens=20, total_tokens=30
197
+ ),
198
+ )
199
+
200
+ with patch(
201
+ "openai.resources.chat.completions.Completions.create",
202
+ return_value=[chunk1, chunk2],
203
+ ):
204
+ client = OpenAI(posthog_client=self.client, api_key="test")
205
+
206
+ messages = [{"role": "user", "content": self.test_user_message}]
207
+
208
+ response_generator = client.chat.completions.create(
209
+ model="gpt-4",
210
+ messages=messages,
211
+ system=self.test_system_prompt,
212
+ stream=True,
213
+ posthog_distinct_id="test-user",
214
+ )
215
+
216
+ list(response_generator) # Consume generator
217
+
218
+ self.assertEqual(len(self.client._enqueue.call_args_list), 1)
219
+ properties = self.client._enqueue.call_args_list[0][0][0]["properties"]
220
+ self._assert_system_prompt_captured(properties["$ai_input"])
221
+
222
+ # Anthropic Tests
223
+ def test_anthropic_messages_array_system_prompt(self):
224
+ """Test Anthropic with system prompt in messages array."""
225
+ try:
226
+ from posthoganalytics.ai.anthropic import Anthropic
227
+ except ImportError:
228
+ self.skipTest("Anthropic package not available")
229
+
230
+ with patch("anthropic.resources.messages.Messages.create") as mock_create:
231
+ mock_response = MagicMock()
232
+ mock_response.usage.input_tokens = 20
233
+ mock_response.usage.output_tokens = 10
234
+ mock_response.usage.cache_read_input_tokens = None
235
+ mock_response.usage.cache_creation_input_tokens = None
236
+ mock_create.return_value = mock_response
237
+
238
+ client = Anthropic(posthog_client=self.client, api_key="test")
239
+
240
+ messages = [
241
+ {"role": "system", "content": self.test_system_prompt},
242
+ {"role": "user", "content": self.test_user_message},
243
+ ]
244
+
245
+ client.messages.create(
246
+ model="claude-3-5-sonnet-20241022",
247
+ messages=messages,
248
+ posthog_distinct_id="test-user",
249
+ )
250
+
251
+ self.assertEqual(len(self.client._enqueue.call_args_list), 1)
252
+ properties = self.client._enqueue.call_args_list[0][0][0]["properties"]
253
+ self._assert_system_prompt_captured(properties["$ai_input"])
254
+
255
+ def test_anthropic_separate_system_parameter(self):
256
+ """Test Anthropic with system prompt as separate parameter."""
257
+ try:
258
+ from posthoganalytics.ai.anthropic import Anthropic
259
+ except ImportError:
260
+ self.skipTest("Anthropic package not available")
261
+
262
+ with patch("anthropic.resources.messages.Messages.create") as mock_create:
263
+ mock_response = MagicMock()
264
+ mock_response.usage.input_tokens = 20
265
+ mock_response.usage.output_tokens = 10
266
+ mock_response.usage.cache_read_input_tokens = None
267
+ mock_response.usage.cache_creation_input_tokens = None
268
+ mock_create.return_value = mock_response
269
+
270
+ client = Anthropic(posthog_client=self.client, api_key="test")
271
+
272
+ messages = [{"role": "user", "content": self.test_user_message}]
273
+
274
+ client.messages.create(
275
+ model="claude-3-5-sonnet-20241022",
276
+ messages=messages,
277
+ system=self.test_system_prompt,
278
+ posthog_distinct_id="test-user",
279
+ )
280
+
281
+ self.assertEqual(len(self.client._enqueue.call_args_list), 1)
282
+ properties = self.client._enqueue.call_args_list[0][0][0]["properties"]
283
+ self._assert_system_prompt_captured(properties["$ai_input"])
284
+
285
+ # Gemini Tests
286
+ def test_gemini_contents_array_system_prompt(self):
287
+ """Test Gemini with system prompt in contents array."""
288
+ try:
289
+ from posthoganalytics.ai.gemini import Client
290
+ except ImportError:
291
+ self.skipTest("Gemini package not available")
292
+
293
+ with patch("google.genai.Client") as mock_genai_class:
294
+ mock_response = MagicMock()
295
+ mock_response.candidates = [MagicMock()]
296
+ mock_response.candidates[0].content.parts = [MagicMock()]
297
+ mock_response.candidates[0].content.parts[0].text = self.test_response
298
+ mock_response.usage_metadata.prompt_token_count = 20
299
+ mock_response.usage_metadata.candidates_token_count = 10
300
+ mock_response.usage_metadata.cached_content_token_count = None
301
+ mock_response.usage_metadata.thoughts_token_count = None
302
+
303
+ mock_client_instance = MagicMock()
304
+ mock_models_instance = MagicMock()
305
+ mock_models_instance.generate_content.return_value = mock_response
306
+ mock_client_instance.models = mock_models_instance
307
+ mock_genai_class.return_value = mock_client_instance
308
+
309
+ client = Client(posthog_client=self.client, api_key="test")
310
+
311
+ contents = [
312
+ {"role": "system", "content": self.test_system_prompt},
313
+ {"role": "user", "content": self.test_user_message},
314
+ ]
315
+
316
+ client.models.generate_content(
317
+ model="gemini-2.0-flash",
318
+ contents=contents,
319
+ posthog_distinct_id="test-user",
320
+ )
321
+
322
+ self.assertEqual(len(self.client._enqueue.call_args_list), 1)
323
+ properties = self.client._enqueue.call_args_list[0][0][0]["properties"]
324
+ self._assert_system_prompt_captured(properties["$ai_input"])
325
+
326
+ def test_gemini_system_instruction_parameter(self):
327
+ """Test Gemini with system_instruction in config parameter."""
328
+ try:
329
+ from posthoganalytics.ai.gemini import Client
330
+ except ImportError:
331
+ self.skipTest("Gemini package not available")
332
+
333
+ with patch("google.genai.Client") as mock_genai_class:
334
+ mock_response = MagicMock()
335
+ mock_response.candidates = [MagicMock()]
336
+ mock_response.candidates[0].content.parts = [MagicMock()]
337
+ mock_response.candidates[0].content.parts[0].text = self.test_response
338
+ mock_response.usage_metadata.prompt_token_count = 20
339
+ mock_response.usage_metadata.candidates_token_count = 10
340
+ mock_response.usage_metadata.cached_content_token_count = None
341
+ mock_response.usage_metadata.thoughts_token_count = None
342
+
343
+ mock_client_instance = MagicMock()
344
+ mock_models_instance = MagicMock()
345
+ mock_models_instance.generate_content.return_value = mock_response
346
+ mock_client_instance.models = mock_models_instance
347
+ mock_genai_class.return_value = mock_client_instance
348
+
349
+ client = Client(posthog_client=self.client, api_key="test")
350
+
351
+ contents = [{"role": "user", "content": self.test_user_message}]
352
+ config = {"system_instruction": self.test_system_prompt}
353
+
354
+ client.models.generate_content(
355
+ model="gemini-2.0-flash",
356
+ contents=contents,
357
+ config=config,
358
+ posthog_distinct_id="test-user",
359
+ )
360
+
361
+ self.assertEqual(len(self.client._enqueue.call_args_list), 1)
362
+ properties = self.client._enqueue.call_args_list[0][0][0]["properties"]
363
+ self._assert_system_prompt_captured(properties["$ai_input"])
@@ -1,4 +1,4 @@
1
- VERSION = "7.6.0"
1
+ VERSION = "7.7.0"
2
2
 
3
3
  if __name__ == "__main__":
4
4
  print(VERSION, end="") # noqa: T201
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthoganalytics
3
- Version: 7.6.0
3
+ Version: 7.7.0
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -12,7 +12,7 @@ posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  posthoganalytics/request.py,sha256=sv5dVU4jg4nFI4BpGCGvyoiz6yZZBLMqoBjhmzoomYo,11844
13
13
  posthoganalytics/types.py,sha256=OxGHSmmhVYwA7ecmJXUznDCZ1c4gAGtERzSLSYlyQFM,11540
14
14
  posthoganalytics/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
15
- posthoganalytics/version.py,sha256=tHn5yxWY8mZPPaTizD8PyqyqB_CKu9nT4MytdO7Qkjc,87
15
+ posthoganalytics/version.py,sha256=N71Rzf8IDjCmTh3rPlaceoLNhCzH4fhLpEfObPtcmmM,87
16
16
  posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  posthoganalytics/ai/sanitization.py,sha256=Dpx_5gKZfDS38KjmK1C0lvvjm9N8Pp_oIxusac888-g,6057
18
18
  posthoganalytics/ai/types.py,sha256=arX98hR1PIPeJ3vFikxTlACIh1xPp6aEUw1gBLcKoB0,3273
@@ -33,6 +33,8 @@ posthoganalytics/ai/openai/openai.py,sha256=UTHmlOyy2yOKP3MDbQLV25BH0xe2miF_9z8l
33
33
  posthoganalytics/ai/openai/openai_async.py,sha256=R1vbRrLDQuvZ3v9TOZAEuioeh21XJ_69zevhatIyVto,23709
34
34
  posthoganalytics/ai/openai/openai_converter.py,sha256=2xl1ZkCGiM5BCTu4RPwtRVYZg5lVQNJsxUzFlHfkuIk,25846
35
35
  posthoganalytics/ai/openai/openai_providers.py,sha256=RPVmj2V0_lAdno_ax5Ul2kwhBA9_rRgAdl_sCqrQc6M,4004
36
+ posthoganalytics/ai/openai_agents/__init__.py,sha256=i12Gy9SlB_7Oqwk8lp2-WFjXiy_NlTr5swvE_mCkMRc,2520
37
+ posthoganalytics/ai/openai_agents/processor.py,sha256=gL_PHj6foOi5wbAvW2B6oTQibVGg66a6k8nKVEXlf2o,31497
36
38
  posthoganalytics/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
39
  posthoganalytics/integrations/django.py,sha256=aJ_fLjeMqnnF01Zp8N3c9OeXvWwDL_X_o7aqhlw3e5U,12660
38
40
  posthoganalytics/test/__init__.py,sha256=VYgM6xPbJbvS-xhIcDiBRs0MFC9V_jT65uNeerCz_rM,299
@@ -50,8 +52,13 @@ posthoganalytics/test/test_request.py,sha256=1wdNXbSpTVieYMAzEwicahvk6RfvvTiRdqG
50
52
  posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
51
53
  posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
52
54
  posthoganalytics/test/test_utils.py,sha256=MTz7-Fvffz2a9IRwyKsVy_TnrvIihs-Ap3hhtqGSSAs,9732
53
- posthoganalytics-7.6.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
54
- posthoganalytics-7.6.0.dist-info/METADATA,sha256=QmJ40fgXah6KIEwu-dUHGs6oEseuwjkOjHfvJtzryvc,6368
55
- posthoganalytics-7.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
- posthoganalytics-7.6.0.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
57
- posthoganalytics-7.6.0.dist-info/RECORD,,
55
+ posthoganalytics/test/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ posthoganalytics/test/ai/test_sanitization.py,sha256=Om1f22Z0q5Y4VSaDwPHgW3IYUZPoMj05ZYcTeCmOBlE,18155
57
+ posthoganalytics/test/ai/test_system_prompts.py,sha256=1IQVvcs-YMxPxXOmkbNCFOhvu_NIx4eAanz1XT-QJ3Y,14371
58
+ posthoganalytics/test/ai/openai_agents/__init__.py,sha256=VGLVcRkGkmj0d4MhjcwQ5IYxoaaMPlw0oR7eXSCcGXI,42
59
+ posthoganalytics/test/ai/openai_agents/test_processor.py,sha256=p65z82yiVjMQUR5coaBMMhzV6xB1CezzsvQD1GIi4o0,30800
60
+ posthoganalytics-7.7.0.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
61
+ posthoganalytics-7.7.0.dist-info/METADATA,sha256=gcHpMd9BOdead_UNaQDqa_RrOMz6MTLBkQWr1i0bNZo,6368
62
+ posthoganalytics-7.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
63
+ posthoganalytics-7.7.0.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
64
+ posthoganalytics-7.7.0.dist-info/RECORD,,