Esempio n. 1
0
def experiment_outcome():
    """ Tracks an outcome/conversion for an experiment """
    if request.content_type.startswith('application/json'):
        content = request.json
        app.logger.info(content)

        correlation_id = content.get('correlationId')
    else:
        correlation_id = request.form.get('correlationId')

    if not correlation_id:
        raise BadRequest('correlationId is required')

    exp_manager = ExperimentManager()

    try:
        experiment = exp_manager.get_by_correlation_id(correlation_id)
        if not experiment:
            return jsonify({ 'status_code': 404, 'message': 'Experiment not found' }), 404

        experiment.track_conversion(correlation_id, get_timestamp_from_request())

        return jsonify(success=True)

    except Exception as e:
        app.logger.exception('Unexpected error logging outcome', e)
        raise BadRequest(message='Unhandled error', status_code=500)
Esempio n. 2
0
def experiment_outcome():
    """ Tracks an outcome/conversion for an experiment """
    if request.content_type.startswith('application/json'):
        content = request.json
        app.logger.info(content)

        correlation_id = content.get('correlationId')
    else:
        correlation_id = request.form.get('correlationId')

    if not correlation_id:
        raise BadRequest('correlationId is required')

    correlation_bits = correlation_id.split('-')
    if len(correlation_bits) != 4:
        raise BadRequest('correlationId is invalid')

    exp_manager = ExperimentManager()
    if not exp_manager.is_configured():
        raise BadRequest('Experiments have not been configured')

    try:
        experiment = exp_manager.get_by_id(correlation_bits[0])
        if not experiment:
            return jsonify({
                'status_code': 404,
                'message': 'Experiment not found'
            }), 404

        user_id = correlation_bits[1]
        variation_index = int(correlation_bits[2])
        result_rank = int(correlation_bits[3])
        experiment.track_conversion(user_id=user_id,
                                    variation_index=variation_index,
                                    result_rank=result_rank)

        return jsonify(success=True)

    except Exception as e:
        app.logger.exception('Unexpected error logging outcome', e)
        raise BadRequest(message='Unhandled error', status_code=500)
Esempio n. 3
0
def get_top_n(
    user_id,
    items,
    feature,
    top_n,
    default_campaign_arn_param_name='retaildemostore-personalized-ranking-campaign-arn'
):
    """
    Gets Top N items using provided campaign.
    Or delegates to experiment manager if there is an active experiment.

    Args:
        user_id (int): User to get the topN for
        items (list[dict]): e.g. [{"itemId":"33", "url":"path_to_product33"},
                                  {"itemId":"22", "url":"path_to_product22"}]
        feature: Used to lookup the currently active experiment.
        top_n (int): Only return the top N ranked if not None.
        default_campaign_arn_param_name: Change this to use a different campaign.

    Returns:
        Items as passed in, but truncated according to picker - also might have experimentation metadata added.
    """

    app.logger.info(f"Items given for top-n: {items}")

    # Extract item IDs from items supplied by caller. Note that unranked items
    # can be specified as a list of objects with just an 'itemId' key or as a
    # list of fully defined items/products (i.e. with an 'id' key).
    item_map = {}
    unranked_items = []
    for item in items:
        item_id = item.get('itemId') if item.get('itemId') else item.get('id')
        item_map[item_id] = item
        unranked_items.append(item_id)

    app.logger.info(f"Pre-selection items: {unranked_items}")

    resp_headers = {}
    experiment = None
    exp_manager = None

    # Get active experiment if one is setup for feature.
    if feature:
        exp_manager = ExperimentManager()
        experiment = exp_manager.get_active(feature)

    if experiment:
        app.logger.info('Using experiment: ' + experiment.name)

        # Get ranked items from experiment.
        tracker = exp_manager.default_tracker()

        topn_items = experiment.get_items(user_id=user_id,
                                          item_list=unranked_items,
                                          tracker=tracker,
                                          num_results=top_n)

        app.logger.debug(
            f"Experiment ranking resolver gave us this ranking: {topn_items}")

        resp_headers['X-Experiment-Name'] = experiment.name
        resp_headers['X-Experiment-Type'] = experiment.type
        resp_headers['X-Experiment-Id'] = experiment.id
    else:
        # Fallback to default behavior of checking for campaign ARN parameter and
        # then the default product resolver.
        values = get_parameter_values(
            [default_campaign_arn_param_name, filter_purchased_param_name])
        app.logger.info(f'Falling back to Personalize: {values}')

        campaign_arn = values[0]
        filter_arn = values[1]

        if campaign_arn:
            resolver = PersonalizeContextComparePickResolver(
                campaign_arn=campaign_arn,
                filter_arn=filter_arn,
                with_context={'Discount': 'Yes'},
                without_context={})
            resp_headers['X-Personalize-Recipe'] = get_recipe(campaign_arn)
        else:
            app.logger.info(f'Falling back to No-op: {values}')
            resolver = RandomPickResolver()

        topn_items = resolver.get_items(user_id=user_id,
                                        product_list=unranked_items,
                                        num_results=top_n)

    logger.info(f"Sorted items: returned from resolver: {topn_items}")

    response_items = []

    for top_item in topn_items:
        # Unlike with /recommendations and /related we are not hitting the products API to get product info back
        # The caller may have left that info in there so in case they have we want to leave it in.
        item_id = top_item['itemId']
        item = item_map[item_id]

        if 'experiment' in top_item:

            item['experiment'] = top_item['experiment']

            if 'url' in item:
                # Append the experiment correlation ID to the product URL so it gets tracked if used by client.
                product_url = item.get('url')
                if '?' in product_url:
                    product_url += '&'
                else:
                    product_url += '?'

                product_url += 'exp=' + top_item['experiment']['correlationId']

                item['url'] = product_url

        response_items.append(item)

    logger.info(f"Top-N response: with details added back in: {topn_items}")

    return response_items, resp_headers
