3434
3535
3636class 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+ """
3745 error : bool
3846 result : Optional [CmabDecision ]
3947 reasons : List [str ]
4048
4149
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+
4280class Decision (NamedTuple ):
4381 """Named tuple containing selected experiment, variation, source and cmab_uuid.
4482 None if no experiment/variation was selected."""
@@ -310,30 +348,38 @@ def get_variation(
310348 user_profile_tracker : Optional [UserProfileTracker ],
311349 reasons : list [str ] = [],
312350 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).
321367
322368 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.
329375
330376 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.
334381 """
335382 user_id = user_context .user_id
336- cmab_uuid = None
337383 if options :
338384 ignore_user_profile = OptimizelyDecideOption .IGNORE_USER_PROFILE_SERVICE in options
339385 else :
@@ -347,20 +393,35 @@ def get_variation(
347393 message = f'Experiment "{ experiment .key } " is not running.'
348394 self .logger .info (message )
349395 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+ }
351402
352403 # Check if the user is forced into a variation
353404 variation : Optional [entities .Variation ]
354405 variation , reasons_received = self .get_forced_variation (project_config , experiment .key , user_id )
355406 decide_reasons += reasons_received
356407 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+ }
358414
359415 # Check to see if user is white-listed for a certain variation
360416 variation , reasons_received = self .get_whitelisted_variation (project_config , experiment , user_id )
361417 decide_reasons += reasons_received
362418 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+ }
364425
365426 # Check to see if user has a decision available for the given experiment
366427 if user_profile_tracker is not None and not ignore_user_profile :
@@ -370,7 +431,12 @@ def get_variation(
370431 f'"{ experiment } " for user "{ user_id } " from user profile.'
371432 self .logger .info (message )
372433 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+ }
374440 else :
375441 self .logger .warning ('User profile has invalid format.' )
376442
@@ -386,12 +452,21 @@ def get_variation(
386452 message = f'User "{ user_id } " does not meet conditions to be in experiment "{ experiment .key } ".'
387453 self .logger .info (message )
388454 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+ }
390461
391462 # Determine bucketing ID to be used
392463 bucketing_id , bucketing_id_reasons = self ._get_bucketing_id (user_id , user_context .get_user_attributes ())
393464 decide_reasons += bucketing_id_reasons
465+ cmab_uuid = None
394466
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.
395470 if experiment .cmab :
396471 CMAB_DUMMY_ENTITY_ID = "$"
397472 # Build the CMAB-specific traffic allocation
@@ -412,18 +487,27 @@ def get_variation(
412487 message = f'User "{ user_id } " not in CMAB experiment "{ experiment .key } " due to traffic allocation.'
413488 self .logger .info (message )
414489 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+ }
416496
417497 # 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+ }
427511 variation_id = cmab_decision ['variation_id' ]
428512 cmab_uuid = cmab_decision ['cmab_uuid' ]
429513 variation = project_config .get_variation_from_id (experiment_key = experiment .key , variation_id = variation_id )
@@ -442,11 +526,21 @@ def get_variation(
442526 user_profile_tracker .update_user_profile (experiment , variation )
443527 except :
444528 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+ }
446535 message = f'User "{ user_id } " is in no variation.'
447536 self .logger .info (message )
448537 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+ }
450544
451545 def get_variation_for_rollout (
452546 self , project_config : ProjectConfig , feature : entities .FeatureFlag , user_context : OptimizelyUserContext
@@ -693,9 +787,12 @@ def get_variations_for_feature_list(
693787 decision_variation = forced_decision_variation
694788 cmab_uuid = None
695789 else :
696- decision_variation , variation_reasons , cmab_uuid = self .get_variation (
790+ variation_result = self .get_variation (
697791 project_config , experiment , user_context , user_profile_tracker , feature_reasons , options
698792 )
793+ cmab_uuid = variation_result ['cmab_uuid' ]
794+ variation_reasons = variation_result ['reasons' ]
795+ decision_variation = variation_result ['variation' ]
699796 feature_reasons .extend (variation_reasons )
700797
701798 if decision_variation :
0 commit comments