posthoganalytics 6.3.3__py3-none-any.whl → 6.3.4__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.
@@ -11,7 +11,7 @@ from posthoganalytics.contexts import (
11
11
  set_context_session as inner_set_context_session,
12
12
  identify_context as inner_identify_context,
13
13
  )
14
- from posthoganalytics.types import FeatureFlag, FlagsAndPayloads
14
+ from posthoganalytics.types import FeatureFlag, FlagsAndPayloads, FeatureFlagResult
15
15
  from posthoganalytics.version import VERSION
16
16
 
17
17
  __version__ = VERSION
@@ -388,9 +388,9 @@ def capture_exception(
388
388
  def feature_enabled(
389
389
  key, # type: str
390
390
  distinct_id, # type: str
391
- groups={}, # type: dict
392
- person_properties={}, # type: dict
393
- group_properties={}, # type: dict
391
+ groups=None, # type: Optional[dict]
392
+ person_properties=None, # type: Optional[dict]
393
+ group_properties=None, # type: Optional[dict]
394
394
  only_evaluate_locally=False, # type: bool
395
395
  send_feature_flag_events=True, # type: bool
396
396
  disable_geoip=None, # type: Optional[bool]
@@ -427,9 +427,9 @@ def feature_enabled(
427
427
  "feature_enabled",
428
428
  key=key,
429
429
  distinct_id=distinct_id,
430
- groups=groups,
431
- person_properties=person_properties,
432
- group_properties=group_properties,
430
+ groups=groups or {},
431
+ person_properties=person_properties or {},
432
+ group_properties=group_properties or {},
433
433
  only_evaluate_locally=only_evaluate_locally,
434
434
  send_feature_flag_events=send_feature_flag_events,
435
435
  disable_geoip=disable_geoip,
@@ -439,9 +439,9 @@ def feature_enabled(
439
439
  def get_feature_flag(
440
440
  key, # type: str
441
441
  distinct_id, # type: str
442
- groups={}, # type: dict
443
- person_properties={}, # type: dict
444
- group_properties={}, # type: dict
442
+ groups=None, # type: Optional[dict]
443
+ person_properties=None, # type: Optional[dict]
444
+ group_properties=None, # type: Optional[dict]
445
445
  only_evaluate_locally=False, # type: bool
446
446
  send_feature_flag_events=True, # type: bool
447
447
  disable_geoip=None, # type: Optional[bool]
@@ -477,9 +477,9 @@ def get_feature_flag(
477
477
  "get_feature_flag",
478
478
  key=key,
479
479
  distinct_id=distinct_id,
480
- groups=groups,
481
- person_properties=person_properties,
482
- group_properties=group_properties,
480
+ groups=groups or {},
481
+ person_properties=person_properties or {},
482
+ group_properties=group_properties or {},
483
483
  only_evaluate_locally=only_evaluate_locally,
484
484
  send_feature_flag_events=send_feature_flag_events,
485
485
  disable_geoip=disable_geoip,
@@ -488,9 +488,9 @@ def get_feature_flag(
488
488
 
489
489
  def get_all_flags(
490
490
  distinct_id, # type: str
491
- groups={}, # type: dict
492
- person_properties={}, # type: dict
493
- group_properties={}, # type: dict
491
+ groups=None, # type: Optional[dict]
492
+ person_properties=None, # type: Optional[dict]
493
+ group_properties=None, # type: Optional[dict]
494
494
  only_evaluate_locally=False, # type: bool
495
495
  disable_geoip=None, # type: Optional[bool]
496
496
  ) -> Optional[dict[str, FeatureFlag]]:
@@ -520,21 +520,64 @@ def get_all_flags(
520
520
  return _proxy(
521
521
  "get_all_flags",
522
522
  distinct_id=distinct_id,
523
- groups=groups,
524
- person_properties=person_properties,
525
- group_properties=group_properties,
523
+ groups=groups or {},
524
+ person_properties=person_properties or {},
525
+ group_properties=group_properties or {},
526
526
  only_evaluate_locally=only_evaluate_locally,
527
527
  disable_geoip=disable_geoip,
528
528
  )
529
529
 
530
530
 
531
+ def get_feature_flag_result(
532
+ key,
533
+ distinct_id,
534
+ groups=None, # type: Optional[dict]
535
+ person_properties=None, # type: Optional[dict]
536
+ group_properties=None, # type: Optional[dict]
537
+ only_evaluate_locally=False,
538
+ send_feature_flag_events=True,
539
+ disable_geoip=None, # type: Optional[bool]
540
+ ):
541
+ # type: (...) -> Optional[FeatureFlagResult]
542
+ """
543
+ Get a FeatureFlagResult object which contains the flag result and payload.
544
+
545
+ This method evaluates a feature flag and returns a FeatureFlagResult object containing:
546
+ - enabled: Whether the flag is enabled
547
+ - variant: The variant value if the flag has variants
548
+ - payload: The payload associated with the flag (automatically deserialized from JSON)
549
+ - key: The flag key
550
+ - reason: Why the flag was enabled/disabled
551
+
552
+ Example:
553
+ ```python
554
+ result = posthog.get_feature_flag_result('beta-feature', 'distinct_id')
555
+ if result and result.enabled:
556
+ # Use the variant and payload
557
+ print(f"Variant: {result.variant}")
558
+ print(f"Payload: {result.payload}")
559
+ ```
560
+ """
561
+ return _proxy(
562
+ "get_feature_flag_result",
563
+ key=key,
564
+ distinct_id=distinct_id,
565
+ groups=groups or {},
566
+ person_properties=person_properties or {},
567
+ group_properties=group_properties or {},
568
+ only_evaluate_locally=only_evaluate_locally,
569
+ send_feature_flag_events=send_feature_flag_events,
570
+ disable_geoip=disable_geoip,
571
+ )
572
+
573
+
531
574
  def get_feature_flag_payload(
532
575
  key,
533
576
  distinct_id,
534
577
  match_value=None,
535
- groups={},
536
- person_properties={},
537
- group_properties={},
578
+ groups=None, # type: Optional[dict]
579
+ person_properties=None, # type: Optional[dict]
580
+ group_properties=None, # type: Optional[dict]
538
581
  only_evaluate_locally=False,
539
582
  send_feature_flag_events=True,
540
583
  disable_geoip=None, # type: Optional[bool]
@@ -544,9 +587,9 @@ def get_feature_flag_payload(
544
587
  key=key,
545
588
  distinct_id=distinct_id,
546
589
  match_value=match_value,
547
- groups=groups,
548
- person_properties=person_properties,
549
- group_properties=group_properties,
590
+ groups=groups or {},
591
+ person_properties=person_properties or {},
592
+ group_properties=group_properties or {},
550
593
  only_evaluate_locally=only_evaluate_locally,
551
594
  send_feature_flag_events=send_feature_flag_events,
552
595
  disable_geoip=disable_geoip,
@@ -575,18 +618,18 @@ def get_remote_config_payload(
575
618
 
576
619
  def get_all_flags_and_payloads(
577
620
  distinct_id,
578
- groups={},
579
- person_properties={},
580
- group_properties={},
621
+ groups=None, # type: Optional[dict]
622
+ person_properties=None, # type: Optional[dict]
623
+ group_properties=None, # type: Optional[dict]
581
624
  only_evaluate_locally=False,
582
625
  disable_geoip=None, # type: Optional[bool]
583
626
  ) -> FlagsAndPayloads:
584
627
  return _proxy(
585
628
  "get_all_flags_and_payloads",
586
629
  distinct_id=distinct_id,
587
- groups=groups,
588
- person_properties=person_properties,
589
- group_properties=group_properties,
630
+ groups=groups or {},
631
+ person_properties=person_properties or {},
632
+ group_properties=group_properties or {},
590
633
  only_evaluate_locally=only_evaluate_locally,
591
634
  disable_geoip=disable_geoip,
592
635
  )
@@ -556,12 +556,9 @@ class CallbackHandler(BaseCallbackHandler):
556
556
  "$ai_latency": run.latency,
557
557
  "$ai_base_url": run.base_url,
558
558
  }
559
+
559
560
  if run.tools:
560
- event_properties["$ai_tools"] = with_privacy_mode(
561
- self._ph_client,
562
- self._privacy_mode,
563
- run.tools,
564
- )
561
+ event_properties["$ai_tools"] = run.tools
565
562
 
566
563
  if isinstance(output, BaseException):
567
564
  event_properties["$ai_http_status"] = _get_http_status(output)
@@ -587,7 +584,8 @@ class CallbackHandler(BaseCallbackHandler):
587
584
  ]
588
585
  else:
589
586
  completions = [
590
- _extract_raw_esponse(generation) for generation in generation_result
587
+ _extract_raw_response(generation)
588
+ for generation in generation_result
591
589
  ]
592
590
  event_properties["$ai_output_choices"] = with_privacy_mode(
593
591
  self._ph_client, self._privacy_mode, completions
@@ -618,7 +616,7 @@ class CallbackHandler(BaseCallbackHandler):
618
616
  )
619
617
 
620
618
 
621
- def _extract_raw_esponse(last_response):
619
+ def _extract_raw_response(last_response):
622
620
  """Extract the response from the last response of the LLM call."""
623
621
  # We return the text of the response if not empty
624
622
  if last_response.text is not None and last_response.text.strip() != "":
@@ -11,6 +11,7 @@ except ImportError:
11
11
 
12
12
  from posthoganalytics.ai.utils import (
13
13
  call_llm_and_track_usage,
14
+ extract_available_tool_calls,
14
15
  get_model_params,
15
16
  with_privacy_mode,
16
17
  )
@@ -167,6 +168,7 @@ class WrappedResponses:
167
168
  usage_stats,
168
169
  latency,
169
170
  output,
171
+ extract_available_tool_calls("openai", kwargs),
170
172
  )
171
173
 
172
174
  return generator()
@@ -182,7 +184,7 @@ class WrappedResponses:
182
184
  usage_stats: Dict[str, int],
183
185
  latency: float,
184
186
  output: Any,
185
- tool_calls: Optional[List[Dict[str, Any]]] = None,
187
+ available_tool_calls: Optional[List[Dict[str, Any]]] = None,
186
188
  ):
187
189
  if posthog_trace_id is None:
188
190
  posthog_trace_id = str(uuid.uuid4())
@@ -212,12 +214,8 @@ class WrappedResponses:
212
214
  **(posthog_properties or {}),
213
215
  }
214
216
 
215
- if tool_calls:
216
- event_properties["$ai_tools"] = with_privacy_mode(
217
- self._client._ph_client,
218
- posthog_privacy_mode,
219
- tool_calls,
220
- )
217
+ if available_tool_calls:
218
+ event_properties["$ai_tools"] = available_tool_calls
221
219
 
222
220
  if posthog_distinct_id is None:
223
221
  event_properties["$process_person_profile"] = False
@@ -341,7 +339,6 @@ class WrappedCompletions:
341
339
  start_time = time.time()
342
340
  usage_stats: Dict[str, int] = {}
343
341
  accumulated_content = []
344
- accumulated_tools = {}
345
342
  if "stream_options" not in kwargs:
346
343
  kwargs["stream_options"] = {}
347
344
  kwargs["stream_options"]["include_usage"] = True
@@ -350,7 +347,6 @@ class WrappedCompletions:
350
347
  def generator():
351
348
  nonlocal usage_stats
352
349
  nonlocal accumulated_content # noqa: F824
353
- nonlocal accumulated_tools # noqa: F824
354
350
 
355
351
  try:
356
352
  for chunk in response:
@@ -389,31 +385,12 @@ class WrappedCompletions:
389
385
  if content:
390
386
  accumulated_content.append(content)
391
387
 
392
- # Process tool calls
393
- tool_calls = getattr(chunk.choices[0].delta, "tool_calls", None)
394
- if tool_calls:
395
- for tool_call in tool_calls:
396
- index = tool_call.index
397
- if index not in accumulated_tools:
398
- accumulated_tools[index] = tool_call
399
- else:
400
- # Append arguments for existing tool calls
401
- if hasattr(tool_call, "function") and hasattr(
402
- tool_call.function, "arguments"
403
- ):
404
- accumulated_tools[
405
- index
406
- ].function.arguments += (
407
- tool_call.function.arguments
408
- )
409
-
410
388
  yield chunk
411
389
 
412
390
  finally:
413
391
  end_time = time.time()
414
392
  latency = end_time - start_time
415
393
  output = "".join(accumulated_content)
416
- tools = list(accumulated_tools.values()) if accumulated_tools else None
417
394
  self._capture_streaming_event(
418
395
  posthog_distinct_id,
419
396
  posthog_trace_id,
@@ -424,7 +401,7 @@ class WrappedCompletions:
424
401
  usage_stats,
425
402
  latency,
426
403
  output,
427
- tools,
404
+ extract_available_tool_calls("openai", kwargs),
428
405
  )
429
406
 
430
407
  return generator()
@@ -440,7 +417,7 @@ class WrappedCompletions:
440
417
  usage_stats: Dict[str, int],
441
418
  latency: float,
442
419
  output: Any,
443
- tool_calls: Optional[List[Dict[str, Any]]] = None,
420
+ available_tool_calls: Optional[List[Dict[str, Any]]] = None,
444
421
  ):
445
422
  if posthog_trace_id is None:
446
423
  posthog_trace_id = str(uuid.uuid4())
@@ -470,12 +447,8 @@ class WrappedCompletions:
470
447
  **(posthog_properties or {}),
471
448
  }
472
449
 
473
- if tool_calls:
474
- event_properties["$ai_tools"] = with_privacy_mode(
475
- self._client._ph_client,
476
- posthog_privacy_mode,
477
- tool_calls,
478
- )
450
+ if available_tool_calls:
451
+ event_properties["$ai_tools"] = available_tool_calls
479
452
 
480
453
  if posthog_distinct_id is None:
481
454
  event_properties["$process_person_profile"] = False
@@ -12,6 +12,7 @@ except ImportError:
12
12
  from posthoganalytics import setup
13
13
  from posthoganalytics.ai.utils import (
14
14
  call_llm_and_track_usage_async,
15
+ extract_available_tool_calls,
15
16
  get_model_params,
16
17
  with_privacy_mode,
17
18
  )
@@ -168,6 +169,7 @@ class WrappedResponses:
168
169
  usage_stats,
169
170
  latency,
170
171
  output,
172
+ extract_available_tool_calls("openai", kwargs),
171
173
  )
172
174
 
173
175
  return async_generator()
@@ -183,7 +185,7 @@ class WrappedResponses:
183
185
  usage_stats: Dict[str, int],
184
186
  latency: float,
185
187
  output: Any,
186
- tool_calls: Optional[List[Dict[str, Any]]] = None,
188
+ available_tool_calls: Optional[List[Dict[str, Any]]] = None,
187
189
  ):
188
190
  if posthog_trace_id is None:
189
191
  posthog_trace_id = str(uuid.uuid4())
@@ -213,12 +215,8 @@ class WrappedResponses:
213
215
  **(posthog_properties or {}),
214
216
  }
215
217
 
216
- if tool_calls:
217
- event_properties["$ai_tools"] = with_privacy_mode(
218
- self._client._ph_client,
219
- posthog_privacy_mode,
220
- tool_calls,
221
- )
218
+ if available_tool_calls:
219
+ event_properties["$ai_tools"] = available_tool_calls
222
220
 
223
221
  if posthog_distinct_id is None:
224
222
  event_properties["$process_person_profile"] = False
@@ -344,7 +342,6 @@ class WrappedCompletions:
344
342
  start_time = time.time()
345
343
  usage_stats: Dict[str, int] = {}
346
344
  accumulated_content = []
347
- accumulated_tools = {}
348
345
 
349
346
  if "stream_options" not in kwargs:
350
347
  kwargs["stream_options"] = {}
@@ -354,7 +351,6 @@ class WrappedCompletions:
354
351
  async def async_generator():
355
352
  nonlocal usage_stats
356
353
  nonlocal accumulated_content # noqa: F824
357
- nonlocal accumulated_tools # noqa: F824
358
354
 
359
355
  try:
360
356
  async for chunk in response:
@@ -393,31 +389,12 @@ class WrappedCompletions:
393
389
  if content:
394
390
  accumulated_content.append(content)
395
391
 
396
- # Process tool calls
397
- tool_calls = getattr(chunk.choices[0].delta, "tool_calls", None)
398
- if tool_calls:
399
- for tool_call in tool_calls:
400
- index = tool_call.index
401
- if index not in accumulated_tools:
402
- accumulated_tools[index] = tool_call
403
- else:
404
- # Append arguments for existing tool calls
405
- if hasattr(tool_call, "function") and hasattr(
406
- tool_call.function, "arguments"
407
- ):
408
- accumulated_tools[
409
- index
410
- ].function.arguments += (
411
- tool_call.function.arguments
412
- )
413
-
414
392
  yield chunk
415
393
 
416
394
  finally:
417
395
  end_time = time.time()
418
396
  latency = end_time - start_time
419
397
  output = "".join(accumulated_content)
420
- tools = list(accumulated_tools.values()) if accumulated_tools else None
421
398
  await self._capture_streaming_event(
422
399
  posthog_distinct_id,
423
400
  posthog_trace_id,
@@ -428,7 +405,7 @@ class WrappedCompletions:
428
405
  usage_stats,
429
406
  latency,
430
407
  output,
431
- tools,
408
+ extract_available_tool_calls("openai", kwargs),
432
409
  )
433
410
 
434
411
  return async_generator()
@@ -444,7 +421,7 @@ class WrappedCompletions:
444
421
  usage_stats: Dict[str, int],
445
422
  latency: float,
446
423
  output: Any,
447
- tool_calls: Optional[List[Dict[str, Any]]] = None,
424
+ available_tool_calls: Optional[List[Dict[str, Any]]] = None,
448
425
  ):