Esempio n. 4
0
def get_ranking(
        user_id,
        items,
        feature,
        default_campaign_arn_param_name='retaildemostore-personalized-ranking-campaign-arn',
        top_n=None,
        context=None):
    """
    Re-ranks a list of items using personalized reranking.
    Or delegates to experiment manager if there is an active experiment.

    Args:
        user_id (int):
        items (list[dict]): e.g. [{"itemId":"33", "url":"path_to_product33"},
                                  {"itemId":"22", "url":"path_to_product22"}]
        feature: Used to lookup the currently active experiment.
        default_campaign_arn_param_name: For discounts this would be different.
        top_n (Optional[int]): Only return the top N ranked if not None.
        context (Optional[dict]): If available, passed to the reranking Personalization recipe.

    Returns:
        Items as passed in, but ordered according to reranker - also might have experimentation metadata added.
    """

    app.logger.info(f"Items given for ranking: {items}")

    # Extract item IDs from items supplied by caller. Note that unranked items
    # can be specified as a list of objects with just an 'itemId' key or as a
    # list of fully defined items/products (i.e. with an 'id' key).
    item_map = {}
    unranked_items = []
    for item in items:
        item_id = item.get('itemId') if item.get('itemId') else item.get('id')
        item_map[item_id] = item
        unranked_items.append(item_id)

    app.logger.info(f"Unranked items: {unranked_items}")

    resp_headers = {}
    experiment = None
    exp_manager = None

    # Get active experiment if one is setup for feature.
    if feature:
        exp_manager = ExperimentManager()
        experiment = exp_manager.get_active(feature)

    if experiment:
        app.logger.info('Using experiment: ' + experiment.name)

        # Get ranked items from experiment.
        tracker = exp_manager.default_tracker()

        ranked_items = experiment.get_items(user_id=user_id,
                                            item_list=unranked_items,
                                            tracker=tracker,
                                            context=context)

        app.logger.debug(
            f"Experiment ranking resolver gave us this ranking: {ranked_items}"
        )

        resp_headers['X-Experiment-Name'] = experiment.name
        resp_headers['X-Experiment-Type'] = experiment.type
        resp_headers['X-Experiment-Id'] = experiment.id
    else:
        # Fallback to default behavior of checking for campaign ARN parameter and
        # then the default product resolver.
        values = get_parameter_values(
            [default_campaign_arn_param_name, filter_purchased_param_name])
        app.logger.info(f'Falling back to Personalize: {values}')

        campaign_arn = values[0]
        filter_arn = values[1]

        if campaign_arn:
            resolver = PersonalizeRankingResolver(campaign_arn=campaign_arn,
                                                  filter_arn=filter_arn)
            resp_headers['X-Personalize-Recipe'] = get_recipe(campaign_arn)
        else:
            app.logger.info(f'Falling back to No-op: {values}')
            resolver = RankingProductsNoOpResolver()

        ranked_items = resolver.get_items(user_id=user_id,
                                          product_list=unranked_items,
                                          context=context)

    response_items = []
    if top_n is not None:
        # We may not want to return them all - for example in a "pick the top N" scenario.
        ranked_items = ranked_items[:top_n]

    for ranked_item in ranked_items:
        # Unlike with /recommendations and /related we are not hitting the products API to get product info back
        # The caller may have left that info in there so in case they have we want to leave it in.
        item = item_map.get(ranked_item.get('itemId'))

        if 'experiment' in ranked_item:

            item['experiment'] = ranked_item['experiment']

            if 'url' in item:
                # Append the experiment correlation ID to the product URL so it gets tracked if used by client.
                product_url = item.get('url')
                if '?' in product_url:
                    product_url += '&'
                else:
                    product_url += '?'

                product_url += 'exp=' + ranked_item['experiment'][
                    'correlationId']

                item['url'] = product_url

        response_items.append(item)

    return response_items, resp_headers
