예제 #1
0
def _compute_diagnostic_overall(
    project: scoring.ScoringProject, diagnostic: diagnostic_pb2.Diagnostic,
    category: Optional[diagnostic_pb2.DiagnosticCategory]
) -> diagnostic_pb2.Diagnostic:
    all_overalls = _DIAGNOSTIC_OVERALL.get_collection(project.database)
    restricted_overalls: Iterable[diagnostic_pb2.DiagnosticTemplate] = []
    if category and (not category.are_strategies_for_alpha_only
                     or project.features_enabled.alpha):
        restricted_overalls = \
            [o for o in all_overalls if o.category_id == category.category_id]
    if not restricted_overalls:
        restricted_overalls = [o for o in all_overalls if not o.category_id]
    overall_template = next((scoring.filter_using_score(
        restricted_overalls, lambda t: t.filters, project)), None)
    if not overall_template:
        # TODO(cyrille): Put a warning here once enough cases are covered with overall templates.
        return diagnostic
    diagnostic.overall_sentence = project.populate_template(
        project.translate_string(overall_template.sentence_template))
    diagnostic.text = project.populate_template(
        project.translate_string(overall_template.text_template))
    diagnostic.strategies_introduction = project.populate_template(
        project.translate_string(overall_template.strategies_introduction))
    diagnostic.overall_score = overall_template.score
    return diagnostic
예제 #2
0
def _compute_diagnostic_overall(
    project: scoring.ScoringProject, diagnostic: diagnostic_pb2.Diagnostic,
    main_challenge: diagnostic_pb2.DiagnosticMainChallenge
) -> diagnostic_pb2.Diagnostic:
    all_overalls = _DIAGNOSTIC_OVERALL.get_collection(project.database)
    restricted_overalls = [
        o for o in all_overalls if o.category_id == main_challenge.category_id
    ]
    try:
        overall_template = next(
            (scoring.filter_using_score(restricted_overalls,
                                        lambda t: t.filters, project)))
    except StopIteration:
        logging.warning('No overall template for project: %s',
                        main_challenge.category_id)
        return diagnostic
    diagnostic.overall_sentence = project.populate_template(
        project.translate_airtable_string(
            'diagnosticOverall',
            overall_template.id,
            'sentence_template',
            is_genderized=True,
            hint=overall_template.sentence_template))
    diagnostic.text = project.populate_template(
        project.translate_airtable_string('diagnosticOverall',
                                          overall_template.id,
                                          'text_template',
                                          is_genderized=True,
                                          hint=overall_template.text_template))
    diagnostic.strategies_introduction = project.populate_template(
        project.translate_airtable_string(
            'diagnosticOverall',
            overall_template.id,
            'strategies_introduction',
            is_genderized=True,
            hint=overall_template.strategies_introduction))
    diagnostic.overall_score = overall_template.score
    diagnostic.bob_explanation = main_challenge.bob_explanation

    all_responses = _DIAGNOSTIC_RESPONSES.get_collection(project.database)
    self_diagnostic_category_id = project.details.original_self_diagnostic.category_id
    response_id = f'{self_diagnostic_category_id}:{main_challenge.category_id}'
    response_text = next(
        (response.text
         for response in all_responses if response.response_id == response_id),
        '')
    diagnostic.response = project.translate_airtable_string(
        'diagnosticResponses',
        response_id,
        'text',
        is_genderized=True,
        hint=response_text)

    return diagnostic