449
426
  if posthog_trace_id is None:
450
427
  posthog_trace_id = str(uuid.uuid4())
@@ -474,12 +451,8 @@ class WrappedCompletions:
474
451
  **(posthog_properties or {}),
475
452
  }
476
453
 
477
- if tool_calls:
478
- event_properties["$ai_tools"] = with_privacy_mode(
479
- self._client._ph_client,
480
- posthog_privacy_mode,
481
- tool_calls,
482
- )
454
+ if available_tool_calls:
455
+ event_properties["$ai_tools"] = available_tool_calls
483
456
 
484
457
  if posthog_distinct_id is None:
485
458
  event_properties["$process_person_profile"] = False
@@ -117,6 +117,8 @@ def format_response(response, provider: str):
117
117
 
118
118
  def format_response_anthropic(response):
119
119
  output = []
120
+ content = []
121
+
120
122
  for choice in response.content:
121
123
  if (
122
124
  hasattr(choice, "type")
@@ -124,32 +126,78 @@ def format_response_anthropic(response):
124
126
  and hasattr(choice, "text")
125
127
  and choice.text
126
128
  ):
127
- output.append(
128
- {
129
- "role": "assistant",
130
- "content": choice.text,
131
- }
132
- )
129
+ content.append({"type": "text", "text": choice.text})
130
+ elif (
131
+ hasattr(choice, "type")
132
+ and choice.type == "tool_use"
133
+ and hasattr(choice, "name")
134
+ and hasattr(choice, "id")
135
+ ):
136
+ tool_call = {
137
+ "type": "function",
138
+ "id": choice.id,
139
+ "function": {
140
+ "name": choice.name,
141
+ "arguments": getattr(choice, "input", {}),
142
+ },
143
+ }
144
+ content.append(tool_call)
145
+
146
+ if content:
147
+ message = {
148
+ "role": "assistant",
149
+ "content": content,
150
+ }
151
+ output.append(message)
152
+
133
153
  return output
