posthoganalytics 6.7.0__tar.gz → 6.7.2__tar.gz

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 (64) hide show
  1. {posthoganalytics-6.7.0/posthoganalytics.egg-info → posthoganalytics-6.7.2}/PKG-INFO +1 -1
  2. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/anthropic/__init__.py +10 -0
  3. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/anthropic/anthropic.py +94 -63
  4. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/anthropic/anthropic_async.py +88 -21
  5. posthoganalytics-6.7.2/posthoganalytics/ai/anthropic/anthropic_converter.py +393 -0
  6. posthoganalytics-6.7.2/posthoganalytics/ai/gemini/__init__.py +22 -0
  7. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/gemini/gemini.py +61 -67
  8. posthoganalytics-6.7.2/posthoganalytics/ai/gemini/gemini_converter.py +438 -0
  9. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/langchain/callbacks.py +3 -2
  10. posthoganalytics-6.7.2/posthoganalytics/ai/openai/__init__.py +20 -0
  11. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/openai/openai.py +114 -148
  12. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/openai/openai_async.py +72 -73
  13. posthoganalytics-6.7.2/posthoganalytics/ai/openai/openai_converter.py +585 -0
  14. posthoganalytics-6.7.2/posthoganalytics/ai/sanitization.py +226 -0
  15. posthoganalytics-6.7.2/posthoganalytics/ai/types.py +142 -0
  16. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/utils.py +232 -255
  17. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/client.py +7 -7
  18. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_feature_flags.py +2 -2
  19. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/version.py +1 -1
  20. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2/posthoganalytics.egg-info}/PKG-INFO +1 -1
  21. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics.egg-info/SOURCES.txt +5 -0
  22. posthoganalytics-6.7.0/posthoganalytics/ai/gemini/__init__.py +0 -11
  23. posthoganalytics-6.7.0/posthoganalytics/ai/openai/__init__.py +0 -5
  24. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/LICENSE +0 -0
  25. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/MANIFEST.in +0 -0
  26. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/README.md +0 -0
  27. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/__init__.py +0 -0
  28. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/__init__.py +0 -0
  29. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/anthropic/anthropic_providers.py +0 -0
  30. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/langchain/__init__.py +0 -0
  31. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/ai/openai/openai_providers.py +0 -0
  32. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/args.py +0 -0
  33. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/consumer.py +0 -0
  34. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/contexts.py +0 -0
  35. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/exception_capture.py +0 -0
  36. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/exception_utils.py +0 -0
  37. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/feature_flags.py +0 -0
  38. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/integrations/__init__.py +0 -0
  39. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/integrations/django.py +0 -0
  40. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/poller.py +0 -0
  41. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/py.typed +0 -0
  42. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/request.py +0 -0
  43. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/__init__.py +0 -0
  44. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_before_send.py +0 -0
  45. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_client.py +0 -0
  46. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_consumer.py +0 -0
  47. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_contexts.py +0 -0
  48. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_exception_capture.py +0 -0
  49. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_feature_flag.py +0 -0
  50. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_feature_flag_result.py +0 -0
  51. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_module.py +0 -0
  52. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_request.py +0 -0
  53. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_size_limited_dict.py +0 -0
  54. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_types.py +0 -0
  55. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/test/test_utils.py +0 -0
  56. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/types.py +0 -0
  57. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics/utils.py +0 -0
  58. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics.egg-info/dependency_links.txt +0 -0
  59. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics.egg-info/requires.txt +0 -0
  60. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/posthoganalytics.egg-info/top_level.txt +0 -0
  61. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/pyproject.toml +0 -0
  62. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/setup.cfg +0 -0
  63. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/setup.py +0 -0
  64. {posthoganalytics-6.7.0 → posthoganalytics-6.7.2}/setup_analytics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: posthoganalytics
3
- Version: 6.7.0
3
+ Version: 6.7.2
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -6,6 +6,12 @@ from .anthropic_providers import (
6
6
  AsyncAnthropicBedrock,
7
7
  AsyncAnthropicVertex,
8
8
  )
9
+ from .anthropic_converter import (
10
+ format_anthropic_response,
11
+ format_anthropic_input,
12
+ extract_anthropic_tools,
13
+ format_anthropic_streaming_content,
14
+ )
9
15
 