예제 #3
0
def _make_strategy(
        project: scoring.ScoringProject, module: strategy_pb2.StrategyModule,
        advice_scores: Dict[str, float]) -> Optional[strategy_pb2.Strategy]:
    score = project.score(module.trigger_scoring_model)
    if not score:
        return None
    score = min(score * 100 / 3,
                100 - project.details.diagnostic.overall_score)
    pieces_of_advice = []
    for advice in module.pieces_of_advice:
        user_advice_id = next(
            (a for a in advice_scores if a.startswith(advice.advice_id)), None)
        if not user_advice_id:
            if advice.is_required:
                # A necessary advice is missing, we drop everything.
                return None
            continue
        pieces_of_advice.append(
            strategy_pb2.StrategyAdvice(
                advice_id=user_advice_id,
                teaser=project.populate_template(
                    project.translate_string(advice.teaser_template)),
                header=project.populate_template(
                    project.translate_string(advice.header_template))))
    if _SPECIFIC_TO_JOB_ADVICE_ID in advice_scores:
        specific_to_job_config = project.specific_to_job_advice_config()
        if specific_to_job_config and module.strategy_id in specific_to_job_config.strategy_ids:
            pieces_of_advice.append(
                strategy_pb2.StrategyAdvice(
                    advice_id=_SPECIFIC_TO_JOB_ADVICE_ID))
    if not pieces_of_advice:
        # Don't want to show a strategy without any advice modules.
        return None
    strategy = project.details.strategies.add(
        description=project.populate_template(
            project.translate_string(module.description_template)),
        score=int(score),
        is_secondary=score <= 10,
        title=project.translate_string(module.title),
        header=project.populate_template(
            project.translate_string(module.header_template)),
        strategy_id=module.strategy_id)

    # Sort found pieces of advice by descending score.
    pieces_of_advice.sort(key=lambda a: advice_scores[a.advice_id],
                          reverse=True)
    strategy.pieces_of_advice.extend(pieces_of_advice)
    return strategy
예제 #4
0
def _compute_diagnostic_text(
        scoring_project: scoring.ScoringProject, unused_overall_score: float) \
        -> Tuple[str, List[int]]:
    """Create the left-side text of the diagnostic for a given project.

    Returns:
        A tuple containing the text,
        and a list of the orders of missing sentences (if text is empty).
    """

    sentences = []
    missing_sentences_orders = []
    templates_per_order = itertools.groupby(
        _SENTENCE_TEMPLATES.get_collection(scoring_project.database),
        key=lambda template: template.order)
    for order, templates_iterator in templates_per_order:
        templates = list(templates_iterator)
        template = next(
            scoring.filter_using_score(templates,
                                       lambda template: template.filters,
                                       scoring_project), None)
        if not template:
            if any(template.optional for template in templates):
                continue
            # TODO(pascal): Set to warning when we have theoretical complete coverage.
            logging.debug('Could not find a sentence %d for user.', order)
            missing_sentences_orders.append(order)
            continue
        translated_template = scoring_project.translate_string(
            template.sentence_template)
        sentences.append(
            scoring_project.populate_template(translated_template))
    return '\n\n'.join(
        sentences
    ) if not missing_sentences_orders else '', missing_sentences_orders
예제 #5
0
 def _assert_proper_explanations(self, explanations: Iterable[str],
                                 scoring_project: scoring.ScoringProject,
                                 msg: str) -> None:
     self.assertIsInstance(explanations, list, msg=msg)
     for explanation in explanations:
         self.assertIsInstance(explanation, str, msg=msg)
         try:
             resolved_explanation = scoring_project.populate_template(
                 explanation, raise_on_missing_var=True)
         except ValueError:
             self.fail(msg=msg)
         self.assertNotRegex(resolved_explanation, r'^[A-Z]', msg=msg)