134
154
 
135
155
 
136
156
  def format_response_openai(response):
137
157
  output = []
158
+
138
159
  if hasattr(response, "choices"):
160
+ content = []
161
+ role = "assistant"
162
+
139
163
  for choice in response.choices:
140
164
  # Handle Chat Completions response format
141
- if hasattr(choice, "message") and choice.message and choice.message.content:
142
- output.append(
143
- {
144
- "content": choice.message.content,
145
- "role": choice.message.role,
146
- }
147
- )
165
+ if hasattr(choice, "message") and choice.message:
166
+ if choice.message.role:
167
+ role = choice.message.role
168
+
169
+ if choice.message.content:
170
+ content.append({"type": "text", "text": choice.message.content})
171
+
172
+ if hasattr(choice.message, "tool_calls") and choice.message.tool_calls:
173
+ for tool_call in choice.message.tool_calls:
174
+ content.append(
175
+ {
176
+ "type": "function",
177
+ "id": tool_call.id,
178
+ "function": {
179
+ "name": tool_call.function.name,
180
+ "arguments": tool_call.function.arguments,
181
+ },
182
+ }
183
+ )
184
+
185
+ if content:
186
+ message = {
187
+ "role": role,
188
+ "content": content,
189
+ }
190
+ output.append(message)
191
+
148
192
  # Handle Responses API format
