| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/contextual_cueing/zero_state_suggestions_page_data.h" |
| |
| #include "base/metrics/histogram_macros_local.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/content_extraction/inner_text.h" |
| #include "chrome/browser/contextual_cueing/contextual_cueing_features.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h" |
| #include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/optimization_guide/core/model_execution/optimization_guide_model_execution_error.h" |
| #include "components/optimization_guide/core/optimization_guide_common.mojom.h" |
| #include "components/optimization_guide/core/optimization_guide_logger.h" |
| #include "components/optimization_guide/core/optimization_guide_permissions_util.h" |
| #include "components/optimization_guide/core/optimization_guide_util.h" |
| #include "components/optimization_guide/proto/contextual_cueing_metadata.pb.h" |
| #include "components/optimization_guide/proto/features/zero_state_suggestions.pb.h" |
| #include "components/optimization_guide/proto/hints.pb.h" |
| #include "content/public/browser/web_contents.h" |
| |
| namespace { |
| |
| // Parse the given metadata and return a std::pair containing: |
| // 1. bool: true if eligible for contextual suggestions. |
| // 2. std::vector<std::string>: suggestions in the metadata, if present. |
| std::pair<bool, std::vector<std::string>> ParseOptimizationMetadata( |
| optimization_guide::OptimizationGuideDecision decision, |
| optimization_guide::OptimizationMetadata& metadata) { |
| if (decision != optimization_guide::OptimizationGuideDecision::kTrue) { |
| return std::make_pair(true, std::vector<std::string>()); |
| } |
| |
| auto suggestions_metadata = metadata.ParsedMetadata< |
| optimization_guide::proto::GlicZeroStateSuggestionsMetadata>(); |
| if (!suggestions_metadata.has_value()) { |
| return std::make_pair(true, std::vector<std::string>()); |
| } |
| if (!suggestions_metadata->contextual_suggestions_eligible()) { |
| return std::make_pair(false, std::vector<std::string>()); |
| } |
| if (!suggestions_metadata->contextual_suggestions().empty()) { |
| return std::make_pair( |
| true, std::vector<std::string>( |
| suggestions_metadata->contextual_suggestions().begin(), |
| suggestions_metadata->contextual_suggestions().end())); |
| } |
| |
| return std::make_pair(true, std::vector<std::string>()); |
| } |
| |
| } // namespace |
| |
| namespace contextual_cueing { |
| |
| ZeroStateSuggestionsPageData::ZeroStateSuggestionsPageData(content::Page& page) |
| : content::PageUserData<ZeroStateSuggestionsPageData>(page) { |
| CHECK(base::FeatureList::IsEnabled(kGlicZeroStateSuggestions)); |
| CHECK(kExtractInnerTextForZeroStateSuggestions.Get() || |
| kExtractAnnotatedPageContentForZeroStateSuggestions.Get()); |
| |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(&(page.GetMainDocument())); |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| optimization_guide_keyed_service_ = |
| OptimizationGuideKeyedServiceFactory::GetForProfile(profile); |
| |
| if (kExtractAnnotatedPageContentForZeroStateSuggestions.Get()) { |
| blink::mojom::AIPageContentOptionsPtr ai_page_content_options; |
| ai_page_content_options = optimization_guide::DefaultAIPageContentOptions(); |
| ai_page_content_options->include_geometry = false; |
| ai_page_content_options->on_critical_path = true; |
| ai_page_content_options->include_hidden_searchable_content = false; |
| optimization_guide::GetAIPageContent( |
| web_contents, std::move(ai_page_content_options), |
| base::BindOnce( |
| &ZeroStateSuggestionsPageData::OnReceivedAnnotatedPageContent, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| OnReceivedAnnotatedPageContent(/*content=*/std::nullopt); |
| } |
| |
| if (kExtractInnerTextForZeroStateSuggestions.Get()) { |
| // TODO(crbug.com/407121627): remove inner text fetch once server is ready |
| // to take annotated page content. |
| content::RenderFrameHost& frame = page.GetMainDocument(); |
| content_extraction::GetInnerText( |
| frame, |
| /*node_id=*/std::nullopt, |
| base::BindOnce(&ZeroStateSuggestionsPageData::OnReceivedInnerText, |
| weak_ptr_factory_.GetWeakPtr())); |
| } else { |
| OnReceivedInnerText(/*result=*/nullptr); |
| } |
| } |
| |
| ZeroStateSuggestionsPageData::~ZeroStateSuggestionsPageData() = default; |
| |
| void ZeroStateSuggestionsPageData::FetchSuggestions( |
| bool is_fre, |
| GlicSuggestionsCallback callback) { |
| if (cached_suggestions_) { |
| std::move(callback).Run(cached_suggestions_->empty() |
| ? std::nullopt |
| : std::make_optional(*cached_suggestions_)); |
| return; |
| } |
| |
| begin_time_ = base::TimeTicks::Now(); |
| |
| // Request for page already in flight - just notify when it comes back. |
| if (suggestions_request_) { |
| suggestions_callbacks_.AddUnsafe(std::move(callback)); |
| return; |
| } |
| |
| suggestions_request_ = optimization_guide::proto:: |
| ZeroStateSuggestionsRequest::default_instance(); |
| suggestions_request_->set_is_fre(is_fre); |
| suggestions_callbacks_.AddUnsafe(std::move(callback)); |
| RequestSuggestionsIfComplete(); |
| } |
| |
| void ZeroStateSuggestionsPageData::OnReceivedAnnotatedPageContent( |
| std::optional<optimization_guide::AIPageContentResult> content) { |
| annotated_page_content_ = std::move(content); |
| annotated_page_content_done_ = true; |
| RequestSuggestionsIfComplete(); |
| } |
| |
| void ZeroStateSuggestionsPageData::OnReceivedInnerText( |
| std::unique_ptr<content_extraction::InnerTextResult> result) { |
| inner_text_result_ = std::move(result); |
| inner_text_done_ = true; |
| RequestSuggestionsIfComplete(); |
| } |
| |
| void ZeroStateSuggestionsPageData::OnReceivedOptimizationMetadata( |
| const GURL& url, |
| const base::flat_map< |
| optimization_guide::proto::OptimizationType, |
| optimization_guide::OptimizationGuideDecisionWithMetadata>& decisions) { |
| optimization_metadata_done_ = true; |
| auto it = |
| decisions.find(optimization_guide::proto::GLIC_ZERO_STATE_SUGGESTIONS); |
| if (it == decisions.end()) { |
| // If not found, treat it as no metadata. |
| optimization_decision_ = |
| optimization_guide::OptimizationGuideDecision::kFalse; |
| } else { |
| optimization_metadata_ = it->second.metadata; |
| optimization_decision_ = it->second.decision; |
| } |
| |
| ProcessSuggestionsIfComplete(); |
| } |
| |
| bool ZeroStateSuggestionsPageData:: |
| ReturnSuggestionsFromOptimizationMetadataIfPossible() { |
| if (!optimization_metadata_done_) { |
| return false; |
| } |
| |
| std::pair<bool, std::vector<std::string>> pair = |
| ParseOptimizationMetadata(optimization_decision_, optimization_metadata_); |
| if (!pair.first) { |
| suggestions_callbacks_.Notify(std::nullopt); |
| cached_suggestions_ = std::make_optional(std::vector<std::string>({})); |
| return true; |
| } |
| if (!pair.second.empty()) { |
| suggestions_callbacks_.Notify(pair.second); |
| cached_suggestions_ = pair.second; |
| return true; |
| } |
| return false; |
| } |
| |
| void ZeroStateSuggestionsPageData::RequestSuggestionsIfComplete() { |
| bool work_done = inner_text_done_ && annotated_page_content_done_; |
| bool has_page_context = inner_text_result_ || annotated_page_content_; |
| if (!work_done) { |
| return; |
| } |
| |
| LOCAL_HISTOGRAM_BOOLEAN( |
| "ContextualCueing.ZeroStateSuggestions.ContextExtractionDone", true); |
| |
| if (!suggestions_request_) { |
| return; |
| } |
| |
| content::WebContents* web_contents = |
| content::WebContents::FromRenderFrameHost(&(page().GetMainDocument())); |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| bool can_request_metadata = |
| optimization_guide::IsUserPermittedToFetchFromRemoteOptimizationGuide( |
| profile->IsOffTheRecord(), profile->GetPrefs()); |
| |
| if (!optimization_metadata_done_ && can_request_metadata) { |
| optimization_decision_ = |
| optimization_guide_keyed_service_->CanApplyOptimization( |
| web_contents->GetLastCommittedURL(), |
| optimization_guide::proto::GLIC_ZERO_STATE_SUGGESTIONS, |
| &optimization_metadata_); |
| optimization_metadata_done_ = true; |
| } |
| if (ReturnSuggestionsFromOptimizationMetadataIfPossible()) { |
| return; |
| } |
| |
| if (!has_page_context) { |
| suggestions_callbacks_.Notify(std::nullopt); |
| cached_suggestions_ = std::make_optional(std::vector<std::string>({})); |
| return; |
| } |
| |
| if (!optimization_metadata_done_ && !can_request_metadata) { |
| optimization_guide_keyed_service_->CanApplyOptimizationOnDemand( |
| {content::WebContents::FromRenderFrameHost(&(page().GetMainDocument())) |
| ->GetLastCommittedURL()}, |
| {optimization_guide::proto::GLIC_ZERO_STATE_SUGGESTIONS}, |
| optimization_guide::proto::RequestContext:: |
| CONTEXT_GLIC_ZERO_STATE_SUGGESTIONS, |
| base::BindRepeating( |
| &ZeroStateSuggestionsPageData::OnReceivedOptimizationMetadata, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| optimization_guide::proto::PageContext* page_context = |
| suggestions_request_->mutable_page_context(); |
| const GURL& page_url = web_contents->GetLastCommittedURL(); |
| if (!page_url.is_empty() && page_url.is_valid()) { |
| page_context->set_url(page_url.spec()); |
| } |
| page_context->set_title(base::UTF16ToUTF8(web_contents->GetTitle())); |
| |
| if (annotated_page_content_) { |
| *page_context->mutable_annotated_page_content() = |
| annotated_page_content_->proto; |
| } |
| if (inner_text_result_) { |
| page_context->set_inner_text(inner_text_result_->inner_text); |
| } |
| |
| optimization_guide_keyed_service_->ExecuteModel( |
| optimization_guide::ModelBasedCapabilityKey::kZeroStateSuggestions, |
| *suggestions_request_, |
| /*execution_timeout=*/std::nullopt, |
| base::BindOnce(&ZeroStateSuggestionsPageData::OnModelExecutionResponse, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ZeroStateSuggestionsPageData::OnModelExecutionResponse( |
| optimization_guide::OptimizationGuideModelExecutionResult result, |
| std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry) { |
| // Clear out suggestions request as it's been fulfilled. |
| suggestions_request_ = std::nullopt; |
| mes_suggestions_result_ = std::make_unique< |
| optimization_guide::OptimizationGuideModelExecutionResult>( |
| std::move(result)); |
| mes_suggestions_done_ = true; |
| |
| base::TimeDelta suggestions_duration = base::TimeTicks::Now() - begin_time_; |
| if (!mes_suggestions_result_->response.has_value()) { |
| OPTIMIZATION_GUIDE_LOG( |
| optimization_guide_common::mojom::LogSource::MODEL_EXECUTION, |
| optimization_guide_keyed_service_->GetOptimizationGuideLogger(), |
| base::StringPrintf( |
| "ZeroStateSuggestionsPageData: Failed to get " |
| "suggestions after %ld ms. Error: %d", |
| suggestions_duration.InMilliseconds(), |
| static_cast<int>( |
| mes_suggestions_result_->response.error().error()))); |
| suggestions_callbacks_.Notify(std::nullopt); |
| |
| if (!mes_suggestions_result_->response.error().transient()) { |
| // Cache empty suggestions if error is not transient. |
| cached_suggestions_ = std::make_optional(std::vector<std::string>({})); |
| } |
| |
| return; |
| } |
| |
| OPTIMIZATION_GUIDE_LOG( |
| optimization_guide_common::mojom::LogSource::MODEL_EXECUTION, |
| optimization_guide_keyed_service_->GetOptimizationGuideLogger(), |
| base::StringPrintf("ZeroStateSuggestionsPageData: Received valid " |
| "suggestions after %ld ms.", |
| suggestions_duration.InMilliseconds())); |
| |
| ProcessSuggestionsIfComplete(); |
| } |
| |
| void ZeroStateSuggestionsPageData::ProcessSuggestionsIfComplete() { |
| // Do not wait for model execution service if optimization metadata has |
| // enough information. |
| if (ReturnSuggestionsFromOptimizationMetadataIfPossible()) { |
| return; |
| } |
| |
| if (!optimization_metadata_done_ || !mes_suggestions_done_) { |
| return; |
| } |
| |
| std::optional<optimization_guide::proto::ZeroStateSuggestionsResponse> |
| response = optimization_guide::ParsedAnyMetadata< |
| optimization_guide::proto::ZeroStateSuggestionsResponse>( |
| mes_suggestions_result_->response.value()); |
| if (!response) { |
| suggestions_callbacks_.Notify(std::nullopt); |
| // Treat this as a transient error that server returned bad data |
| // momentarily. |
| return; |
| } |
| |
| std::vector<std::string> suggestions; |
| for (int i = 0; i < response->suggestions_size(); ++i) { |
| suggestions.push_back(response->suggestions(i).label()); |
| OPTIMIZATION_GUIDE_LOG( |
| optimization_guide_common::mojom::LogSource::MODEL_EXECUTION, |
| optimization_guide_keyed_service_->GetOptimizationGuideLogger(), |
| base::StringPrintf("ZeroStateSuggestionsPageData: Suggestion %d: %s", |
| i + 1, response->suggestions(i).label())); |
| } |
| suggestions_callbacks_.Notify(suggestions); |
| |
| cached_suggestions_ = suggestions; |
| } |
| |
| PAGE_USER_DATA_KEY_IMPL(ZeroStateSuggestionsPageData); |
| |
| } // namespace contextual_cueing |