posthoganalytics 6.3.2__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.
- posthoganalytics/__init__.py +74 -31
- posthoganalytics/ai/langchain/callbacks.py +5 -7
- posthoganalytics/ai/openai/openai.py +9 -36
- posthoganalytics/ai/openai/openai_async.py +9 -36
- posthoganalytics/ai/utils.py +136 -93
- posthoganalytics/client.py +53 -33
- posthoganalytics/test/test_client.py +113 -4
- posthoganalytics/types.py +8 -3
- posthoganalytics/version.py +1 -1
- {posthoganalytics-6.3.2.dist-info → posthoganalytics-6.3.4.dist-info}/METADATA +1 -1
- {posthoganalytics-6.3.2.dist-info → posthoganalytics-6.3.4.dist-info}/RECORD +14 -14
- {posthoganalytics-6.3.2.dist-info → posthoganalytics-6.3.4.dist-info}/WHEEL +0 -0
- {posthoganalytics-6.3.2.dist-info → posthoganalytics-6.3.4.dist-info}/licenses/LICENSE +0 -0
- {posthoganalytics-6.3.2.dist-info → posthoganalytics-6.3.4.dist-info}/top_level.txt +0 -0
posthoganalytics/__init__.py
CHANGED
|
@@ -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=
|
|
392
|
-
person_properties=
|
|
393
|
-
group_properties=
|
|
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=
|
|
443
|
-
person_properties=
|
|
444
|
-
group_properties=
|
|
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=
|
|
492
|
-
person_properties=
|
|
493
|
-
group_properties=
|
|
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"] =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
216
|
-
event_properties["$ai_tools"] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
474
|
-
event_properties["$ai_tools"] =
|
|
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
|
-
|
|
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
|
|
217
|
-
event_properties["$ai_tools"] =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
478
|
-
event_properties["$ai_tools"] =
|
|
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
|
posthoganalytics/ai/utils.py
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
+
content.append(
|
|
179
217
|
{
|
|
180
|
-
"
|
|
181
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
296
|
+
def extract_available_tool_calls(provider: str, kwargs: Dict[str, Any]):
|
|
232
297
|
if provider == "anthropic":
|
|
233
|
-
if
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
444
|
+
available_tool_calls = extract_available_tool_calls(provider, kwargs)
|
|
399
445
|
|
|
400
|
-
if
|
|
401
|
-
event_properties["$ai_tools"] =
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
posthoganalytics/client.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
@@ -1285,7 +1302,7 @@ class Client(object):
|
|
|
1285
1302
|
lookup_match_value = override_match_value or flag_value
|
|
1286
1303
|
payload = (
|
|
1287
1304
|
self._compute_payload_locally(key, lookup_match_value)
|
|
1288
|
-
if lookup_match_value
|
|
1305
|
+
if lookup_match_value is not None
|
|
1289
1306
|
else None
|
|
1290
1307
|
)
|
|
1291
1308
|
flag_result = FeatureFlagResult.from_value_and_payload(
|
|
@@ -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,
|
|
@@ -1586,7 +1603,7 @@ class Client(object):
|
|
|
1586
1603
|
f"$feature/{key}": response,
|
|
1587
1604
|
}
|
|
1588
1605
|
|
|
1589
|
-
if payload:
|
|
1606
|
+
if payload is not None:
|
|
1590
1607
|
# if payload is not a string, json serialize it to a string
|
|
1591
1608
|
properties["$feature_flag_payload"] = payload
|
|
1592
1609
|
|
|
@@ -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
|
|
|
@@ -1790,7 +1810,7 @@ class Client(object):
|
|
|
1790
1810
|
matched_payload = self._compute_payload_locally(
|
|
1791
1811
|
flag["key"], flags[flag["key"]]
|
|
1792
1812
|
)
|
|
1793
|
-
if matched_payload:
|
|
1813
|
+
if matched_payload is not None:
|
|
1794
1814
|
payloads[flag["key"]] = matched_payload
|
|
1795
1815
|
except InconclusiveMatchError:
|
|
1796
1816
|
# No need to log this, since it's just telling us to fall back to `/decide`
|
|
@@ -647,8 +647,8 @@ class TestClient(unittest.TestCase):
|
|
|
647
647
|
timeout=3,
|
|
648
648
|
distinct_id="distinct_id",
|
|
649
649
|
groups={},
|
|
650
|
-
person_properties=
|
|
651
|
-
group_properties=
|
|
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=
|
|
715
|
-
group_properties=
|
|
714
|
+
person_properties={},
|
|
715
|
+
group_properties={},
|
|
716
716
|
geoip_disable=False,
|
|
717
717
|
)
|
|
718
718
|
|
|
@@ -2246,3 +2246,112 @@ class TestClient(unittest.TestCase):
|
|
|
2246
2246
|
with self.assertRaises(TypeError) as cm:
|
|
2247
2247
|
client._parse_send_feature_flags(None)
|
|
2248
2248
|
self.assertIn("Invalid type for send_feature_flags", str(cm.exception))
|
|
2249
|
+
|
|
2250
|
+
@mock.patch("posthog.client.batch_post")
|
|
2251
|
+
def test_get_feature_flag_result_with_empty_string_payload(self, patch_batch_post):
|
|
2252
|
+
"""Test that get_feature_flag_result returns a FeatureFlagResult when payload is empty string"""
|
|
2253
|
+
client = Client(
|
|
2254
|
+
FAKE_TEST_API_KEY,
|
|
2255
|
+
personal_api_key="test_personal_api_key",
|
|
2256
|
+
sync_mode=True,
|
|
2257
|
+
)
|
|
2258
|
+
|
|
2259
|
+
# Set up local evaluation with a flag that has empty string payload
|
|
2260
|
+
client.feature_flags = [
|
|
2261
|
+
{
|
|
2262
|
+
"id": 1,
|
|
2263
|
+
"name": "Test flag",
|
|
2264
|
+
"key": "test-flag",
|
|
2265
|
+
"is_simple_flag": False,
|
|
2266
|
+
"active": True,
|
|
2267
|
+
"rollout_percentage": None,
|
|
2268
|
+
"filters": {
|
|
2269
|
+
"groups": [
|
|
2270
|
+
{
|
|
2271
|
+
"properties": [],
|
|
2272
|
+
"rollout_percentage": None,
|
|
2273
|
+
"variant": "empty-variant",
|
|
2274
|
+
}
|
|
2275
|
+
],
|
|
2276
|
+
"multivariate": {
|
|
2277
|
+
"variants": [
|
|
2278
|
+
{
|
|
2279
|
+
"key": "empty-variant",
|
|
2280
|
+
"name": "Empty Variant",
|
|
2281
|
+
"rollout_percentage": 100,
|
|
2282
|
+
}
|
|
2283
|
+
]
|
|
2284
|
+
},
|
|
2285
|
+
"payloads": {
|
|
2286
|
+
"empty-variant": "" # Empty string payload
|
|
2287
|
+
},
|
|
2288
|
+
},
|
|
2289
|
+
}
|
|
2290
|
+
]
|
|
2291
|
+
|
|
2292
|
+
# Test get_feature_flag_result
|
|
2293
|
+
result = client.get_feature_flag_result(
|
|
2294
|
+
"test-flag", "test-user", only_evaluate_locally=True
|
|
2295
|
+
)
|
|
2296
|
+
|
|
2297
|
+
# Should return a FeatureFlagResult, not None
|
|
2298
|
+
self.assertIsNotNone(result)
|
|
2299
|
+
self.assertEqual(result.key, "test-flag")
|
|
2300
|
+
self.assertEqual(result.get_value(), "empty-variant")
|
|
2301
|
+
self.assertEqual(result.payload, "") # Should be empty string, not None
|
|
2302
|
+
|
|
2303
|
+
@mock.patch("posthog.client.batch_post")
|
|
2304
|
+
def test_get_all_flags_and_payloads_with_empty_string(self, patch_batch_post):
|
|
2305
|
+
"""Test that get_all_flags_and_payloads includes flags with empty string payloads"""
|
|
2306
|
+
client = Client(
|
|
2307
|
+
FAKE_TEST_API_KEY,
|
|
2308
|
+
personal_api_key="test_personal_api_key",
|
|
2309
|
+
sync_mode=True,
|
|
2310
|
+
)
|
|
2311
|
+
|
|
2312
|
+
# Set up multiple flags with different payload types
|
|
2313
|
+
client.feature_flags = [
|
|
2314
|
+
{
|
|
2315
|
+
"id": 1,
|
|
2316
|
+
"name": "Flag with empty payload",
|
|
2317
|
+
"key": "empty-payload-flag",
|
|
2318
|
+
"is_simple_flag": False,
|
|
2319
|
+
"active": True,
|
|
2320
|
+
"filters": {
|
|
2321
|
+
"groups": [{"properties": [], "variant": "variant1"}],
|
|
2322
|
+
"multivariate": {
|
|
2323
|
+
"variants": [{"key": "variant1", "rollout_percentage": 100}]
|
|
2324
|
+
},
|
|
2325
|
+
"payloads": {"variant1": ""}, # Empty string
|
|
2326
|
+
},
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
"id": 2,
|
|
2330
|
+
"name": "Flag with normal payload",
|
|
2331
|
+
"key": "normal-payload-flag",
|
|
2332
|
+
"is_simple_flag": False,
|
|
2333
|
+
"active": True,
|
|
2334
|
+
"filters": {
|
|
2335
|
+
"groups": [{"properties": [], "variant": "variant2"}],
|
|
2336
|
+
"multivariate": {
|
|
2337
|
+
"variants": [{"key": "variant2", "rollout_percentage": 100}]
|
|
2338
|
+
},
|
|
2339
|
+
"payloads": {"variant2": "normal payload"},
|
|
2340
|
+
},
|
|
2341
|
+
},
|
|
2342
|
+
]
|
|
2343
|
+
|
|
2344
|
+
result = client.get_all_flags_and_payloads(
|
|
2345
|
+
"test-user", only_evaluate_locally=True
|
|
2346
|
+
)
|
|
2347
|
+
|
|
2348
|
+
# Check that both flags are included
|
|
2349
|
+
self.assertEqual(result["featureFlags"]["empty-payload-flag"], "variant1")
|
|
2350
|
+
self.assertEqual(result["featureFlags"]["normal-payload-flag"], "variant2")
|
|
2351
|
+
|
|
2352
|
+
# Check that empty string payload is included (not filtered out)
|
|
2353
|
+
self.assertIn("empty-payload-flag", result["featureFlagPayloads"])
|
|
2354
|
+
self.assertEqual(result["featureFlagPayloads"]["empty-payload-flag"], "")
|
|
2355
|
+
self.assertEqual(
|
|
2356
|
+
result["featureFlagPayloads"]["normal-payload-flag"], "normal payload"
|
|
2357
|
+
)
|
posthoganalytics/types.py
CHANGED
|
@@ -110,7 +110,7 @@ class FeatureFlag:
|
|
|
110
110
|
variant=variant,
|
|
111
111
|
reason=None,
|
|
112
112
|
metadata=LegacyFlagMetadata(
|
|
113
|
-
payload=payload
|
|
113
|
+
payload=payload,
|
|
114
114
|
),
|
|
115
115
|
)
|
|
116
116
|
|
|
@@ -178,7 +178,9 @@ class FeatureFlagResult:
|
|
|
178
178
|
key=key,
|
|
179
179
|
enabled=enabled,
|
|
180
180
|
variant=variant,
|
|
181
|
-
payload=json.loads(payload)
|
|
181
|
+
payload=json.loads(payload)
|
|
182
|
+
if isinstance(payload, str) and payload
|
|
183
|
+
else payload,
|
|
182
184
|
reason=None,
|
|
183
185
|
)
|
|
184
186
|
|
|
@@ -219,6 +221,7 @@ class FeatureFlagResult:
|
|
|
219
221
|
payload=(
|
|
220
222
|
json.loads(details.metadata.payload)
|
|
221
223
|
if isinstance(details.metadata.payload, str)
|
|
224
|
+
and details.metadata.payload
|
|
222
225
|
else details.metadata.payload
|
|
223
226
|
),
|
|
224
227
|
reason=details.reason.description if details.reason else None,
|
|
@@ -296,5 +299,7 @@ def to_payloads(response: FlagsResponse) -> Optional[dict[str, str]]:
|
|
|
296
299
|
return {
|
|
297
300
|
key: value.metadata.payload
|
|
298
301
|
for key, value in response.get("flags", {}).items()
|
|
299
|
-
if isinstance(value, FeatureFlag)
|
|
302
|
+
if isinstance(value, FeatureFlag)
|
|
303
|
+
and value.enabled
|
|
304
|
+
and value.metadata.payload is not None
|
|
300
305
|
}
|
posthoganalytics/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
posthoganalytics/__init__.py,sha256=
|
|
1
|
+
posthoganalytics/__init__.py,sha256=66HkeJ1fkzbKC2ggl3F164oajFeiGm8v84kJR0Yf5BI,25987
|
|
2
2
|
posthoganalytics/args.py,sha256=iZ2JWeANiAREJKhS-Qls9tIngjJOSfAVR8C4xFT5sHw,3307
|
|
3
|
-
posthoganalytics/client.py,sha256=
|
|
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
|
|
@@ -9,11 +9,11 @@ posthoganalytics/feature_flags.py,sha256=O_kXmw3goB2E9XMBosdPeBAuo9MsnsH8PyNWq95
|
|
|
9
9
|
posthoganalytics/poller.py,sha256=jBz5rfH_kn_bBz7wCB46Fpvso4ttx4uzqIZWvXBCFmQ,595
|
|
10
10
|
posthoganalytics/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
posthoganalytics/request.py,sha256=TaeySYpcvHMf5Ftf5KqqlO0VPJpirKBCRrThlS04Kew,6124
|
|
12
|
-
posthoganalytics/types.py,sha256=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
27
|
-
posthoganalytics/ai/openai/openai_async.py,sha256=
|
|
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=
|
|
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.
|
|
46
|
-
posthoganalytics-6.3.
|
|
47
|
-
posthoganalytics-6.3.
|
|
48
|
-
posthoganalytics-6.3.
|
|
49
|
-
posthoganalytics-6.3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|