10
16
  __all__ = [
11
17
  "Anthropic",
@@ -14,4 +20,8 @@ __all__ = [
14
20
  "AsyncAnthropicBedrock",
15
21
  "AnthropicVertex",
16
22
  "AsyncAnthropicVertex",
23
+ "format_anthropic_response",
24
+ "format_anthropic_input",
25
+ "extract_anthropic_tools",
26
+ "format_anthropic_streaming_content",
17
27
  ]
@@ -8,14 +8,21 @@ except ImportError:
8
8
 
9
9
  import time
10
10
  import uuid
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, List, Optional
12
12
 
13
+ from posthoganalytics.ai.types import StreamingContentBlock, ToolInProgress
13
14
  from posthoganalytics.ai.utils import (
14
15
  call_llm_and_track_usage,
15
- get_model_params,
16
- merge_system_prompt,
17
- with_privacy_mode,
16
+ merge_usage_stats,
18
17
  )
18
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
19
+ extract_anthropic_usage_from_event,
20
+ handle_anthropic_content_block_start,
21
+ handle_anthropic_text_delta,
22
+ handle_anthropic_tool_delta,
23
+ finalize_anthropic_tool_input,
24
+ )
25
+ from posthoganalytics.ai.sanitization import sanitize_anthropic
19
26
  from posthoganalytics.client import Client as PostHogClient
20
27
  from posthoganalytics import setup
21
28
 
@@ -61,6 +68,7 @@ class WrappedMessages(Messages):
61
68
  posthog_groups: Optional group analytics properties
62
69
  **kwargs: Arguments passed to Anthropic's messages.create
