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
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
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
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
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)
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
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)
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)
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
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)