Skip to content

[FSSDK-11175] Update: Implement Decision Service methods to handle CMAB #457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
88b4f1e
update: integrate CMAB components into OptimizelyFactory
FarhanAnjum-opti Jun 4, 2025
2563c7b
update: add cmab_service parameter to Optimizely constructor for CMAB…
FarhanAnjum-opti Jun 4, 2025
fac8946
update: add docstring to DefaultCmabService class for improved docume…
FarhanAnjum-opti Jun 4, 2025
f74bc8c
update: implement CMAB support in bucketer and decision service, reve…
FarhanAnjum-opti Jun 13, 2025
6d1f73d
linting fix
FarhanAnjum-opti Jun 13, 2025
91d53b6
update: add cmab_uuid handling in DecisionService and related tests
FarhanAnjum-opti Jun 16, 2025
3eb755f
- updated function bucket_to_entity_id
FarhanAnjum-opti Jun 16, 2025
a5e4993
update: add None parameter to Decision constructor in user context tests
FarhanAnjum-opti Jun 16, 2025
c1cd97a
update: enhance CMAB decision handling and add related tests
FarhanAnjum-opti Jun 16, 2025
fd7c723
update: fix logger message formatting in CMAB experiment tests
FarhanAnjum-opti Jun 16, 2025
ec19c3b
mypy fix
FarhanAnjum-opti Jun 16, 2025
029262d
update: refine traffic allocation type hints and key naming in bucket…
FarhanAnjum-opti Jun 16, 2025
180fdee
update: remove unused import of cast in bucketer.py
FarhanAnjum-opti Jun 16, 2025
cd5ba39
update: fix return type for numeric_metric_value in get_numeric_value…
FarhanAnjum-opti Jun 16, 2025
92a3258
update: specify type hint for numeric_metric_value in get_numeric_val…
FarhanAnjum-opti Jun 16, 2025
fe100cb
update: fix logger reference in DefaultCmabClient initialization and …
FarhanAnjum-opti Jun 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion optimizely/bucketer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
if TYPE_CHECKING:
# prevent circular dependenacy by skipping import at runtime
from .project_config import ProjectConfig
from .entities import Experiment, Variation
from .entities import Experiment, Variation, Group
from .helpers.types import TrafficAllocation


Expand Down Expand Up @@ -164,3 +164,67 @@ def bucket(
decide_reasons.append(message)

return None, decide_reasons

def bucket_to_entity_id(
self,
bucketing_id: str,
experiment: Experiment,
traffic_allocations: list[TrafficAllocation],
group: Optional[Group] = None
) -> tuple[Optional[str], list[str]]:
"""
Buckets the user and returns the entity ID (for CMAB experiments).
Args:
bucketing_id: The bucketing ID string for the user.
experiment: The experiment object (for group/groupPolicy logic if needed).
traffic_allocations: List of traffic allocation dicts (should have 'entity_id' and 'end_of_range' keys).
group: (optional) Group object for mutex group support.

Returns:
Tuple of (entity_id or None, list of decide reasons).
"""
decide_reasons = []

group_id = getattr(experiment, 'groupId', None)
if group_id and group and getattr(group, 'policy', None) == 'random':
bucket_key = bucketing_id + group_id
bucket_val = self._generate_bucket_value(bucket_key)
decide_reasons.append(f'Generated group bucket value {bucket_val} for key "{bucket_key}".')

matched = False
for allocation in group.trafficAllocation:
end_of_range = allocation['endOfRange']
entity_id = allocation['entityId']
if bucket_val < end_of_range:
matched = True
if entity_id != experiment.id:
decide_reasons.append(
f'User not bucketed into experiment "{experiment.id}" (got "{entity_id}").'
)
return None, decide_reasons
decide_reasons.append(
f'User is bucketed into experiment "{experiment.id}" within group "{group_id}".'
)
break
if not matched:
decide_reasons.append(
f'User not bucketed into any experiment in group "{group_id}".'
)
return None, decide_reasons

# Main experiment bucketing
bucket_key = bucketing_id + experiment.id
bucket_val = self._generate_bucket_value(bucket_key)
decide_reasons.append(f'Generated experiment bucket value {bucket_val} for key "{bucket_key}".')

for allocation in traffic_allocations:
end_of_range = allocation['endOfRange']
entity_id = allocation['entityId']
if bucket_val < end_of_range:
decide_reasons.append(
f'User bucketed into entity id "{entity_id}".'
)
return entity_id, decide_reasons

decide_reasons.append('User not bucketed into any entity id.')
return None, decide_reasons
12 changes: 12 additions & 0 deletions optimizely/cmab/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2025, Optimizely
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
12 changes: 12 additions & 0 deletions optimizely/cmab/cmab_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ class CmabCacheValue(TypedDict):


class DefaultCmabService:
"""
DefaultCmabService handles decisioning for Contextual Multi-Armed Bandit (CMAB) experiments,
including caching and filtering user attributes for efficient decision retrieval.

Attributes:
cmab_cache: LRUCache for user CMAB decisions.
cmab_client: Client to fetch decisions from the CMAB backend.
logger: Optional logger.

Methods:
get_decision: Retrieves a CMAB decision with caching and attribute filtering.
"""
def __init__(self, cmab_cache: LRUCache[str, CmabCacheValue],
cmab_client: DefaultCmabClient, logger: Optional[_logging.Logger] = None):
self.cmab_cache = cmab_cache
Expand Down
Loading