34
34
35
35
36
36
class CmabDecisionResult (TypedDict ):
37
+ """
38
+ TypedDict representing the result of a CMAB (Contextual Multi-Armed Bandit) decision.
39
+
40
+ Attributes:
41
+ error (bool): Indicates whether an error occurred during the decision process.
42
+ result (Optional[CmabDecision]): Resulting CmabDecision object if the decision was successful, otherwise None.
43
+ reasons (List[str]): A list of reasons or messages explaining the outcome or any errors encountered.
44
+ """
37
45
error : bool
38
46
result : Optional [CmabDecision ]
39
47
reasons : List [str ]
40
48
41
49
50
+ class VariationResult (TypedDict ):
51
+ """
52
+ TypedDict representing the result of a variation decision process.
53
+
54
+ Attributes:
55
+ cmab_uuid (Optional[str]): The unique identifier for the CMAB experiment, if applicable.
56
+ error (bool): Indicates whether an error occurred during the decision process.
57
+ reasons (List[str]): A list of reasons explaining the outcome or any errors encountered.
58
+ variation (Optional[entities.Variation]): The selected variation entity, or None if no variation was assigned.
59
+ """
60
+ cmab_uuid : Optional [str ]
61
+ error : bool
62
+ reasons : List [str ]
63
+ variation : Optional [entities .Variation ]
64
+
65
+
66
+ class DecisionResult (TypedDict ):
67
+ """
68
+ A TypedDict representing the result of a decision process.
69
+
70
+ Attributes:
71
+ decision (Decision): The decision object containing the outcome of the evaluation.
72
+ error (bool): Indicates whether an error occurred during the decision process.
73
+ reasons (List[str]): A list of reasons explaining the decision or any errors encountered.
74
+ """
75
+ decision : Decision
76
+ error : bool
77
+ reasons : List [str ]
78
+
79
+
42
80
class Decision (NamedTuple ):
43
81
"""Named tuple containing selected experiment, variation, source and cmab_uuid.
44
82
None if no experiment/variation was selected."""
@@ -310,30 +348,38 @@ def get_variation(
310
348
user_profile_tracker : Optional [UserProfileTracker ],
311
349
reasons : list [str ] = [],
312
350
options : Optional [Sequence [str ]] = None
313
- ) -> tuple [Optional [entities .Variation ], list [str ], Optional [str ]]:
314
- """ Top-level function to help determine variation user should be put in.
315
-
316
- First, check if experiment is running.
317
- Second, check if user is forced in a variation.
318
- Third, check if there is a stored decision for the user and return the corresponding variation.
319
- Fourth, figure out if user is in the experiment by evaluating audience conditions if any.
320
- Fifth, bucket the user and return the variation.
351
+ ) -> VariationResult :
352
+ """
353
+ Determines the variation a user should be assigned to for a given experiment.
354
+
355
+ The decision process is as follows:
356
+ 1. Check if the experiment is running.
357
+ 2. Check if the user is forced into a variation via the forced variation map.
358
+ 3. Check if the user is whitelisted into a variation for the experiment.
359
+ 4. If user profile tracking is enabled and not ignored, check for a stored variation.
360
+ 5. Evaluate audience conditions to determine if the user qualifies for the experiment.
361
+ 6. For CMAB experiments:
362
+ a. Check if the user is in the CMAB traffic allocation.
363
+ b. If so, fetch the CMAB decision and assign the corresponding variation and cmab_uuid.
364
+ 7. For non-CMAB experiments, bucket the user into a variation.
365
+ 8. If a variation is assigned, optionally update the user profile.
366
+ 9. Return the assigned variation, decision reasons, and cmab_uuid (if applicable).
321
367
322
368
Args:
323
- project_config: Instance of ProjectConfig.
324
- experiment: Experiment for which user variation needs to be determined.
325
- user_context: contains user id and attributes.
326
- user_profile_tracker: tracker for reading and updating user profile of the user.
327
- reasons: Decision reasons.
328
- options: Decide options.
369
+ project_config: Instance of ProjectConfig.
370
+ experiment: Experiment for which the user's variation needs to be determined.
371
+ user_context: Contains user id and attributes.
372
+ user_profile_tracker: Tracker for reading and updating the user's profile .
373
+ reasons: List of decision reasons.
374
+ options: Decide options.
329
375
330
376
Returns:
331
- Variation user should see. None if user is not in experiment or experiment is not running,
332
- an array of log messages representing decision making
333
- and a cmab_uuid if experiment is cmab-experiment
377
+ A tuple of:
378
+ - The assigned Variation (or None if not assigned).
379
+ - A list of log messages representing decision making.
380
+ - The cmab_uuid if the experiment is a CMAB experiment, otherwise None.
334
381
"""
335
382
user_id = user_context .user_id
336
- cmab_uuid = None
337
383
if options :
338
384
ignore_user_profile = OptimizelyDecideOption .IGNORE_USER_PROFILE_SERVICE in options
339
385
else :
@@ -347,20 +393,35 @@ def get_variation(
347
393
message = f'Experiment "{ experiment .key } " is not running.'
348
394
self .logger .info (message )
349
395
decide_reasons .append (message )
350
- return None , decide_reasons , cmab_uuid
396
+ return {
397
+ 'cmab_uuid' : None ,
398
+ 'error' : False ,
399
+ 'reasons' : decide_reasons ,
400
+ 'variation' : None
401
+ }
351
402
352
403
# Check if the user is forced into a variation
353
404
variation : Optional [entities .Variation ]
354
405
variation , reasons_received = self .get_forced_variation (project_config , experiment .key , user_id )
355
406
decide_reasons += reasons_received
356
407
if variation :
357
- return variation , decide_reasons , cmab_uuid
408
+ return {
409
+ 'cmab_uuid' : None ,
410
+ 'error' : False ,
411
+ 'reasons' : decide_reasons ,
412
+ 'variation' : variation
413
+ }
358
414
359
415
# Check to see if user is white-listed for a certain variation
360
416
variation , reasons_received = self .get_whitelisted_variation (project_config , experiment , user_id )
361
417
decide_reasons += reasons_received
362
418
if variation :
363
- return variation , decide_reasons , cmab_uuid
419
+ return {
420
+ 'cmab_uuid' : None ,
421
+ 'error' : False ,
422
+ 'reasons' : decide_reasons ,
423
+ 'variation' : variation
424
+ }
364
425
365
426
# Check to see if user has a decision available for the given experiment
366
427
if user_profile_tracker is not None and not ignore_user_profile :
@@ -370,7 +431,12 @@ def get_variation(
370
431
f'"{ experiment } " for user "{ user_id } " from user profile.'
371
432
self .logger .info (message )
372
433
decide_reasons .append (message )
373
- return variation , decide_reasons , cmab_uuid
434
+ return {
435
+ 'cmab_uuid' : None ,
436
+ 'error' : False ,
437
+ 'reasons' : decide_reasons ,
438
+ 'variation' : variation
439
+ }
374
440
else :
375
441
self .logger .warning ('User profile has invalid format.' )
376
442
@@ -386,12 +452,21 @@ def get_variation(
386
452
message = f'User "{ user_id } " does not meet conditions to be in experiment "{ experiment .key } ".'
387
453
self .logger .info (message )
388
454
decide_reasons .append (message )
389
- return None , decide_reasons , cmab_uuid
455
+ return {
456
+ 'cmab_uuid' : None ,
457
+ 'error' : False ,
458
+ 'reasons' : decide_reasons ,
459
+ 'variation' : None
460
+ }
390
461
391
462
# Determine bucketing ID to be used
392
463
bucketing_id , bucketing_id_reasons = self ._get_bucketing_id (user_id , user_context .get_user_attributes ())
393
464
decide_reasons += bucketing_id_reasons
465
+ cmab_uuid = None
394
466
467
+ # Check if this is a CMAB experiment
468
+ # If so, handle CMAB-specific traffic allocation and decision logic.
469
+ # Otherwise, proceed with standard bucketing logic for non-CMAB experiments.
395
470
if experiment .cmab :
396
471
CMAB_DUMMY_ENTITY_ID = "$"
397
472
# Build the CMAB-specific traffic allocation
@@ -412,18 +487,27 @@ def get_variation(
412
487
message = f'User "{ user_id } " not in CMAB experiment "{ experiment .key } " due to traffic allocation.'
413
488
self .logger .info (message )
414
489
decide_reasons .append (message )
415
- return None , decide_reasons , cmab_uuid
490
+ return {
491
+ 'cmab_uuid' : None ,
492
+ 'error' : False ,
493
+ 'reasons' : decide_reasons ,
494
+ 'variation' : None
495
+ }
416
496
417
497
# User is in CMAB allocation, proceed to CMAB decision
418
- decision_variation_value = self ._get_decision_for_cmab_experiment (project_config ,
419
- experiment ,
420
- user_context ,
421
- options )
422
- decide_reasons += decision_variation_value .get ('reasons' , [])
423
- cmab_decision = decision_variation_value .get ('result' )
424
- if not cmab_decision or decision_variation_value ['error' ]:
425
- self .logger .error (Errors .CMAB_FETCH_FAILED .format (decide_reasons [0 ]))
426
- return None , decide_reasons , cmab_uuid
498
+ cmab_decision_result = self ._get_decision_for_cmab_experiment (project_config ,
499
+ experiment ,
500
+ user_context ,
501
+ options )
502
+ decide_reasons += cmab_decision_result .get ('reasons' , [])
503
+ cmab_decision = cmab_decision_result .get ('result' )
504
+ if not cmab_decision or cmab_decision_result ['error' ]:
505
+ return {
506
+ 'cmab_uuid' : None ,
507
+ 'error' : True ,
508
+ 'reasons' : decide_reasons ,
509
+ 'variation' : None
510
+ }
427
511
variation_id = cmab_decision ['variation_id' ]
428
512
cmab_uuid = cmab_decision ['cmab_uuid' ]
429
513
variation = project_config .get_variation_from_id (experiment_key = experiment .key , variation_id = variation_id )
@@ -442,11 +526,21 @@ def get_variation(
442
526
user_profile_tracker .update_user_profile (experiment , variation )
443
527
except :
444
528
self .logger .exception (f'Unable to save user profile for user "{ user_id } ".' )
445
- return variation , decide_reasons , cmab_uuid
529
+ return {
530
+ 'cmab_uuid' : cmab_uuid ,
531
+ 'error' : False ,
532
+ 'reasons' : decide_reasons ,
533
+ 'variation' : variation
534
+ }
446
535
message = f'User "{ user_id } " is in no variation.'
447
536
self .logger .info (message )
448
537
decide_reasons .append (message )
449
- return None , decide_reasons , cmab_uuid
538
+ return {
539
+ 'cmab_uuid' : None ,
540
+ 'error' : False ,
541
+ 'reasons' : decide_reasons ,
542
+ 'variation' : None
543
+ }
450
544
451
545
def get_variation_for_rollout (
452
546
self , project_config : ProjectConfig , feature : entities .FeatureFlag , user_context : OptimizelyUserContext
@@ -693,9 +787,12 @@ def get_variations_for_feature_list(
693
787
decision_variation = forced_decision_variation
694
788
cmab_uuid = None
695
789
else :
696
- decision_variation , variation_reasons , cmab_uuid = self .get_variation (
790
+ variation_result = self .get_variation (
697
791
project_config , experiment , user_context , user_profile_tracker , feature_reasons , options
698
792
)
793
+ cmab_uuid = variation_result ['cmab_uuid' ]
794
+ variation_reasons = variation_result ['reasons' ]
795
+ decision_variation = variation_result ['variation' ]
699
796
feature_reasons .extend (variation_reasons )
700
797
701
798
if decision_variation :
0 commit comments