Esempio n. 5
0
def get_products(feature,
                 user_id,
                 current_item_id,
                 num_results,
                 campaign_arn_param_name,
                 user_reqd_for_campaign=False,
                 fully_qualify_image_urls=False):
    """ Returns products given a UI feature, user, item/product.

    If a feature name is provided and there is an active experiment for the 
    feature, the experiment will be used to retrieve products. Otherwise, 
    the default behavior will be used which will look to see if an Amazon Personalize 
    campaign is available. If not, the Product service will be called to get products 
    from the same category as the current product.
    """

    # Check environment for host and port first in case we're running in a local Docker container (dev mode)
    products_service_host = os.environ.get('PRODUCT_SERVICE_HOST')
    products_service_port = os.environ.get('PRODUCT_SERVICE_PORT', 80)

    if not products_service_host:
        # Get product service instance. We'll need it rehydrate product info for recommendations.
        response = servicediscovery.discover_instances(
            NamespaceName='retaildemostore.local',
            ServiceName='products',
            MaxResults=1,
            HealthStatus='HEALTHY')

        products_service_host = response['Instances'][0]['Attributes'][
            'AWS_INSTANCE_IPV4']

    items = []
    resp_headers = {}
    experiment = None
    exp_manager = None

    # Get active experiment if one is setup for feature and we have a user.
    if feature and user_id:
        exp_manager = ExperimentManager()
        experiment = exp_manager.get_active(feature)

    if experiment:
        # Get items from experiment.
        tracker = exp_manager.default_tracker()

        items = experiment.get_items(user_id=user_id,
                                     current_item_id=current_item_id,
                                     num_results=num_results,
                                     tracker=tracker)

        resp_headers['X-Experiment-Name'] = experiment.name
        resp_headers['X-Experiment-Type'] = experiment.type
        resp_headers['X-Experiment-Id'] = experiment.id
    else:
        # Fallback to default behavior of checking for campaign ARN parameter and
        # then the default product resolver.
        values = get_parameter_values(
            [campaign_arn_param_name, filter_purchased_param_name])

        campaign_arn = values[0]
        filter_arn = values[1]

        if campaign_arn and (user_id or not user_reqd_for_campaign):
            resolver = PersonalizeRecommendationsResolver(
                campaign_arn=campaign_arn, filter_arn=filter_arn)

            items = resolver.get_items(user_id=user_id,
                                       product_id=current_item_id,
                                       num_results=num_results)

            resp_headers['X-Personalize-Recipe'] = get_recipe(campaign_arn)
        else:
            resolver = DefaultProductResolver(
                products_service_host=products_service_host,
                products_service_port=products_service_port)

            items = resolver.get_items(product_id=current_item_id,
                                       num_results=num_results)

    for item in items:
        itemId = item['itemId']
        url = f'http://{products_service_host}:{products_service_port}/products/id/{itemId}?fullyQualifyImageUrls={fully_qualify_image_urls}'
        app.logger.debug(f"Asking for product info from {url}")
        response = requests.get(url)
        app.logger.debug(f"Got product info: {response}")

        if response.ok:
            product = response.json()

            if 'experiment' in item and 'url' in product:
                # Append the experiment correlation ID to the product URL so it gets tracked if used by client.
                product_url = product.get('url')
                if '?' in product_url:
                    product_url += '&'
                else:
                    product_url += '?'

                product_url += 'exp=' + item['experiment']['correlationId']

                product['url'] = product_url

            item.update({'product': product})

        item.pop('itemId')

    resp = Response(json.dumps(items, cls=CompatEncoder),
                    content_type='application/json',
                    headers=resp_headers)
    return resp