63
70
  """
71
+
64
72
  if posthog_trace_id is None:
65
73
  posthog_trace_id = str(uuid.uuid4())
66
74
 
@@ -119,34 +127,65 @@ class WrappedMessages(Messages):
119
127
  ):
120
128
  start_time = time.time()
121
129
  usage_stats: Dict[str, int] = {"input_tokens": 0, "output_tokens": 0}
122
- accumulated_content = []
130
+ accumulated_content = ""
131
+ content_blocks: List[StreamingContentBlock] = []
132
+ tools_in_progress: Dict[str, ToolInProgress] = {}
133
+ current_text_block: Optional[StreamingContentBlock] = None
123
134
  response = super().create(**kwargs)
124
135
 
125
136
  def generator():
126
137
  nonlocal usage_stats
127
- nonlocal accumulated_content # noqa: F824
138
+ nonlocal accumulated_content
139
+ nonlocal content_blocks
140
+ nonlocal tools_in_progress
141
+ nonlocal current_text_block
142
+
128
143
  try:
129
144
  for event in response:
130
- if hasattr(event, "usage") and event.usage:
131
- usage_stats = {
132
- k: getattr(event.usage, k, 0)
133
- for k in [
134
- "input_tokens",
135
- "output_tokens",
136
- "cache_read_input_tokens",
137
- "cache_creation_input_tokens",
138
- ]
139
- }
140
-
141
- if hasattr(event, "content") and event.content:
142
- accumulated_content.append(event.content)
145
+ # Extract usage stats from event
146
+ event_usage = extract_anthropic_usage_from_event(event)
147
+ merge_usage_stats(usage_stats, event_usage)
148
+
149
+ # Handle content block start events
150
+ if hasattr(event, "type") and event.type == "content_block_start":
151
+ block, tool = handle_anthropic_content_block_start(event)
152
+
153
+ if block:
154
+ content_blocks.append(block)
155
+
156
+ if block.get("type") == "text":
157
+ current_text_block = block
158
+ else:
159
+ current_text_block = None
160
+
161
+ if tool:
162
+ tool_id = tool["block"].get("id")
163
+ if tool_id:
164
+ tools_in_progress[tool_id] = tool
165
+
166
+ # Handle text delta events
167
+ delta_text = handle_anthropic_text_delta(event, current_text_block)
168
+
169
+ if delta_text:
170
+ accumulated_content += delta_text
171
+
172
+ # Handle tool input delta events
173
+ handle_anthropic_tool_delta(
174
+ event, content_blocks, tools_in_progress
175
+ )
176
+
177
+ # Handle content block stop events
178
+ if hasattr(event, "type") and event.type == "content_block_stop":
179
+ current_text_block = None
180
+ finalize_anthropic_tool_input(
181
+ event, content_blocks, tools_in_progress
182
+ )
143
183
 
144
184
  yield event
145
185
 
146
186
  finally:
147
187
  end_time = time.time()
148
188
  latency = end_time - start_time
149
- output = "".join(accumulated_content)
150
189
 
151
190
  self._capture_streaming_event(
152
191
  posthog_distinct_id,
@@ -157,7 +196,8 @@ class WrappedMessages(Messages):
157
196
  kwargs,
158
197
  usage_stats,
159
198
  latency,
160
- output,
199
+ content_blocks,
200
+ accumulated_content,
161
201
  )
162
202
 
163
203
  return generator()
@@ -172,47 +212,38 @@ class WrappedMessages(Messages):
172
212
  kwargs: Dict[str, Any],
173
213
  usage_stats: Dict[str, int],
174
214
  latency: float,
175
- output: str,
215
+ content_blocks: List[StreamingContentBlock],
216
+ accumulated_content: str,
176
217
  ):
177
- if posthog_trace_id is None:
178
- posthog_trace_id = str(uuid.uuid4())
179
-
180
- event_properties = {
181
- "$ai_provider": "anthropic",
182
- "$ai_model": kwargs.get("model"),
183
- "$ai_model_parameters": get_model_params(kwargs),
184
- "$ai_input": with_privacy_mode(
185
- self._client._ph_client,
186
- posthog_privacy_mode,
187
- merge_system_prompt(kwargs, "anthropic"),
188
- ),
189
- "$ai_output_choices": with_privacy_mode(
190
- self._client._ph_client,
191
- posthog_privacy_mode,
192
- [{"content": output, "role": "assistant"}],
193
- ),
194
- "$ai_http_status": 200,
195
- "$ai_input_tokens": usage_stats.get("input_tokens", 0),
196
- "$ai_output_tokens": usage_stats.get("output_tokens", 0),
197
- "$ai_cache_read_input_tokens": usage_stats.get(
198
- "cache_read_input_tokens", 0
199
- ),
200
- "$ai_cache_creation_input_tokens": usage_stats.get(
201
- "cache_creation_input_tokens", 0
218
+ from posthoganalytics.ai.types import StreamingEventData
219
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
220
+ standardize_anthropic_usage,
221
+ format_anthropic_streaming_input,
222
+ format_anthropic_streaming_output_complete,
223
+ )
224
+ from posthoganalytics.ai.utils import capture_streaming_event
225
+
226
+ # Prepare standardized event data
227
+ formatted_input = format_anthropic_streaming_input(kwargs)
228
+ sanitized_input = sanitize_anthropic(formatted_input)
229
+
230
+ event_data = StreamingEventData(
231
+ provider="anthropic",
232
+ model=kwargs.get("model", "unknown"),
233
+ base_url=str(self._client.base_url),
234
+ kwargs=kwargs,
235
+ formatted_input=sanitized_input,
236
+ formatted_output=format_anthropic_streaming_output_complete(
237
+ content_blocks, accumulated_content
202
238
  ),
203
- "$ai_latency": latency,
204
- "$ai_trace_id": posthog_trace_id,
205
- "$ai_base_url": str(self._client.base_url),
206
- **(posthog_properties or {}),
207
- }
208
-
209
- if posthog_distinct_id is None:
210
- event_properties["$process_person_profile"] = False
211
-
212
- if hasattr(self._client._ph_client, "capture"):
213
- self._client._ph_client.capture(
214
- distinct_id=posthog_distinct_id or posthog_trace_id,
215
- event="$ai_generation",
216
- properties=event_properties,
217
- groups=posthog_groups,
218
- )
239
+ usage_stats=standardize_anthropic_usage(usage_stats),
240
+ latency=latency,
241
+ distinct_id=posthog_distinct_id,
242
+ trace_id=posthog_trace_id,
243
+ properties=posthog_properties,
244
+ privacy_mode=posthog_privacy_mode,
245
+ groups=posthog_groups,
246
+ )
247
+
248
+ # Use the common capture function
249
+ capture_streaming_event(self._client._ph_client, event_data)
@@ -8,15 +8,27 @@ except ImportError:
8
8
 
9
9
  import time
10
10
  import uuid
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, List, Optional
12
12
 
13
13
  from posthoganalytics import setup
14
+ from posthoganalytics.ai.types import StreamingContentBlock, ToolInProgress
14
15
  from posthoganalytics.ai.utils import (
15
16
  call_llm_and_track_usage_async,
17
+ extract_available_tool_calls,
16
18
  get_model_params,
17
19
  merge_system_prompt,
20
+ merge_usage_stats,
18
21
  with_privacy_mode,
19
22
  )
23
+ from posthoganalytics.ai.anthropic.anthropic_converter import (
24
+ format_anthropic_streaming_content,
25
+ extract_anthropic_usage_from_event,
26
+ handle_anthropic_content_block_start,
27
+ handle_anthropic_text_delta,
28
+ handle_anthropic_tool_delta,
29
+ finalize_anthropic_tool_input,
30
+ )
31
+ from posthoganalytics.ai.sanitization import sanitize_anthropic
20
32
  from posthoganalytics.client import Client as PostHogClient
21
33
 
22
34
 
@@ -61,6 +73,7 @@ class AsyncWrappedMessages(AsyncMessages):
61
73
  posthog_groups: Optional group analytics properties
62
74
  **kwargs: Arguments passed to Anthropic's messages.create
63
75
  """
76
+
64
77
  if posthog_trace_id is None:
65
78
  posthog_trace_id = str(uuid.uuid4())
66
79
 
@@ -119,34 +132,65 @@ class AsyncWrappedMessages(AsyncMessages):
119
132
  ):
120
133
  start_time = time.time()