예제 #6
0
def _make_strategy(
        project: scoring.ScoringProject, module: strategy_pb2.StrategyModule,
        advice_scores: dict[str, float]) -> Optional[strategy_pb2.Strategy]:
    if module.is_for_alpha and not project.features_enabled.alpha:
        return None
    score = project.score(module.trigger_scoring_model)
    if not score:
        return None
    score = min(score * 100 / 3, 100 - project.details.diagnostic.overall_score)
    pieces_of_advice = []
    for advice in module.pieces_of_advice:
        user_advice_id = next((a for a in advice_scores if a.startswith(advice.advice_id)), None)
        if not user_advice_id:
            if advice.is_required:
                # A necessary advice is missing, we drop everything.
                return None
            continue
        pieces_of_advice.append(strategy_pb2.StrategyAdvice(
            advice_id=user_advice_id,
            teaser=project.populate_template(project.translate_string(advice.teaser_template)),
            why=project.populate_template(project.translate_string(advice.why_template))))
    if _SPECIFIC_TO_JOB_ADVICE_ID in advice_scores:
        specific_to_job_config = project.specific_to_job_advice_config()
        if specific_to_job_config and module.strategy_id in specific_to_job_config.strategy_ids:
            pieces_of_advice.append(strategy_pb2.StrategyAdvice(
                advice_id=_SPECIFIC_TO_JOB_ADVICE_ID))
    if not pieces_of_advice and not module.external_url_template:
        # Don't want to show a strategy without any advice modules.
        return None
    strategy = project.details.strategies.add(
        description=project.populate_template(project.translate_airtable_string(
            'strategyModules', module.strategy_id, 'description_template',
            hint=module.description_template)),
        score=int(score),
        is_secondary=score <= 10,
        title=project.translate_airtable_string(
            'strategyModules', module.strategy_id, 'title', hint=module.title),
        header=project.populate_template(project.translate_airtable_string(
            'strategyModules', module.strategy_id, 'header_template',
            hint=module.header_template)),
        strategy_id=module.strategy_id,
        external_url=project.populate_template(module.external_url_template),
        infinitive_title=project.translate_airtable_string(
            'strategyModules', module.strategy_id, 'infinitive_title',
            hint=module.infinitive_title),
        action_ids=module.action_ids,
        description_speech=project.populate_template(project.translate_airtable_string(
            'strategyModules', module.strategy_id, 'description_speech',
            hint=module.description_speech, is_genderized=True)),
    )

    if strategy.external_url and pieces_of_advice:
        logging.error(
            'Strategy %s has both an external URL and some pieces of advice:\n%s',
            strategy.strategy_id, ', '.join(a.advice_id for a in pieces_of_advice))

    # Sort found pieces of advice by descending score.
    pieces_of_advice.sort(key=lambda a: advice_scores[a.advice_id], reverse=True)
    strategy.pieces_of_advice.extend(pieces_of_advice)
    return strategy
예제 #7
0
def compute_sub_diagnostic_observations(
        scoring_project: scoring.ScoringProject, topic: diagnostic_pb2.DiagnosticTopic) \
        -> Iterator[diagnostic_pb2.SubDiagnosticObservation]:
    """Find all relevant observations for a given sub-diagnostic topic."""

    templates = scoring.filter_using_score(
        (template
         for template in _SUBTOPIC_OBSERVATION_TEMPLATES.get_collection(
             scoring_project.database) if template.topic == topic),
        lambda template: template.filters, scoring_project)
    for template in templates:
        yield diagnostic_pb2.SubDiagnosticObservation(
            text=scoring_project.populate_template(
                scoring_project.translate_string(template.sentence_template)),
            is_attention_needed=template.is_attention_needed)
예제 #8
0
def _compute_sub_diagnostic_text(
        scoring_project: scoring.ScoringProject, sub_diagnostic: diagnostic_pb2.SubDiagnostic) \
        -> str:
    """Create the sentence of the diagnostic for a given project on a given topic.

    Returns:
        The text for the diagnostic submetric.
    """

    template = next(
        scoring.filter_using_score(
            (template
             for template in _SUBTOPIC_SENTENCE_TEMPLATES.get_collection(
                 scoring_project.database)
             if template.topic == sub_diagnostic.topic),
            lambda template: template.filters, scoring_project), None)
    if not template:
        # TODO(cyrille): Change to warning once we have theoretical complete coverage.
        logging.debug('Could not find a sentence for topic %s for user.',
                      sub_diagnostic.topic)
        return ''
    translated_template = scoring_project.translate_string(
        template.sentence_template)
    return scoring_project.populate_template(translated_template)
