blob: 9ef6e9112ad463c7e5be1ff3c6d5e44c3bc98325 [file] [log] [blame]
// 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