@@ -256,18 +256,20 @@ def get_bandit_action(
256
256
Args:
257
257
flag_key (str): The feature flag key that contains the bandit as one of the variations.
258
258
subject_key (str): The key identifying the subject.
259
- subject_context (ActionContexts | ActionAttributes ): The subject context.
259
+ subject_context (Union[ContextAttributes, Attributes] ): The subject context.
260
260
If supplying an ActionAttributes, it gets converted to an ActionContexts instance
261
- actions (ActionContexts | ActionAttributes): The dictionary that maps action keys
261
+ actions (Union[ ActionContexts, ActionAttributes] ): The dictionary that maps action keys
262
262
to their context of actions with their contexts.
263
263
If supplying an ActionAttributes, it gets converted to an ActionContexts instance.
264
- default (str): The default variation to use if the subject is not part of the bandit.
264
+ default (str): The default variation to use if an error is encountered retrieving the
265
+ assigned variation.
265
266
266
267
Returns:
267
268
BanditResult: The result containing either the bandit action if the subject is part of the bandit,
268
269
or the assignment if they are not. The BanditResult includes:
269
270
- variation (str): The assignment key indicating the subject's variation.
270
- - action (str): The key of the selected action if the subject is part of the bandit.
271
+ - action (Optional[str]): The key of the selected action if the subject was assigned one
272
+ by the bandit.
271
273
272
274
Example:
273
275
result = client.get_bandit_action(
@@ -286,102 +288,116 @@ def get_bandit_action(
286
288
},
287
289
"default"
288
290
)
289
- if result.action is None :
290
- do_variation (result.variation)
291
+ if result.action:
292
+ do_action (result.variation)
291
293
else:
292
- do_action(result.action )
294
+ do_status_quo( )
293
295
"""
296
+ variation = default
297
+ action = None
294
298
try :
295
- return self .get_bandit_action_detail (
299
+ subject_attributes = convert_context_attributes_to_attributes (
300
+ subject_context
301
+ )
302
+
303
+ # first, get experiment assignment
304
+ variation = self .get_string_assignment (
296
305
flag_key ,
297
306
subject_key ,
298
- subject_context ,
299
- actions ,
307
+ subject_attributes ,
300
308
default ,
301
309
)
310
+
311
+ if variation in self .get_bandit_keys ():
312
+ # next, if assigned a bandit, get the selected action
313
+ action = self .evaluate_bandit_action (
314
+ flag_key ,
315
+ variation , # for now, we assume the variation value is always equal to the bandit key
316
+ subject_key ,
317
+ subject_context ,
318
+ actions ,
319
+ )
302
320
except Exception as e :
303
321
if self .__is_graceful_mode :
304
322
logger .error ("[Eppo SDK] Error getting bandit action: " + str (e ))
305
- return BanditResult (default , None )
306
- raise e
323
+ else :
324
+ raise e
325
+
326
+ return BanditResult (variation , action )
307
327
308
- def get_bandit_action_detail (
328
+ def evaluate_bandit_action (
309
329
self ,
310
330
flag_key : str ,
331
+ bandit_key : str ,
311
332
subject_key : str ,
312
333
subject_context : Union [ContextAttributes , Attributes ],
313
334
actions : Union [ActionContexts , ActionAttributes ],
314
- default : str ,
315
- ) -> BanditResult :
316
- subject_attributes = convert_subject_context_to_attributes ( subject_context )
317
- action_contexts = convert_actions_to_action_contexts ( actions )
335
+ ) -> Union [ str , None ]:
336
+ # if no actions are given--a valid use case--return the variation with no action
337
+ if len ( actions ) == 0 :
338
+ return None
318
339
319
- # get experiment assignment
320
- # ignoring type because Dict[str, str] satisfies Dict[str, str | ...] but mypy does not understand
321
- variation = self .get_string_assignment (
322
- flag_key ,
323
- subject_key ,
324
- subject_attributes .categorical_attributes
325
- | subject_attributes .numeric_attributes , # type: ignore
326
- default ,
327
- )
328
-
329
- # if the variation is not the bandit key, then the subject is not allocated in the bandit
330
- if variation not in self .get_bandit_keys ():
331
- return BanditResult (variation , None )
332
-
333
- # for now, assume that the variation is equal to the bandit key
334
- bandit_data = self .__config_requestor .get_bandit_model (variation )
340
+ bandit_data = self .__config_requestor .get_bandit_model (bandit_key )
335
341
336
342
if not bandit_data :
337
343
logger .warning (
338
344
f"[Eppo SDK] No assigned action. Bandit not found for flag: { flag_key } "
339
345
)
340
- return BanditResult (variation , None )
346
+ return None
347
+
348
+ subject_context_attributes = convert_attributes_to_context_attributes (
349
+ subject_context
350
+ )
351
+ action_contexts = convert_actions_to_action_contexts (actions )
341
352
342
353
evaluation = self .__bandit_evaluator .evaluate_bandit (
343
354
flag_key ,
344
355
subject_key ,
345
- subject_attributes ,
356
+ subject_context_attributes ,
346
357
action_contexts ,
347
358
bandit_data .bandit_model_data ,
348
359
)
349
360
350
361
# log bandit action
351
- bandit_event = {
352
- "flagKey" : flag_key ,
353
- "banditKey" : bandit_data .bandit_key ,
354
- "subject" : subject_key ,
355
- "action" : evaluation .action_key if evaluation else None ,
356
- "actionProbability" : evaluation .action_weight if evaluation else None ,
357
- "optimalityGap" : evaluation .optimality_gap if evaluation else None ,
358
- "modelVersion" : bandit_data .bandit_model_version if evaluation else None ,
359
- "timestamp" : datetime .datetime .utcnow ().isoformat (),
360
- "subjectNumericAttributes" : (
361
- subject_attributes .numeric_attributes
362
- if evaluation .subject_attributes
363
- else {}
364
- ),
365
- "subjectCategoricalAttributes" : (
366
- subject_attributes .categorical_attributes
367
- if evaluation .subject_attributes
368
- else {}
369
- ),
370
- "actionNumericAttributes" : (
371
- evaluation .action_attributes .numeric_attributes
372
- if evaluation .action_attributes
373
- else {}
374
- ),
375
- "actionCategoricalAttributes" : (
376
- evaluation .action_attributes .categorical_attributes
377
- if evaluation .action_attributes
378
- else {}
379
- ),
380
- "metaData" : {"sdkLanguage" : "python" , "sdkVersion" : __version__ },
381
- }
382
- self .__assignment_logger .log_bandit_action (bandit_event )
362
+ try :
363
+ bandit_event = {
364
+ "flagKey" : flag_key ,
365
+ "banditKey" : bandit_data .bandit_key ,
366
+ "subject" : subject_key ,
367
+ "action" : evaluation .action_key if evaluation else None ,
368
+ "actionProbability" : evaluation .action_weight if evaluation else None ,
369
+ "optimalityGap" : evaluation .optimality_gap if evaluation else None ,
370
+ "modelVersion" : (
371
+ bandit_data .bandit_model_version if evaluation else None
372
+ ),
373
+ "timestamp" : datetime .datetime .utcnow ().isoformat (),
374
+ "subjectNumericAttributes" : (
375
+ subject_context_attributes .numeric_attributes
376
+ if evaluation .subject_attributes
377
+ else {}
378
+ ),
379
+ "subjectCategoricalAttributes" : (
380
+ subject_context_attributes .categorical_attributes
381
+ if evaluation .subject_attributes
382
+ else {}
383
+ ),
384
+ "actionNumericAttributes" : (
385
+ evaluation .action_attributes .numeric_attributes
386
+ if evaluation .action_attributes
387
+ else {}
388
+ ),
389
+ "actionCategoricalAttributes" : (
390
+ evaluation .action_attributes .categorical_attributes
391
+ if evaluation .action_attributes
392
+ else {}
393
+ ),
394
+ "metaData" : {"sdkLanguage" : "python" , "sdkVersion" : __version__ },
395
+ }
396
+ self .__assignment_logger .log_bandit_action (bandit_event )
397
+ except Exception as e :
398
+ logger .warn ("[Eppo SDK] Error logging bandit event: " + str (e ))
383
399
384
- return BanditResult ( variation , evaluation .action_key if evaluation else None )
400
+ return evaluation .action_key
385
401
386
402
def get_flag_keys (self ):
387
403
"""
@@ -406,6 +422,9 @@ def get_bandit_keys(self):
406
422
"""
407
423
return self .__config_requestor .get_bandit_keys ()
408
424
425
+ def set_is_graceful_mode (self , is_graceful_mode : bool ):
426
+ self .__is_graceful_mode = is_graceful_mode
427
+
409
428
def is_initialized (self ):
410
429
"""
411
430
Returns True if the client has successfully initialized
@@ -443,18 +462,33 @@ def check_value_type_match(
443
462
return False
444
463
445
464
446
- def convert_subject_context_to_attributes (
465
+ def convert_context_attributes_to_attributes (
466
+ subject_context : Union [ContextAttributes , Attributes ]
467
+ ) -> Attributes :
468
+ if isinstance (subject_context , dict ):
469
+ return subject_context
470
+
471
+ # ignoring type because Dict[str, str] satisfies Dict[str, str | ...] but mypy does not understand
472
+ return subject_context .numeric_attributes | subject_context .categorical_attributes # type: ignore
473
+
474
+
475
+ def convert_attributes_to_context_attributes (
447
476
subject_context : Union [ContextAttributes , Attributes ]
448
477
) -> ContextAttributes :
449
478
if isinstance (subject_context , dict ):
450
479
return ContextAttributes .from_dict (subject_context )
451
- return subject_context
480
+
481
+ stringified_categorical_attributes = {
482
+ key : str (value ) for key , value in subject_context .categorical_attributes .items ()
483
+ }
484
+
485
+ return ContextAttributes (
486
+ numeric_attributes = subject_context .numeric_attributes ,
487
+ categorical_attributes = stringified_categorical_attributes ,
488
+ )
452
489
453
490
454
491
def convert_actions_to_action_contexts (
455
492
actions : Union [ActionContexts , ActionAttributes ]
456
493
) -> ActionContexts :
457
- return {
458
- k : ContextAttributes .from_dict (v ) if isinstance (v , dict ) else v
459
- for k , v in actions .items ()
460
- }
494
+ return {k : convert_attributes_to_context_attributes (v ) for k , v in actions .items ()}
0 commit comments