121
134
  usage_stats: Dict[str, int] = {"input_tokens": 0, "output_tokens": 0}
122
- accumulated_content = []
135
+ accumulated_content = ""
136
+ content_blocks: List[StreamingContentBlock] = []
137
+ tools_in_progress: Dict[str, ToolInProgress] = {}
138
+ current_text_block: Optional[StreamingContentBlock] = None
123
139
  response = await super().create(**kwargs)
124
140
 
125
141
  async def generator():
126
142
  nonlocal usage_stats
127
- nonlocal accumulated_content # noqa: F824
143
+ nonlocal accumulated_content
144
+ nonlocal content_blocks
145
+ nonlocal tools_in_progress
146
+ nonlocal current_text_block
147
+
128
148
  try:
129
149
  async for event in response:
130
- if hasattr(event, "usage") and event.usage:
131
- usage_stats = {
132
- k: getattr(event.usage, k, 0)
133
- for k in [
134
- "input_tokens",
135
- "output_tokens",
136
- "cache_read_input_tokens",
137
- "cache_creation_input_tokens",
138
- ]
139
- }
140
-
141
- if hasattr(event, "content") and event.content:
142
- accumulated_content.append(event.content)
150
+ # Extract usage stats from event
151
+ event_usage = extract_anthropic_usage_from_event(event)
152
+ merge_usage_stats(usage_stats, event_usage)
153
+
154
+ # Handle content block start events
155
+ if hasattr(event, "type") and event.type == "content_block_start":
156
+ block, tool = handle_anthropic_content_block_start(event)
157
+
158
+ if block:
159
+ content_blocks.append(block)
160
+
161
+ if block.get("type") == "text":
162
+ current_text_block = block
163
+ else:
164
+ current_text_block = None
165
+
166
+ if tool:
167
+ tool_id = tool["block"].get("id")
168
+ if tool_id:
169
+ tools_in_progress[tool_id] = tool
170
+
171
+ # Handle text delta events
172
+ delta_text = handle_anthropic_text_delta(event, current_text_block)
173
+
174
+ if delta_text:
175
+ accumulated_content += delta_text
176
+
177
+ # Handle tool input delta events
178
+ handle_anthropic_tool_delta(
179
+ event, content_blocks, tools_in_progress
180
+ )
181
+
182
+ # Handle content block stop events
183
+ if hasattr(event, "type") and event.type == "content_block_stop":
184
+ current_text_block = None
185
+ finalize_anthropic_tool_input(
186
+ event, content_blocks, tools_in_progress
187
+ )
143
188
 
144
189
  yield event
145
190
 
146
191
  finally:
147
192
  end_time = time.time()
148
193
  latency = end_time - start_time
149
- output = "".join(accumulated_content)
150
194
 
151
195
  await self._capture_streaming_event(
152
196
  posthog_distinct_id,
@@ -157,7 +201,8 @@ class AsyncWrappedMessages(AsyncMessages):
157
201
  kwargs,
158
202
  usage_stats,
159
203
  latency,
160
- output,
204
+ content_blocks,
205
+ accumulated_content,
161
206
  )
162
207
 
163
208
  return generator()
@@ -172,11 +217,27 @@ class AsyncWrappedMessages(AsyncMessages):
172
217
  kwargs: Dict[str, Any],
173
218
  usage_stats: Dict[str, int],
174
219
  latency: float,
175
- output: str,
220
+ content_blocks: List[StreamingContentBlock],
221
+ accumulated_content: str,
176
222
  ):
177
223
  if posthog_trace_id is None:
178
224
  posthog_trace_id = str(uuid.uuid4())
179
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
+
180
241
  event_properties = {
181
242
  "$ai_provider": "anthropic",
182
243
  "$ai_model": kwargs.get("model"),
@@ -184,12 +245,12 @@ class AsyncWrappedMessages(AsyncMessages):
184
245
  "$ai_input": with_privacy_mode(
185
246
  self._client._ph_client,
186
247
  posthog_privacy_mode,
187
- merge_system_prompt(kwargs, "anthropic"),
248
+ sanitize_anthropic(merge_system_prompt(kwargs, "anthropic")),
188
249
  ),
189
250
  "$ai_output_choices": with_privacy_mode(
190
251
  self._client._ph_client,
191
252
  posthog_privacy_mode,
192
- [{"content": output, "role": "assistant"}],
253
+ formatted_output,
193
254
  ),
194
255
  "$ai_http_status": 200,
195
256
  "$ai_input_tokens": usage_stats.get("input_tokens", 0),
@@ -206,6 +267,12 @@ class AsyncWrappedMessages(AsyncMessages):
206
267
  **(posthog_properties or {}),
207
268
  }
208
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
+
209
276
  if posthog_distinct_id is None:
210
277
  event_properties["$process_person_profile"] = False
211
278