예제 #9
0
def _compute_available_methods(
    scoring_project: scoring.ScoringProject,
    method_modules: Iterable[advisor_pb2.AdviceModule],
    scoring_timeout_seconds: float
) -> Generator[project_pb2.Advice, None, Set[str]]:
    scores: Dict[str, float] = {}
    reasons: Dict[str, List[str]] = {}
    missing_fields: Set[str] = set()
    for module in method_modules:
        if not module.is_ready_for_prod and not scoring_project.features_enabled.alpha:
            continue
        scoring_model = scoring.get_scoring_model(module.trigger_scoring_model)
        if scoring_model is None:
            logging.warning(
                'Not able to score advice "%s", the scoring model "%s" is unknown.',
                module.advice_id, module.trigger_scoring_model)
            continue
        if scoring_project.user.features_enabled.all_modules:
            scores[module.advice_id] = 3
        else:
            thread = threading.Thread(target=_compute_score_and_reasons,
                                      args=(scores, reasons, module,
                                            scoring_model, scoring_project,
                                            missing_fields))
            thread.start()
            # TODO(pascal): Consider scoring different models in parallel.
            thread.join(timeout=scoring_timeout_seconds)
            if thread.is_alive():
                logging.warning('Timeout while scoring advice "%s" for:\n%s',
                                module.trigger_scoring_model, scoring_project)

    modules = sorted(method_modules,
                     key=lambda m: (scores.get(m.advice_id, 0), m.advice_id),
                     reverse=True)
    incompatible_modules: Set[str] = set()
    has_module = False
    for module in modules:
        score = scores.get(module.advice_id)
        if not score:
            # We can break as others will have 0 score as well.
            break
        if module.airtable_id in incompatible_modules and \
                not scoring_project.user.features_enabled.all_modules:
            continue
        piece_of_advice = project_pb2.Advice(
            advice_id=module.advice_id,
            num_stars=score,
            is_for_alpha_only=not module.is_ready_for_prod)
        piece_of_advice.explanations.extend(
            scoring_project.populate_template(reason)
            for reason in reasons.get(module.advice_id, []))

        incompatible_modules.update(module.incompatible_advice_ids)

        _maybe_override_advice_data(piece_of_advice, module, scoring_project)
        has_module = True
        yield piece_of_advice

    if not has_module and method_modules:
        logging.warning(
            'We could not find *any* advice for a project:\nModules tried:\n"%s"\nProject:\n%s',
            '", "'.join(m.advice_id for m in method_modules), scoring_project)

    return missing_fields
예제 #10
0
def compute_available_methods(
        scoring_project: scoring.ScoringProject,
        method_modules: Iterable[advisor_pb2.AdviceModule],
        scoring_timeout_seconds: float = 3) \
        -> Iterator[Tuple[project_pb2.Advice, frozenset[str]]]:
    """Advise on a user project.

    Args:
        scoring_project: the user's data.
        advice_modules: a list of modules, from which we want to derive the advices.
        scoring_timeout_seconds: how long we wait to compute each advice scoring model.
    Returns:
        an Iterator of recommendations, each with a list of fields that would help improve
        the process.
    """

    ready_modules = {
        module.advice_id: module.trigger_scoring_model
        for module in method_modules
        if module.is_ready_for_prod or scoring_project.features_enabled.alpha
    }

    scores: Mapping[str, float] = {}
    reasons: Mapping[str, tuple[str, ...]] = {}
    missing_fields: Mapping[str, frozenset[str]] = {}

    if scoring_project.user.features_enabled.all_modules:
        scores = {key: 3 for key in ready_modules}
    else:
        scores, reasons, missing_fields = scoring_project.score_and_explain_all(
            ready_modules.items(),
            scoring_timeout_seconds=scoring_timeout_seconds)

    modules = sorted(method_modules,
                     key=lambda m: (scores.get(m.advice_id, 0), m.advice_id),
                     reverse=True)
    incompatible_modules: Set[str] = set()
    has_module = False
    for module in modules:
        score = scores.get(module.advice_id)
        if not score:
            # We can break as others will have 0 score as well.
            break
        if module.airtable_id in incompatible_modules and \
                not scoring_project.user.features_enabled.all_modules:
            continue
        piece_of_advice = project_pb2.Advice(
            advice_id=module.advice_id,
            num_stars=score,
            is_for_alpha_only=not module.is_ready_for_prod)
        piece_of_advice.explanations.extend(
            scoring_project.populate_template(reason)
            for reason in reasons.get(module.advice_id, []))

        incompatible_modules.update(module.incompatible_advice_ids)

        _maybe_override_advice_data(piece_of_advice, module, scoring_project)
        has_module = True
        yield piece_of_advice, missing_fields.get(module.advice_id,
                                                  frozenset())

    if not has_module and method_modules:
        logging.warning(
            'We could not find *any* advice for a project:\nModules tried:\n"%s"\nProject:\n%s',
            '", "'.join(m.advice_id for m in method_modules), scoring_project)