Esempio n. 6
0
def rerank():
    """ Re-ranks a list of items using personalized reranking """
    if request.method == 'POST':
        try:
            content = request.json
            app.logger.info(content)

            user_id = content.get('userID')
            if not user_id:
                raise BadRequest('userID is required')

            items = content.get('items')
            if not items:
                raise BadRequest('items is required')

            # Determine name of feature where reranked items are being displayed
            feature = request.args.get('feature')

            app.logger.info(items)

            # Extract item IDs from items supplied by caller. Note that unranked items
            # can be specified as a list of objects with just an 'itemId' key or as a
            # list of fully defined items/products (i.e. with an 'id' key).
            item_map = {}
            unranked_items = []
            for item in items:
                item_id = item.get('itemId') if item.get(
                    'itemId') else item.get('id')
                item_map[item_id] = item
                unranked_items.append(item_id)

            app.logger.info(unranked_items)

            ranked_items = []
            resp_headers = {}
            experiment = None

            # Get active experiment if one is setup for feature.
            if feature:
                exp_manager = ExperimentManager()
                experiment = exp_manager.get_active(feature)

            if experiment:
                app.logger.info('Using experiment: ' + experiment.name)

                # Get ranked items from experiment.
                tracker = exp_manager.default_tracker()

                ranked_items = experiment.get_items(user_id=user_id,
                                                    item_list=unranked_items,
                                                    tracker=tracker)

                resp_headers['X-Experiment-Name'] = experiment.name
                resp_headers['X-Experiment-Type'] = experiment.type
                resp_headers['X-Experiment-Id'] = experiment.id
            else:
                # Fallback to default behavior of checking for campaign ARN parameter and
                # then the default product resolver.
                values = get_parameter_values([
                    'retaildemostore-personalized-ranking-campaign-arn',
                    filter_purchased_param_name
                ])

                campaign_arn = values[0]
                filter_arn = values[1]

                if campaign_arn:
                    resolver = PersonalizeRankingResolver(
                        campaign_arn=campaign_arn, filter_arn=filter_arn)
                    resp_headers['X-Personalize-Recipe'] = get_recipe(
                        campaign_arn)
                else:
                    resolver = RankingProductsNoOpResolver()

                ranked_items = resolver.get_items(user_id=user_id,
                                                  product_list=unranked_items)

            response_items = []
            for ranked_item in ranked_items:
                item = item_map.get(ranked_item.get('itemId'))

                if 'experiment' in ranked_item and 'url' in item:
                    # Append the experiment correlation ID to the product URL so it gets tracked if used by client.
                    product_url = item.get('url')
                    if '?' in product_url:
                        product_url += '&'
                    else:
                        product_url += '?'

                    product_url += 'exp=' + ranked_item['experiment'][
                        'correlationId']

                    item['url'] = product_url

                response_items.append(item)

            resp = Response(json.dumps(response_items, cls=CompatEncoder),
                            content_type='application/json',
                            headers=resp_headers)
            return resp

        except Exception as e:
            app.logger.exception('Unexpected error reranking items', e)
            return json.dumps(items)

    if request.method == 'GET':
        app.logger.info("Request Received, Processing")
Esempio n. 7
0
def rerank():
    """ Re-ranks a list of items using personalized reranking """
    if request.method == 'POST':
        try:
            content = request.json
            app.logger.info(content)

            user_id = content.get('userID')
            if not user_id:
                raise BadRequest('userID is required')

            items = content.get('items')
            if not items:
                raise BadRequest('items is required')

            # Determine name of feature where reranked items are being displayed
            feature = request.args.get('feature')

            app.logger.info(items)

            # Extract item IDs from items supplied by caller. Note that unranked items 
            # can be specified as a list of objects with just an 'itemId' key or as a 
            # list of fully defined items/products (i.e. with an 'id' key).
            item_map = {}
            unranked_items = []
            for item in items:
                item_id = item.get('itemId') if item.get('itemId') else item.get('id')
                item_map[item_id] = item
                unranked_items.append(item_id)

            app.logger.info(unranked_items)

            ranked_items = []
            experiment = None

            # Get active experiment if one is setup for feature and we have a user.
            if feature and user_id:
                exp_manager = ExperimentManager()
                experiment = exp_manager.get_active(feature)

            if experiment:
                # Get ranked items from experiment.
                tracker = exp_manager.default_tracker()

                ranked_items = experiment.get_items(
                    user_id = user_id, 
                    item_list = unranked_items,
                    tracker = tracker
                )
            else:
                # No experiment so check if there's a ranking campaign configured.
                response = ssm.get_parameter(
                    Name='retaildemostore-personalized-ranking-campaign-arn'
                )

                if response['Parameter']['Value'] != 'NONE':
                    resolver = PersonalizeRankingResolver(campaign_arn = response['Parameter']['Value'])
                else:
                    resolver = RankingProductsNoOpResolver()

                ranked_items = resolver.get_items(
                    user_id = user_id, 
                    product_list = unranked_items
                )

            response_items = []
            for ranked_item in ranked_items:
                item = item_map.get(ranked_item.get('itemId'))

                if 'experiment' in ranked_item and 'url' in item:
                    # Append the experiment correlation ID to the product URL so it gets tracked if used by client.
                    product_url = item.get('url')
                    if '?' in product_url:
                        product_url += '&'
                    else:
                        product_url += '?'

                    product_url += 'exp=' + ranked_item['experiment']['correlationId']

                    item['url'] = product_url

                response_items.append(item)

            return json.dumps(response_items)
    
        except Exception as e:
            app.logger.exception('Unexpected error reranking items', e)
            return json.dumps(items) 

    if request.method == 'GET':
        app.logger.info("Request Received, Processing")
Esempio n. 8
0
def get_products(feature, user_id, current_item_id, num_results, default_inference_arn_param_name,
                 default_filter_arn_param_name, filter_values=None, user_reqd_for_inference=False, fully_qualify_image_urls=False,
                 ):
    """ Returns products given a UI feature, user, item/product.

    If a feature name is provided and there is an active experiment for the
    feature, the experiment will be used to retrieve products. Otherwise,
    the default behavior will be used which will look to see if an Amazon Personalize
    campaign/recommender is available. If not, the Product service will be called to get products
    from the same category as the current product.
    Args:
        feature: Used to track different experiments - different experiments pertain to different features
        user_id: If supplied we are looking at user personalization
        current_item_id: Or maybe we are looking at related items
        num_results: Num to return
        default_inference_arn_param_name: If no experiment active, use this SSM parameters to get recommender Arn
        default_filter_arn_param_name: If no experiment active, use this SSM parameter to get filter Arn, if exists
        filter_values: Values to pass at inference for the filter
        user_reqd_for_inference: Require a user ID to use Personalze - otherwise default
        fully_qualify_image_urls: Fully qualify image URLs n here
    Returns:
        A prepared HTTP response object.
    """

    items = []
    resp_headers = {}
    experiment = None
    exp_manager = None

    # Get active experiment if one is setup for feature and we have a user.
    if feature and user_id:
        exp_manager = ExperimentManager()
        experiment = exp_manager.get_active(feature, user_id)

    if experiment:
        # Get items from experiment.
        tracker = exp_manager.default_tracker()

        items = experiment.get_items(
            user_id = user_id,
            current_item_id = current_item_id,
            num_results = num_results,
            tracker = tracker,
            filter_values = filter_values,
            timestamp = get_timestamp_from_request()
        )

        resp_headers['X-Experiment-Name'] = experiment.name
        resp_headers['X-Experiment-Type'] = experiment.type
        resp_headers['X-Experiment-Id'] = experiment.id
    else:
        # Fallback to default behavior of checking for campaign/recommender ARN parameter and
        # then the default product resolver.
        values = get_parameter_values([default_inference_arn_param_name, default_filter_arn_param_name])

        inference_arn = values[0]
        filter_arn = values[1]

        if inference_arn and (user_id or not user_reqd_for_inference):

            logger.info(f"get_products: Supplied campaign/recommender: {inference_arn} (from {default_inference_arn_param_name}) Supplied filter: {filter_arn} (from {default_filter_arn_param_name}) Supplied user: {user_id}")

            resolver = PersonalizeRecommendationsResolver(inference_arn = inference_arn, filter_arn = filter_arn)

            items = resolver.get_items(
                user_id = user_id,
                product_id = current_item_id,
                num_results = num_results,
                filter_values = filter_values
            )

            resp_headers['X-Personalize-Recipe'] = get_recipe(inference_arn)
        else:
            products_service_host, products_service_port = get_products_service_host_and_port()
            resolver = DefaultProductResolver(products_service_host = products_service_host, products_service_port = products_service_port)

            items = resolver.get_items(product_id = current_item_id, num_results = num_results)

    item_ids = [item['itemId'] for item in items]

    products = fetch_product_details(item_ids, fully_qualify_image_urls)
    for item in items:
        item_id = item['itemId']

        product = next((p for p in products if p['id'] == item_id), None)
        if product is not None and 'experiment' in item and 'url' in product:
            # Append the experiment correlation ID to the product URL so it gets tracked if used by client.
            product_url = product.get('url')
            if '?' in product_url:
                product_url += '&'
            else:
                product_url += '?'

            product_url += 'exp=' + item['experiment']['correlationId']

            product['url'] = product_url

        item.update({
            'product': product
        })

        item.pop('itemId')

    resp = Response(json.dumps(items, cls=CompatEncoder), content_type = 'application/json', headers = resp_headers)
    return resp