149
193
  if hasattr(response, "output"):
194
+ content = []
195
+ role = "assistant"
196
+
150
197
  for item in response.output:
151
198
  if item.type == "message":
152
- # Extract text content from the content list
199
+ role = item.role
200
+
153
201
  if hasattr(item, "content") and isinstance(item.content, list):
154
202
  for content_item in item.content:
155
203
  if (
@@ -157,112 +205,110 @@ def format_response_openai(response):
157
205
  and content_item.type == "output_text"
158
206
  and hasattr(content_item, "text")
159
207
  ):
160
- output.append(
161
- {
162
- "content": content_item.text,
163
- "role": item.role,
164
- }
165
- )
208
+ content.append({"type": "text", "text": content_item.text})
166
209
  elif hasattr(content_item, "text"):
167
- output.append(
168
- {
169
- "content": content_item.text,
170
- "role": item.role,
171
- }
172
- )
210
+ content.append({"type": "text", "text": content_item.text})
173
211
  elif (
174
212
  hasattr(content_item, "type")
175
213
  and content_item.type == "input_image"
176
214
  and hasattr(content_item, "image_url")
177
215
  ):
178
- output.append(
216
+ content.append(
179
217
  {
180
- "content": {
181
- "type": "image",
182
- "image": content_item.image_url,
183
- },
184
- "role": item.role,
218
+ "type": "image",
219
+ "image": content_item.image_url,
185
220
  }
186
221
  )
187
- else:
188
- output.append(
189
- {
190
- "content": item.content,
191
- "role": item.role,
192
- }
193
- )
222
+ elif hasattr(item, "content"):
223
+ content.append({"type": "text", "text": str(item.content)})
224
+
225
+ elif hasattr(item, "type") and item.type == "function_call":
226
+ content.append(
227
+ {
228
+ "type": "function",
229
+ "id": getattr(item, "call_id", getattr(item, "id", "")),
230
+ "function": {
231
+ "name": item.name,
232
+ "arguments": getattr(item, "arguments", {}),
233
+ },
234
+ }
235
+ )
236
+
237
+ if content:
238
+ message = {
239
+ "role": role,
240
+ "content": content,
241
+ }
242
+ output.append(message)
243
+
194
244
  return output
195
245
 
196
246
 
197
247
  def format_response_gemini(response):
198
248
  output = []
249
+
199
250
  if hasattr(response, "candidates") and response.candidates:
200
251
  for candidate in response.candidates:
201
252
  if hasattr(candidate, "content") and candidate.content:
202
- content_text = ""
253
+ content = []
254
+
203
255
  if hasattr(candidate.content, "parts") and candidate.content.parts:
204
256
  for part in candidate.content.parts:
205
257
  if hasattr(part, "text") and part.text:
206
- content_text += part.text
207
- if content_text:
208
- output.append(
209
- {
210
- "role": "assistant",
211
- "content": content_text,
212
- }
213
- )
258
+ content.append({"type": "text", "text": part.text})
259
+ elif hasattr(part, "function_call") and part.function_call:
260
+ function_call = part.function_call
261
+ content.append(
262
+ {
263
+ "type": "function",
264
+ "function": {
265
+ "name": function_call.name,
266
+ "arguments": function_call.args,
267
+ },
268
+ }
269
+ )
270
+
271
+ if content:
272
+ message = {
273
+ "role": "assistant",
274
+ "content": content,
275
+ }
276
+ output.append(message)
277
+
214
278
  elif hasattr(candidate, "text") and candidate.text:
215
279
  output.append(
216
280
  {
217
281
  "role": "assistant",
218
- "content": candidate.text,
282
+ "content": [{"type": "text", "text": candidate.text}],
219
283
  }
220
284
  )
221
285
  elif hasattr(response, "text") and response.text:
222
286
  output.append(
223
287
  {
224
288
  "role": "assistant",
225
- "content": response.text,
289
+ "content": [{"type": "text", "text": response.text}],
226
290
  }
227
291
  )
292
+
228
293
  return output
229
294
 
230
295
 
231
- def format_tool_calls(response, provider: str):
296
+ def extract_available_tool_calls(provider: str, kwargs: Dict[str, Any]):
232
297
  if provider == "anthropic":
233
- if hasattr(response, "content") and response.content:
234
- tool_calls = []
235
-
236
- for content_item in response.content:
237
- if hasattr(content_item, "type") and content_item.type == "tool_use":
238
- tool_calls.append(
239
- {
240
- "type": content_item.type,
241
- "id": content_item.id,
242
- "name": content_item.name,
243
- "input": content_item.input,
244
- }
245
- )
246
-
247
- return tool_calls if tool_calls else None
298
+ if "tools" in kwargs:
299
+ return kwargs["tools"]
300
+
301
+ return None
302
+ elif provider == "gemini":
303
+ if "config" in kwargs and hasattr(kwargs["config"], "tools"):
304
+ return kwargs["config"].tools
305
+
306
+ return None
248
307
  elif provider == "openai":
249
- # Handle both Chat Completions and Responses API
250
- if hasattr(response, "choices") and response.choices:
251
- # Check for tool_calls in message (Chat Completions format)
252
- if (
253
- hasattr(response.choices[0], "message")
254
- and hasattr(response.choices[0].message, "tool_calls")
255
- and response.choices[0].message.tool_calls
256
- ):
257
- return response.choices[0].message.tool_calls
258
-
259
- # Check for tool_calls directly in response (Responses API format)
260
- if (
261
- hasattr(response.choices[0], "tool_calls")
262
- and response.choices[0].tool_calls
263
- ):
264
- return response.choices[0].tool_calls
265
- return None
308
+ if "tools" in kwargs:
309
+ return kwargs["tools"]
310
+
311
+ return None
266
312
 
267
313
 
268
314
  def merge_system_prompt(kwargs: Dict[str, Any], provider: str):
@@ -395,12 +441,10 @@ def call_llm_and_track_usage(
395
441
  **(error_params or {}),
396
442
  }
397
443
 
398
- tool_calls = format_tool_calls(response, provider)
444
+ available_tool_calls = extract_available_tool_calls(provider, kwargs)
399
445
 
400
- if tool_calls:
401
- event_properties["$ai_tools"] = with_privacy_mode(
402
- ph_client, posthog_privacy_mode, tool_calls
403
- )
446
+ if available_tool_calls:
447
+ event_properties["$ai_tools"] = available_tool_calls
404
448
 
405
449
  if (
406
450
  usage.get("cache_read_input_tokens") is not None
@@ -511,11 +555,10 @@ async def call_llm_and_track_usage_async(
511
555
  **(error_params or {}),
512
556
  }
513
557
 
514
- tool_calls = format_tool_calls(response, provider)
515
- if tool_calls:
516
- event_properties["$ai_tools"] = with_privacy_mode(
517
- ph_client, posthog_privacy_mode, tool_calls
518
- )
558
+ available_tool_calls = extract_available_tool_calls(provider, kwargs)
559
+
560
+ if available_tool_calls:
561
+ event_properties["$ai_tools"] = available_tool_calls
519
562
 
520
563
  if (
521
564
  usage.get("cache_read_input_tokens") is not None
@@ -83,6 +83,7 @@ def get_identity_state(passed) -> tuple[str, bool]:
83
83
 
84
84
 
85
85
  def add_context_tags(properties):
86
+ properties = properties or {}
86
87
  current_context = _get_current_context()
87
88
  if current_context:
88
89
  context_tags = current_context.collect_tags()
@@ -395,7 +396,7 @@ class Client(object):
395
396
  def get_flags_decision(
396
397
  self,
397
398
  distinct_id: Optional[ID_TYPES] = None,
398
- groups: Optional[dict] = {},
399
+ groups: Optional[dict] = None,
399
400
  person_properties=None,
400
401
  group_properties=None,
401
402
  disable_geoip=None,
@@ -418,6 +419,9 @@ class Client(object):
418
419
  Category:
419
420
  Feature Flags
420
421
  """
422
+ groups = groups or {}
423
+ person_properties = person_properties or {}
424
+ group_properties = group_properties or {}
421
425
 
422
426
  if distinct_id is None:
423
427
  distinct_id = get_context_distinct_id()
@@ -505,6 +509,7 @@ class Client(object):
505
509
  properties = {**(properties or {}), **system_context()}
506
510
 
507
511
  properties = add_context_tags(properties)
512
+ assert properties is not None # Type hint for mypy
508
513
 
509
514
  (distinct_id, personless) = get_identity_state(distinct_id)
510
515
 
@@ -520,7 +525,7 @@ class Client(object):
520
525
  }
521
526
 
522
527
  if groups:
523
- msg["properties"]["$groups"] = groups
528
+ properties["$groups"] = groups
524
529
 
525
530
  extra_properties: dict[str, Any] = {}
526
531
  feature_variants: Optional[dict[str, Union[bool, str]]] = {}
@@ -575,7 +580,8 @@ class Client(object):
575
580
  extra_properties["$active_feature_flags"] = active_feature_flags
576
581
 
577
582
  if extra_properties:
578
- msg["properties"] = {**extra_properties, **msg["properties"]}
583
+ properties = {**extra_properties, **properties}
584
+ msg["properties"] = properties
579
585
 
580
586
  return self._enqueue(msg, disable_geoip)
581
587
 
@@ -1153,11 +1159,15 @@ class Client(object):
1153
1159
  feature_flag,
1154
1160
  distinct_id,
1155
1161
  *,
1156
- groups={},
1157
- person_properties={},
1158
- group_properties={},
1162
+ groups=None,
1163
+ person_properties=None,
1164
+ group_properties=None,
1159
1165
  warn_on_unknown_groups=True,
1160
1166
  ) -> FlagValue:
1167
+ groups = groups or {}
1168
+ person_properties = person_properties or {}
1169
+ group_properties = group_properties or {}
1170
+
1161
1171
  if feature_flag.get("ensure_experience_continuity", False):
1162
1172
  raise InconclusiveMatchError("Flag has experience continuity enabled")
1163
1173
 
@@ -1203,9 +1213,9 @@ class Client(object):
1203
1213
  key,
1204
1214
  distinct_id,
1205
1215
  *,
1206
- groups={},
1207
- person_properties={},
1208
- group_properties={},
1216
+ groups=None,
1217
+ person_properties=None,
1218
+ group_properties=None,
1209
1219
  only_evaluate_locally=False,
1210
1220
  send_feature_flag_events=True,
1211
1221
  disable_geoip=None,
@@ -1256,9 +1266,9 @@ class Client(object):
1256
1266
  distinct_id: ID_TYPES,
1257
1267
  *,
1258
1268
  override_match_value: Optional[FlagValue] = None,
1259
- groups: Dict[str, str] = {},
1260
- person_properties={},
1261
- group_properties={},
1269
+ groups: Optional[Dict[str, str]] = None,
1270
+ person_properties=None,
1271
+ group_properties=None,
1262
1272
  only_evaluate_locally=False,
1263
1273
  send_feature_flag_events=True,
1264
1274
  disable_geoip=None,
@@ -1268,9 +1278,16 @@ class Client(object):
1268
1278
 
1269
1279
  person_properties, group_properties = (
1270
1280
  self._add_local_person_and_group_properties(
1271
- distinct_id, groups, person_properties, group_properties
1281
+ distinct_id,
1282
+ groups or {},
1283
+ person_properties or {},
1284
+ group_properties or {},
1272
1285
  )
1273
1286
  )
1287
+ # Ensure non-None values for type checking
1288
+ groups = groups or {}
1289
+ person_properties = person_properties or {}
1290
+ group_properties = group_properties or {}
1274
1291
 
1275
1292
  flag_result = None
1276
1293
  flag_details = None
@@ -1354,9 +1371,9 @@ class Client(object):
1354
1371
  key,
1355
1372
  distinct_id,
1356
1373
  *,
1357
- groups={},
1358
- person_properties={},
1359
- group_properties={},
1374
+ groups=None,
1375
+ person_properties=None,
1376
+ group_properties=None,
1360
1377
  only_evaluate_locally=False,
1361
1378
  send_feature_flag_events=True,
1362
1379
  disable_geoip=None,
@@ -1404,9 +1421,9 @@ class Client(object):
1404
1421
  key,
1405
1422
  distinct_id,
1406
1423
  *,
1407
- groups={},
1408
- person_properties={},
1409
- group_properties={},
1424
+ groups=None,
1425
+ person_properties=None,
1426
+ group_properties=None,
1410
1427
  only_evaluate_locally=False,
1411
1428
  send_feature_flag_events=True,
1412
1429
  disable_geoip=None,
@@ -1492,9 +1509,9 @@ class Client(object):
1492
1509
  distinct_id,
1493
1510
  *,
1494
1511
  match_value: Optional[FlagValue] = None,
1495
- groups={},
1496
- person_properties={},
1497
- group_properties={},
1512
+ groups=None,
1513
+ person_properties=None,
1514
+ group_properties=None,
1498
1515
  only_evaluate_locally=False,
1499
1516
  send_feature_flag_events=True,
1500
1517
  disable_geoip=None,
@@ -1662,9 +1679,9 @@ class Client(object):
1662
1679
  self,
1663
1680
  distinct_id,
1664
1681
  *,
1665
- groups={},
1666
- person_properties={},
1667
- group_properties={},
1682
+ groups=None,
1683
+ person_properties=None,
1684
+ group_properties=None,
1668
1685
  only_evaluate_locally=False,
1669
1686
  disable_geoip=None,
1670
1687
  ) -> Optional[dict[str, Union[bool, str]]]:
@@ -1702,9 +1719,9 @@ class Client(object):
1702
1719
  self,
1703
1720
  distinct_id,
1704
1721
  *,
1705
- groups={},
1706
- person_properties={},
1707
- group_properties={},
1722
+ groups=None,
1723
+ person_properties=None,
1724
+ group_properties=None,
1708
1725
  only_evaluate_locally=False,
1709
1726
  disable_geoip=None,
1710
1727
  ) -> FlagsAndPayloads:
@@ -1765,10 +1782,13 @@ class Client(object):
1765
1782
  distinct_id: ID_TYPES,
1766
1783
  *,
1767
1784
  groups: Dict[str, Union[str, int]],
1768
- person_properties={},
1769
- group_properties={},
1785
+ person_properties=None,
1786
+ group_properties=None,
1770
1787
  warn_on_unknown_groups=False,
1771
1788
  ) -> tuple[FlagsAndPayloads, bool]:
1789
+ person_properties = person_properties or {}
1790
+ group_properties = group_properties or {}
1791
+
1772
1792
  if self.feature_flags is None and self.personal_api_key:
1773
1793
  self.load_feature_flags()
1774
1794
 
@@ -647,8 +647,8 @@ class TestClient(unittest.TestCase):
647
647
  timeout=3,
648
648
  distinct_id="distinct_id",
649
649
  groups={},
650
- person_properties=None,
651
- group_properties=None,
650
+ person_properties={},
651
+ group_properties={},
652
652
  geoip_disable=True,
653
653
  )
654
654
 
@@ -711,8 +711,8 @@ class TestClient(unittest.TestCase):
711
711
  timeout=12,
712
712
  distinct_id="distinct_id",
713
713
  groups={},
714
- person_properties=None,
715
- group_properties=None,
714
+ person_properties={},
715
+ group_properties={},
716
716
  geoip_disable=False,
717
717
  )
718
718
 
@@ -1,4 +1,4 @@
1
- VERSION = "6.3.3"
1
+ VERSION = "6.3.4"
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: 6.3.3
3
+ Version: 6.3.4
4
4
  Summary: Integrate PostHog into any python application.
5
5
  Home-page: https://github.com/posthog/posthog-python
6
6
  Author: Posthog
@@ -1,6 +1,6 @@
1
- posthoganalytics/__init__.py,sha256=N_Mq_RDLFbd02HCMWxJPTtcSYj3NWcartohQKa7MNlw,24133
1
+ posthoganalytics/__init__.py,sha256=66HkeJ1fkzbKC2ggl3F164oajFeiGm8v84kJR0Yf5BI,25987
2
2
  posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
3
- posthoganalytics/client.py,sha256=P3fBadb30Gohz41yQAfzqKBo4a78h4IjNb3EpX041tM,67647
3
+ posthoganalytics/client.py,sha256=tbyYFWy7-ctXzQuKNUcXvp45aW2adlAA4mvGQii4tkA,68445
4
4
  posthoganalytics/consumer.py,sha256=CiNbJBdyW9jER3ZYCKbX-JFmEDXlE1lbDy1MSl43-a0,4617
5
5
  posthoganalytics/contexts.py,sha256=LFSFIYpUFWKTBnGMjV9n1aYHWbAzz5zLJGr2qG34PoE,9405
6
6
  posthoganalytics/exception_capture.py,sha256=1VHBfffrXXrkK0PT8iVgKPpj_R1pGAzG5f3Qw0WF79w,1783
@@ -11,9 +11,9 @@ posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  posthoganalytics/request.py,sha256=TaeySYpcvHMf5Ftf5KqqlO0VPJpirKBCRrThlS04Kew,6124
12
12
  posthoganalytics/types.py,sha256=k_IE_tvAE7wBKHthTSPEf4zB-SPuK2y3LDlsGXuU5_8,10093
13
13
  posthoganalytics/utils.py,sha256=-0w-OLcCaoldkbBebPzQyBzLJSo9G9yBOg8NDVz7La8,16088
14
- posthoganalytics/version.py,sha256=TwaNyeWFPCGS2dLL3HiS9owbDI9LX2LeDWSL03vZecg,87
14
+ posthoganalytics/version.py,sha256=vNG5nM1HgvgDhamB_uuh6J2cSrOXYt9hq6ZIbwIUcfo,87
15
15
  posthoganalytics/ai/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- posthoganalytics/ai/utils.py,sha256=-iT0gOf_5Q3E6X20jKGOyJgyarwkml73vaf60bUzRtM,20165
16
+ posthoganalytics/ai/utils.py,sha256=92RlL395wjL5V9FstS8BeebwMtaz6DP6zS9miCNla9M,21106
17
17
  posthoganalytics/ai/anthropic/__init__.py,sha256=fFhDOiRzTXzGQlgnrRDL-4yKC8EYIl8NW4a2QNR6xRU,368
18
18
  posthoganalytics/ai/anthropic/anthropic.py,sha256=P8o-pZ2rbJXDiHO73OWjO7OgboGiEm_wVY4pbvHnUEs,7397
19
19
  posthoganalytics/ai/anthropic/anthropic_async.py,sha256=iAwVlAY6VeW0dGZdMkdfniBTBFUdZZrDMZi-O9vdiuo,7511
@@ -21,16 +21,16 @@ posthoganalytics/ai/anthropic/anthropic_providers.py,sha256=y1_qc8Lbip-YDmpimPGg
21
21
  posthoganalytics/ai/gemini/__init__.py,sha256=bMNBnJ6NO_PCQCwmxKIiw4adFuEQ06hFFBALt-aDW-0,174
22
22
  posthoganalytics/ai/gemini/gemini.py,sha256=oi7VIPJLMEHPqRQwvAGwLjkaF0RZhvloCqOJgsQrmJ8,13285
23
23
  posthoganalytics/ai/langchain/__init__.py,sha256=9CqAwLynTGj3ASAR80C3PmdTdrYGmu99tz0JL-HPFgI,70
24
- posthoganalytics/ai/langchain/callbacks.py,sha256=9WP09msvuq1dWFVsIlDINn1QL_QdX7yJ8foI7uU7FmQ,29519
24
+ posthoganalytics/ai/langchain/callbacks.py,sha256=imnhz4u5uPsqUCqtRymdD-PWNzSuWHiace7g_iHyqTU,29423
25
25
  posthoganalytics/ai/openai/__init__.py,sha256=_flZxkyaDZme9hxJsY31sMlq4nP1dtc75HmNgj-21Kg,197
26
- posthoganalytics/ai/openai/openai.py,sha256=6bC3OxH9TP7EFkCGQRfxfcormVTAwLP4Wfj3ID3RwEc,23431
27
- posthoganalytics/ai/openai/openai_async.py,sha256=0gEhTr-ePiOhS8h1WznQDSz_lJm1aferk5K1ZAMo-K0,23838
26
+ posthoganalytics/ai/openai/openai.py,sha256=Ii0kU8S8hBSRxjTRcnAJAVGGmMDYFy03wtM75MrCtlA,22176
27
+ posthoganalytics/ai/openai/openai_async.py,sha256=F0VYOGVvLYr7TevXENy9vULqiknsCErKbaprswR3aSc,22583
28
28
  posthoganalytics/ai/openai/openai_providers.py,sha256=RPVmj2V0_lAdno_ax5Ul2kwhBA9_rRgAdl_sCqrQc6M,4004
29
29
  posthoganalytics/integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  posthoganalytics/integrations/django.py,sha256=KYtBr7CkiZQynRc2TCWWYHe-J3ie8iSUa42WPshYZdc,6795
31
31
  posthoganalytics/test/__init__.py,sha256=VYgM6xPbJbvS-xhIcDiBRs0MFC9V_jT65uNeerCz_rM,299
32
32
  posthoganalytics/test/test_before_send.py,sha256=A1_UVMewhHAvO39rZDWfS606vG_X-q0KNXvh5DAKiB8,7930
33
- posthoganalytics/test/test_client.py,sha256=PDsEgJ5nFZh75bJ8Vugyc_C5iRtGOU8qWYPYDF8z24c,91606
33
+ posthoganalytics/test/test_client.py,sha256=x66Qly5QQySl9OcUlqviuWuOnWCgf5oLT1Jh6nm8I1E,91598
34
34
  posthoganalytics/test/test_consumer.py,sha256=HGMfU9PzQ5ZAe_R3kHnZNsMvD7jUjHL-gie0isrvMMk,7107
35
35
  posthoganalytics/test/test_contexts.py,sha256=c--hNUIEf6SHQ7H9vdPhU1oLCN0SnD4wDbFr-eLPHDo,7013
36
36
  posthoganalytics/test/test_exception_capture.py,sha256=al37Kg6wjzL_IBCFUUXRvkP6nVrqS6IZRCOKSo29Nh8,1063
@@ -42,8 +42,8 @@ posthoganalytics/test/test_request.py,sha256=Zc0VbkjpVmj8mKokQm9rzdgTr0b1U44vvMY
42
42
  posthoganalytics/test/test_size_limited_dict.py,sha256=-5IQjIEr_-Dql24M0HusdR_XroOMrtgiT0v6ZQCRvzo,774
43
43
  posthoganalytics/test/test_types.py,sha256=bRPHdwVpP7hu7emsplU8UVyzSQptv6PaG5lAoOD_BtM,7595
44
44
  posthoganalytics/test/test_utils.py,sha256=sqUTbfweVcxxFRd3WDMFXqPMyU6DvzOBeAOc68Py9aw,9620
45
- posthoganalytics-6.3.3.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
46
- posthoganalytics-6.3.3.dist-info/METADATA,sha256=N6EyQGMGCozK-Hb73B5dTMLF_m7G-71zb0wiY3E5ytM,6024
47
- posthoganalytics-6.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
- posthoganalytics-6.3.3.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
49
- posthoganalytics-6.3.3.dist-info/RECORD,,
45
+ posthoganalytics-6.3.4.dist-info/licenses/LICENSE,sha256=wGf9JBotDkSygFj43m49oiKlFnpMnn97keiZKF-40vE,2450
46
+ posthoganalytics-6.3.4.dist-info/METADATA,sha256=zaMxVnSIWOBBSi4LIgUZUtfN_jtw-tiBnfJ7uv0pThw,6024
47
+ posthoganalytics-6.3.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
+ posthoganalytics-6.3.4.dist-info/top_level.txt,sha256=8QsNIqIkBh1p2TXvKp0Em9ZLZKwe3uIqCETyW4s1GOE,17
49
+ posthoganalytics-6.3.4.dist-info/RECORD,,