Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Cast provided categorical attributes to strings. Still log and return variation if no actions#59

Merged
aarsilv merged 12 commits intomainfrom
lisa/ff-2573-python-sdk-do-not-evaluate-string-assignment-or-bandit-if
Jul 24, 2024
Merged

Cast provided categorical attributes to strings. Still log and return variation if no actions#59
aarsilv merged 12 commits intomainfrom
lisa/ff-2573-python-sdk-do-not-evaluate-string-assignment-or-bandit-if

Conversation

@lisaah
Copy link
Contributor

@lisaah lisaah commented Jul 8, 2024

Fixes: #FF-2573

Motivation and Context

Tests were modified, catching some edge cases.

  • Handle dynamic casting for categorical attributes
  • We want to return the default value for a bandit flag with no actions Return the assigned variation, but None for action, if there were no actions provided or there was an error evaluating the bandit

Description

  • Converts provided categorical attributes to strings.
  • If bandit action selection is unsuccessful, return the variation if it has been successfully computed
  • Surrounded bandit logging with a try block

How has this been tested?

New unit tests in client_bandit_test.py

Copy link
Collaborator

@typotter typotter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for continuing to tackle the missing features in this SDK, Lisa.

)
return BanditResult(variation, None)

# if no actions are given - a valid use case - return the default value
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to compute the flag string assignment (because it will log the flag assignment) and then return the default value if no actions are passed. If no actions are passed and the flag is not associated with bandits, we can compute and return the string assignment.
This requires reading the bandits field of the UFC response.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was correct at the time, but has since changed. See this Slack thread.

I've implemented the new plan, and have re-requesting a review

@aarsilv aarsilv force-pushed the lisa/ff-2573-python-sdk-do-not-evaluate-string-assignment-or-bandit-if branch from ea4c247 to 4441e50 Compare July 18, 2024 22:52
@aarsilv aarsilv changed the title Return the default value for a bandit flag if no actions. Cast provided categorical attributes to strings. Cast provided categorical attributes to strings. Still log and return variation if no actions Jul 18, 2024
)
if result.action is None:
do_variation(result.variation)
if result.action:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more representative of how we expect users to use our bandits for now (with the two variation template)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

to their context of actions with their contexts.
If supplying an ActionAttributes, it gets converted to an ActionContexts instance.
default (str): The default variation to use if the subject is not part of the bandit.
default (str): The default variation to use if an error is encountered retrieving the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected the comment

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind updating the types in the doc block at line 259 for subject_context? => type should be Union[ContextAttributes, Attributes]

or the assignment if they are not. The BanditResult includes:
- variation (str): The assignment key indicating the subject's variation.
- action (str): The key of the selected action if the subject is part of the bandit.
- action (str | null): The key of the selected action if the subject was assigned one
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returned actions can be None

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should say None instead of null, or better: Optional[str] to keep it Pythonic

action = None
try:
return self.get_bandit_action_detail(
subject_attributes = convert_context_attributes_to_attributes(subject_context)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_bandit_action_detail was ensuring the attributes were passed in with context and then disaggregating them back down to Attributes.

Now that we pass these in before we try to ensure context, we want to do the opposite, which is to make sure they are disaggregated.

else:
raise e

return BanditResult(variation, action)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By having this be the only return statement, if we got a variation but hit an error later on evaluating the bandit, we'll still return the variation (to match our logging of it)

Comment on lines +475 to +477
stringified_categorical_attributes = {
key: str(value) for key, value in subject_context.categorical_attributes.items()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lisaah's original fix, which is to stringify the values of all explicitly passed in categorical attributes so they can be evaluated correctly against the model

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also ensure conversion to string when subject_context is a dictionary?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess in that case we don't know what values should be converted, so bugs can still arise but we have no way to fix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ContextAttributes.from_dict() handles this... bucketing all numeric values numeric attributes and then the string values of strings and bools as categorical attributes



def test_get_bandit_action_flag_without_bandit():
def test_get_bandit_action_flag_has_no_bandit():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have this use a real flag and get assigned a non-default (but non-bandit) variation



def test_get_bandit_action_bandit_does_not_exist():
def test_get_bandit_action_flag_not_exist():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really what the test is testing

assert result == BanditResult("control", None)

@patch.object(BanditEvaluator, 'evaluate_bandit', side_effect=Exception("Mocked Exception"))
def test_get_bandit_action_bandit_error(mock_bandit_evaluator):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests save lives! (part I)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

)

@patch.object(MockAssignmentLogger, 'log_bandit_action', side_effect=Exception("Mocked Exception"))
def test_get_bandit_action_bandit_logger_error(patched_mock_assignment_logger):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests save lives! (part II)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯
feel free to file issues against the other SDKs to ensure we've got protection around logging in each.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯
feel free to file issues against the other SDKs to ensure we've got protection around logging in each.

Copy link

@schmit schmit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the changes, this looks good to me. I don't feel great that we can have 'null' actions after working so hard to removing the null variation in the UFC, but I also don't know how to better handle the empty actions list case.

or the assignment if they are not. The BanditResult includes:
- variation (str): The assignment key indicating the subject's variation.
- action (str): The key of the selected action if the subject is part of the bandit.
- action (str | null): The key of the selected action if the subject was assigned one
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should say None instead of null, or better: Optional[str] to keep it Pythonic

Comment on lines +475 to +477
stringified_categorical_attributes = {
key: str(value) for key, value in subject_context.categorical_attributes.items()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also ensure conversion to string when subject_context is a dictionary?

Comment on lines +475 to +477
stringified_categorical_attributes = {
key: str(value) for key, value in subject_context.categorical_attributes.items()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess in that case we don't know what values should be converted, so bugs can still arise but we have no way to fix.

@schmit schmit removed their assignment Jul 19, 2024
)
if result.action is None:
do_variation(result.variation)
if result.action:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

to their context of actions with their contexts.
If supplying an ActionAttributes, it gets converted to an ActionContexts instance.
default (str): The default variation to use if the subject is not part of the bandit.
default (str): The default variation to use if an error is encountered retrieving the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind updating the types in the doc block at line 259 for subject_context? => type should be Union[ContextAttributes, Attributes]

assert result == BanditResult("control", None)

@patch.object(BanditEvaluator, 'evaluate_bandit', side_effect=Exception("Mocked Exception"))
def test_get_bandit_action_bandit_error(mock_bandit_evaluator):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

)

@patch.object(MockAssignmentLogger, 'log_bandit_action', side_effect=Exception("Mocked Exception"))
def test_get_bandit_action_bandit_logger_error(patched_mock_assignment_logger):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯
feel free to file issues against the other SDKs to ensure we've got protection around logging in each.

)

@patch.object(MockAssignmentLogger, 'log_bandit_action', side_effect=Exception("Mocked Exception"))
def test_get_bandit_action_bandit_logger_error(patched_mock_assignment_logger):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯
feel free to file issues against the other SDKs to ensure we've got protection around logging in each.

@aarsilv aarsilv merged commit 70595d1 into main Jul 24, 2024
@aarsilv aarsilv deleted the lisa/ff-2573-python-sdk-do-not-evaluate-string-assignment-or-bandit-if branch July 24, 2024 15:12
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants