posthoganalytics 6.7.5__py3-none-any.whl → 7.4.3__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 (37) hide show
  1. posthoganalytics/__init__.py +84 -7
  2. posthoganalytics/ai/anthropic/anthropic_async.py +30 -67
  3. posthoganalytics/ai/anthropic/anthropic_converter.py +40 -0
  4. posthoganalytics/ai/gemini/__init__.py +3 -0
  5. posthoganalytics/ai/gemini/gemini.py +1 -1
  6. posthoganalytics/ai/gemini/gemini_async.py +423 -0
  7. posthoganalytics/ai/gemini/gemini_converter.py +160 -24
  8. posthoganalytics/ai/langchain/callbacks.py +55 -11
  9. posthoganalytics/ai/openai/openai.py +27 -2
  10. posthoganalytics/ai/openai/openai_async.py +49 -5
  11. posthoganalytics/ai/openai/openai_converter.py +130 -0
  12. posthoganalytics/ai/sanitization.py +27 -5
  13. posthoganalytics/ai/types.py +1 -0
  14. posthoganalytics/ai/utils.py +32 -2
  15. posthoganalytics/client.py +338 -90
  16. posthoganalytics/contexts.py +81 -0
  17. posthoganalytics/exception_utils.py +250 -2
  18. posthoganalytics/feature_flags.py +26 -10
  19. posthoganalytics/flag_definition_cache.py +127 -0
  20. posthoganalytics/integrations/django.py +149 -50
  21. posthoganalytics/request.py +203 -23
  22. posthoganalytics/test/test_client.py +250 -22
  23. posthoganalytics/test/test_exception_capture.py +418 -0
  24. posthoganalytics/test/test_feature_flag_result.py +441 -2
  25. posthoganalytics/test/test_feature_flags.py +306 -102
  26. posthoganalytics/test/test_flag_definition_cache.py +612 -0
  27. posthoganalytics/test/test_module.py +0 -8
  28. posthoganalytics/test/test_request.py +536 -0
  29. posthoganalytics/test/test_utils.py +4 -1
  30. posthoganalytics/types.py +40 -0
  31. posthoganalytics/version.py +1 -1
  32. {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/METADATA +12 -12
  33. posthoganalytics-7.4.3.dist-info/RECORD +57 -0
  34. posthoganalytics-6.7.5.dist-info/RECORD +0 -54
  35. {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/WHEEL +0 -0
  36. {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/licenses/LICENSE +0 -0
  37. {posthoganalytics-6.7.5.dist-info → posthoganalytics-7.4.3.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,61 @@
1
1
  import datetime # noqa: F401
2
- from typing import Callable, Dict, Optional, Any # noqa: F401
2
+ from typing import Any, Callable, Dict, Optional # noqa: F401
3
+
3
4
  from typing_extensions import Unpack
4
5
 
5
- from posthoganalytics.args import OptionalCaptureArgs, OptionalSetArgs, ExceptionArg
6
+ from posthoganalytics.args import ExceptionArg, OptionalCaptureArgs, OptionalSetArgs
6
7
  from posthoganalytics.client import Client
8
+ from posthoganalytics.contexts import (
9
+ identify_context as inner_identify_context,
10
+ )
7
11
  from posthoganalytics.contexts import (
8
12
  new_context as inner_new_context,
13
+ )
14
+ from posthoganalytics.contexts import (
9
15
  scoped as inner_scoped,
10
- tag as inner_tag,
16
+ )
17
+ from posthoganalytics.contexts import (
18
+ set_capture_exception_code_variables_context as inner_set_capture_exception_code_variables_context,
19
+ )
20
+ from posthoganalytics.contexts import (
21
+ set_code_variables_ignore_patterns_context as inner_set_code_variables_ignore_patterns_context,
22
+ )
23
+ from posthoganalytics.contexts import (
24
+ set_code_variables_mask_patterns_context as inner_set_code_variables_mask_patterns_context,
25
+ )
26
+ from posthoganalytics.contexts import (
11
27
  set_context_session as inner_set_context_session,
12
- identify_context as inner_identify_context,
13
28
  )
14
- from posthoganalytics.types import FeatureFlag, FlagsAndPayloads, FeatureFlagResult
29
+ from posthoganalytics.contexts import (
30
+ tag as inner_tag,
31
+ )
32
+ from posthoganalytics.exception_utils import (
33
+ DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS,
34
+ DEFAULT_CODE_VARIABLES_MASK_PATTERNS,
35
+ )
36
+ from posthoganalytics.feature_flags import (
37
+ InconclusiveMatchError as InconclusiveMatchError,
38
+ )
39
+ from posthoganalytics.feature_flags import (
40
+ RequiresServerEvaluation as RequiresServerEvaluation,
41
+ )
42
+ from posthoganalytics.flag_definition_cache import (
43
+ FlagDefinitionCacheData as FlagDefinitionCacheData,
44
+ FlagDefinitionCacheProvider as FlagDefinitionCacheProvider,
45
+ )
46
+ from posthoganalytics.request import (
47
+ disable_connection_reuse as disable_connection_reuse,
48
+ enable_keep_alive as enable_keep_alive,
49
+ set_socket_options as set_socket_options,
50
+ SocketOptions as SocketOptions,
51
+ )
52
+ from posthoganalytics.types import (
53
+ FeatureFlag,
54
+ FlagsAndPayloads,
55
+ )
56
+ from posthoganalytics.types import (
57
+ FeatureFlagResult as FeatureFlagResult,
58
+ )
15
59
  from posthoganalytics.version import VERSION
16
60
 
17
61
  __version__ = VERSION
@@ -19,13 +63,14 @@ __version__ = VERSION
19
63
  """Context management."""
20
64
 
21
65
 
22
- def new_context(fresh=False, capture_exceptions=True):
66
+ def new_context(fresh=False, capture_exceptions=True, client=None):
23
67
  """
24
68
  Create a new context scope that will be active for the duration of the with block.
25
69
 
26
70
  Args:
27
71
  fresh: Whether to start with a fresh context (default: False)
28
72
  capture_exceptions: Whether to capture exceptions raised within the context (default: True)
73
+ client: Optional Posthog client instance to use for this context (default: None)
29
74
 
30
75
  Examples:
31
76
  ```python
@@ -38,7 +83,9 @@ def new_context(fresh=False, capture_exceptions=True):
38
83
  Category:
39
84
  Contexts
40
85
  """
41
- return inner_new_context(fresh=fresh, capture_exceptions=capture_exceptions)
86
+ return inner_new_context(
87
+ fresh=fresh, capture_exceptions=capture_exceptions, client=client
88
+ )
42
89
 
43
90
 
44
91
  def scoped(fresh=False, capture_exceptions=True):
@@ -102,6 +149,27 @@ def identify_context(distinct_id: str):
102
149
  return inner_identify_context(distinct_id)
103
150
 
104
151
 
152
+ def set_capture_exception_code_variables_context(enabled: bool):
153
+ """
154
+ Set whether code variables are captured for the current context.
155
+ """
156
+ return inner_set_capture_exception_code_variables_context(enabled)
157
+
158
+
159
+ def set_code_variables_mask_patterns_context(mask_patterns: list):
160
+ """
161
+ Variable names matching these patterns will be masked with *** when capturing code variables.
162
+ """
163
+ return inner_set_code_variables_mask_patterns_context(mask_patterns)
164
+
165
+
166
+ def set_code_variables_ignore_patterns_context(ignore_patterns: list):
167
+ """
168
+ Variable names matching these patterns will be ignored completely when capturing code variables.
169
+ """
170
+ return inner_set_code_variables_ignore_patterns_context(ignore_patterns)
171
+
172
+
105
173
  def tag(name: str, value: Any):
106
174
  """
107
175
  Add a tag to the current context.
@@ -149,6 +217,11 @@ enable_local_evaluation = True # type: bool
149
217
 
150
218
  default_client = None # type: Optional[Client]
151
219
 
220
+ capture_exception_code_variables = False
221
+ code_variables_mask_patterns = DEFAULT_CODE_VARIABLES_MASK_PATTERNS
222
+ code_variables_ignore_patterns = DEFAULT_CODE_VARIABLES_IGNORE_PATTERNS
223
+ in_app_modules = None # type: Optional[list[str]]
224
+
152
225
 
153
226
  # NOTE - this and following functions take unpacked kwargs because we needed to make
154
227
  # it impossible to write `posthog.capture(distinct-id, event-name)` - basically, to enforce
@@ -743,6 +816,10 @@ def setup() -> Client:
743
816
  enable_exception_autocapture=enable_exception_autocapture,
744
817
  log_captured_exceptions=log_captured_exceptions,
745
818
  enable_local_evaluation=enable_local_evaluation,
819
+ capture_exception_code_variables=capture_exception_code_variables,
820
+ code_variables_mask_patterns=code_variables_mask_patterns,
821
+ code_variables_ignore_patterns=code_variables_ignore_patterns,
822
+ in_app_modules=in_app_modules,
746
823
  )
747
824
 
748
825
  # always set incase user changes it
@@ -14,14 +14,9 @@ from posthoganalytics import setup
14
14
  from posthoganalytics.ai.types import StreamingContentBlock, TokenUsage, ToolInProgress
15
15
  from posthoganalytics.ai.utils import (
16
16
  call_llm_and_track_usage_async,
17
- extract_available_tool_calls,
18
- get_model_params,
19
- merge_system_prompt,
20
17
  merge_usage_stats,
21
- with_privacy_mode,
22
18
  )
23
19
  from posthoganalytics.ai.anthropic.anthropic_converter import (
24
- format_anthropic_streaming_content,
25
20
  extract_anthropic_usage_from_event,
26
21
  handle_anthropic_content_block_start,
27
22
  handle_anthropic_text_delta,
@@ -220,66 +215,34 @@ class AsyncWrappedMessages(AsyncMessages):
220
215
  content_blocks: List[StreamingContentBlock],
221
216
  accumulated_content: str,
222
217
  ):
223
- if posthog_trace_id is None:
224
- posthog_trace_id = str(uuid.uuid4())
225
-
226
- # Format output using converter
227
- formatted_content = format_anthropic_streaming_content(content_blocks)
228
- formatted_output = []
229
-
230
- if formatted_content:
231
- formatted_output = [{"role": "assistant", "content": formatted_content}]
232
- else:
233
- # Fallback to accumulated content if no blocks
234
- formatted_output = [
235
- {
236
- "role": "assistant",
237
- "content": [{"type": "text", "text": accumulated_content}],
238
- }
239
- ]
240
-
241
- event_properties = {
242
- "$ai_provider": "anthropic",
243
- "$ai_model": kwargs.get("model"),
244
- "$ai_model_parameters": get_model_params(kwargs),
245
- "$ai_input": with_privacy_mode(
246
- self._client._ph_client,
247
- posthog_privacy_mode,
248
- sanitize_anthropic(merge_system_prompt(kwargs, "anthropic")),
249
- ),
250
- "$ai_output_choices": with_privacy_mode(
251
- self._client._ph_client,
252
- posthog_privacy_mode,
253
- formatted_output,
254
- ),
255
- "$ai_http_status": 200,
256
- "$ai_input_tokens": usage_stats.get("input_tokens", 0),
257
- "$ai_output_tokens": usage_stats.get("output_tokens", 0),
258
- "$ai_cache_read_input_tokens": usage_stats.get(
259
- "cache_read_input_tokens", 0
260
- ),
261
- "$ai_cache_creation_input_tokens": usage_stats.get(
262
- "cache_creation_input_tokens", 0
218
+ from posthoganalytics.ai.types import StreamingEventData
219
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
220
+ format_anthropic_streaming_input,
221
+ format_anthropic_streaming_output_complete,
222
+ )
223
+ from posthoganalytics.ai.utils import capture_streaming_event
224
+
225
+ # Prepare standardized event data
226
+ formatted_input = format_anthropic_streaming_input(kwargs)
227
+ sanitized_input = sanitize_anthropic(formatted_input)
228
+
229
+ event_data = StreamingEventData(
230
+ provider="anthropic",
231
+ model=kwargs.get("model", "unknown"),
232
+ base_url=str(self._client.base_url),
233
+ kwargs=kwargs,
234
+ formatted_input=sanitized_input,
235
+ formatted_output=format_anthropic_streaming_output_complete(
236
+ content_blocks, accumulated_content
263
237
  ),
264
- "$ai_latency": latency,
265
- "$ai_trace_id": posthog_trace_id,
266
- "$ai_base_url": str(self._client.base_url),
267
- **(posthog_properties or {}),
268
- }
269
-
270
- # Add tools if available
271
- available_tools = extract_available_tool_calls("anthropic", kwargs)
272
-
273
- if available_tools:
274
- event_properties["$ai_tools"] = available_tools
275
-
276
- if posthog_distinct_id is None:
277
- event_properties["$process_person_profile"] = False
278
-
279
- if hasattr(self._client._ph_client, "capture"):
280
- self._client._ph_client.capture(
281
- distinct_id=posthog_distinct_id or posthog_trace_id,
282
- event="$ai_generation",
283
- properties=event_properties,
284
- groups=posthog_groups,
285
- )
238
+ usage_stats=usage_stats,
239
+ latency=latency,
240
+ distinct_id=posthog_distinct_id,
241
+ trace_id=posthog_trace_id,
242
+ properties=posthog_properties,
243
+ privacy_mode=posthog_privacy_mode,
244
+ groups=posthog_groups,
245
+ )
246
+
247
+ # Use the common capture function
248
+ capture_streaming_event(self._client._ph_client, event_data)
@@ -163,6 +163,32 @@ def format_anthropic_streaming_content(
163
163
  return formatted
164
164
 
165
165
 
166
+ def extract_anthropic_web_search_count(response: Any) -> int:
167
+ """
168
+ Extract web search count from Anthropic response.
169
+
170
+ Anthropic provides exact web search counts via usage.server_tool_use.web_search_requests.
171
+
172
+ Args:
173
+ response: The response from Anthropic API
174
+
175
+ Returns:
176
+ Number of web search requests (0 if none)
177
+ """
178
+ if not hasattr(response, "usage"):
179
+ return 0
180
+
181
+ if not hasattr(response.usage, "server_tool_use"):
182
+ return 0
183
+
184
+ server_tool_use = response.usage.server_tool_use
185
+
186
+ if hasattr(server_tool_use, "web_search_requests"):
187
+ return max(0, int(getattr(server_tool_use, "web_search_requests", 0)))
188
+
189
+ return 0
190
+
191
+
166
192
  def extract_anthropic_usage_from_response(response: Any) -> TokenUsage:
167
193
  """
168
194
  Extract usage from a full Anthropic response (non-streaming).
@@ -191,6 +217,10 @@ def extract_anthropic_usage_from_response(response: Any) -> TokenUsage:
191
217
  if cache_creation and cache_creation > 0:
192
218
  result["cache_creation_input_tokens"] = cache_creation
193
219
 
220
+ web_search_count = extract_anthropic_web_search_count(response)
221
+ if web_search_count > 0:
222
+ result["web_search_count"] = web_search_count
223
+
194
224
  return result
195
225
 
196
226
 
@@ -222,6 +252,16 @@ def extract_anthropic_usage_from_event(event: Any) -> TokenUsage:
222
252
  if hasattr(event, "usage") and event.usage:
223
253
  usage["output_tokens"] = getattr(event.usage, "output_tokens", 0)
224
254
 
255
+ # Extract web search count from usage
256
+ if hasattr(event.usage, "server_tool_use"):
257
+ server_tool_use = event.usage.server_tool_use
258
+ if hasattr(server_tool_use, "web_search_requests"):
259
+ web_search_count = int(
260
+ getattr(server_tool_use, "web_search_requests", 0)
261
+ )
262
+ if web_search_count > 0:
263
+ usage["web_search_count"] = web_search_count
264
+
225
265
  return usage
226
266
 
227
267
 
@@ -1,4 +1,5 @@
1
1
  from .gemini import Client
2
+ from .gemini_async import AsyncClient
2
3
  from .gemini_converter import (
3
4
  format_gemini_input,
4
5
  format_gemini_response,
@@ -9,12 +10,14 @@ from .gemini_converter import (
9
10
  # Create a genai-like module for perfect drop-in replacement
10
11
  class _GenAI:
11
12
  Client = Client
13
+ AsyncClient = AsyncClient
12
14
 
13
15
 
14
16
  genai = _GenAI()
15
17
 
16
18
  __all__ = [
17
19
  "Client",
20
+ "AsyncClient",
18
21
  "genai",
19
22
  "format_gemini_input",
20
23
  "format_gemini_response",
@@ -304,7 +304,7 @@ class Models:
304
304
 
305
305
  def generator():
306
306
  nonlocal usage_stats
307
- nonlocal accumulated_content # noqa: F824
307
+ nonlocal accumulated_content
308
308
  try:
309
309
  for chunk in response:
310
310
  # Extract usage stats from chunk