| // Copyright 2026 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/glic/selection/explain_selection_trigger.h" |
| |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/json/json_reader.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/glic/public/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 "chrome/grit/generated_resources.h" |
| #include "components/optimization_guide/core/model_execution/feature_keys.h" |
| #include "components/optimization_guide/core/model_execution/optimization_guide_model_execution_error.h" |
| #include "components/optimization_guide/core/model_execution/remote_model_executor.h" |
| #include "components/optimization_guide/core/model_quality/model_quality_log_entry.h" |
| #include "components/optimization_guide/core/optimization_guide_util.h" |
| #include "components/optimization_guide/proto/string_value.pb.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/gurl.h" |
| |
| namespace glic { |
| |
| ExplainSelectionTrigger::ExplainSelectionTrigger() = default; |
| |
| ExplainSelectionTrigger::~ExplainSelectionTrigger() = default; |
| |
| // static |
| std::string ExplainSelectionTrigger::GetPromptTemplate() { |
| const auto* command_line = base::CommandLine::ForCurrentProcess(); |
| if (command_line->HasSwitch("glic-inline-prompt")) { |
| return command_line->GetSwitchValueASCII("glic-inline-prompt"); |
| } |
| if (command_line->HasSwitch("glic-inline-prompt-file")) { |
| base::FilePath prompt_file = |
| command_line->GetSwitchValuePath("glic-inline-prompt-file"); |
| std::string file_content; |
| if (base::ReadFileToString(prompt_file, &file_content)) { |
| return std::string( |
| base::TrimWhitespaceASCII(file_content, base::TRIM_ALL)); |
| } |
| } |
| |
| std::string param_prompt = |
| features::kGlicSelectionPromptInlinePromptTemplate.Get(); |
| if (!param_prompt.empty()) { |
| return param_prompt; |
| } |
| |
| // TODO(b/539514187): Load default prompt template from Chrome branded and |
| // translated resource strings when Finch parameter is not specified. |
| return ""; |
| } |
| |
| // static |
| std::string ExplainSelectionTrigger::GetGeminiApiKey() { |
| const auto* command_line = base::CommandLine::ForCurrentProcess(); |
| if (command_line->HasSwitch("glic-gemini-api-key")) { |
| return command_line->GetSwitchValueASCII("glic-gemini-api-key"); |
| } |
| return ""; |
| } |
| |
| // static |
| bool ExplainSelectionTrigger::IsInlineFulfillmentSupported() { |
| if (!features::kGlicSelectionPromptInlineFulfillment.Get()) { |
| return false; |
| } |
| return !GetPromptTemplate().empty(); |
| } |
| |
| // static |
| bool ExplainSelectionTrigger::ShouldUseOptimizationGuide() { |
| const auto* command_line = base::CommandLine::ForCurrentProcess(); |
| return command_line->HasSwitch("glic-inline-use-optimization-guide"); |
| } |
| |
| // static |
| std::string ExplainSelectionTrigger::FormatPrompt( |
| const std::string& prompt_template, |
| const std::string& selected_text, |
| const std::string& surrounding_text) { |
| std::string formatted_prompt = prompt_template; |
| size_t pos = formatted_prompt.find("$1"); |
| if (pos != std::string::npos) { |
| formatted_prompt.replace(pos, 2, selected_text); |
| } else { |
| formatted_prompt += "\n" + selected_text; |
| } |
| |
| size_t pos2 = formatted_prompt.find("$2"); |
| if (pos2 != std::string::npos) { |
| formatted_prompt.replace(pos2, 2, surrounding_text); |
| } |
| return formatted_prompt; |
| } |
| |
| void ExplainSelectionTrigger::RequestExplanation( |
| content::WebContents* web_contents, |
| const std::string& selected_text, |
| const std::string& surrounding_text, |
| StreamUpdateCallback callback) { |
| if (!web_contents || !callback) { |
| if (callback) { |
| callback.Run("", true, "Invalid web contents"); |
| } |
| return; |
| } |
| |
| if (!IsInlineFulfillmentSupported()) { |
| callback.Run("", true, |
| "Inline fulfillment disabled or private prompt resource " |
| "unavailable"); |
| return; |
| } |
| |
| start_time_ = base::TimeTicks::Now(); |
| |
| // TODO(b/539511437): Evaluate whether enterprise Data Loss Prevention (DLP) |
| // restrictions apply before sending selected text to the Gemini model. |
| |
| std::string prompt = |
| FormatPrompt(GetPromptTemplate(), selected_text, surrounding_text); |
| |
| if (ShouldUseOptimizationGuide()) { |
| SendOptimizationGuideRequest(web_contents, prompt, callback); |
| } else { |
| SendGeminiApiRequest(web_contents, prompt, selected_text, callback); |
| } |
| } |
| |
| void ExplainSelectionTrigger::SendOptimizationGuideRequest( |
| content::WebContents* web_contents, |
| const std::string& prompt, |
| StreamUpdateCallback callback) { |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| if (!profile) { |
| callback.Run(/*markdown_output=*/"", /*is_complete=*/true, |
| /*error_message=*/"Invalid profile"); |
| return; |
| } |
| |
| OptimizationGuideKeyedService* optimization_guide_keyed_service = |
| OptimizationGuideKeyedServiceFactory::GetForProfile(profile); |
| if (!optimization_guide_keyed_service) { |
| callback.Run( |
| /*markdown_output=*/"", /*is_complete=*/true, |
| /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE)); |
| return; |
| } |
| |
| optimization_guide::proto::StringValue request_metadata; |
| request_metadata.set_value(prompt); |
| |
| optimization_guide_keyed_service->ExecuteModel( |
| optimization_guide::ModelBasedCapabilityKey::kTest, request_metadata, |
| /*options=*/{}, |
| base::BindOnce(&ExplainSelectionTrigger::OnOptimizationGuideResponse, |
| weak_ptr_factory_.GetWeakPtr(), callback)); |
| } |
| |
| void ExplainSelectionTrigger::OnOptimizationGuideResponse( |
| StreamUpdateCallback callback, |
| optimization_guide::OptimizationGuideModelExecutionResult result, |
| std::unique_ptr<optimization_guide::ModelQualityLogEntry> log_entry) { |
| base::TimeDelta latency = base::TimeTicks::Now() - start_time_; |
| base::UmaHistogramTimes( |
| "OptimizationGuide.ModelExecution.ExplainSelection.TTFT", latency); |
| base::UmaHistogramTimes( |
| "OptimizationGuide.ModelExecution.ExplainSelection.TotalLatency", |
| latency); |
| |
| if (!result.response.has_value()) { |
| callback.Run( |
| /*markdown_output=*/"", /*is_complete=*/true, |
| /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE)); |
| return; |
| } |
| |
| std::optional<optimization_guide::proto::StringValue> response = |
| optimization_guide::ParsedAnyMetadata< |
| optimization_guide::proto::StringValue>(result.response.value()); |
| if (!response || response->value().empty()) { |
| callback.Run( |
| /*markdown_output=*/"", /*is_complete=*/true, |
| /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE)); |
| return; |
| } |
| |
| callback.Run(response->value(), /*is_complete=*/true, /*error_message=*/""); |
| } |
| |
| void ExplainSelectionTrigger::SendGeminiApiRequest( |
| content::WebContents* web_contents, |
| const std::string& formatted_prompt, |
| const std::string& selected_text, |
| StreamUpdateCallback callback) { |
| std::string api_key = GetGeminiApiKey(); |
| |
| // Construct Gemini API generateContent payload |
| base::DictValue payload; |
| base::ListValue contents; |
| base::DictValue user_content; |
| user_content.Set("role", "user"); |
| |
| base::ListValue parts; |
| base::DictValue text_part; |
| text_part.Set("text", formatted_prompt); |
| parts.Append(std::move(text_part)); |
| |
| user_content.Set("parts", std::move(parts)); |
| contents.Append(std::move(user_content)); |
| payload.Set("contents", std::move(contents)); |
| |
| base::DictValue gen_config; |
| gen_config.Set("temperature", 0.2); |
| gen_config.Set("maxOutputTokens", 500); |
| payload.Set("generationConfig", std::move(gen_config)); |
| |
| std::string request_body; |
| base::JSONWriter::Write(payload, &request_body); |
| |
| std::string model_name = "gemini-flash-lite-latest"; |
| const auto* command_line = base::CommandLine::ForCurrentProcess(); |
| if (command_line->HasSwitch("glic-gemini-model")) { |
| model_name = command_line->GetSwitchValueASCII("glic-gemini-model"); |
| } |
| |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| std::string url_str = |
| "https://generativelanguage.googleapis.com/v1beta/models/" + model_name + |
| ":generateContent"; |
| |
| if (!api_key.empty()) { |
| url_str += "?key=" + api_key; |
| resource_request->headers.SetHeader("X-Goog-Api-Key", api_key); |
| } |
| |
| resource_request->url = GURL(url_str); |
| resource_request->method = "POST"; |
| resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("glic_selection_ask_gemini_api", R"( |
| semantics { |
| sender: "Gemini in Chrome" |
| description: |
| "Sends selected text on a web page to the Gemini API to request " |
| "an inline explanation response rendered directly on the selection " |
| "widget." |
| trigger: "User clicks the Ask Gemini button on text selection widget." |
| data: |
| "The text selected by the user on the web page and the prompt " |
| "template used for generating the inline explanation." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { |
| owners: "//chrome/browser/glic/OWNERS" |
| } |
| } |
| user_data { |
| type: USER_CONTENT |
| type: WEB_CONTENT |
| type: ACCESS_TOKEN |
| } |
| last_reviewed: "2026-07-23" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "Users can enable or disable this feature via Chrome flags or " |
| "settings for Glic selection prompt features." |
| chrome_policy { |
| GeminiSettings { |
| GeminiSettings: 1 |
| } |
| GenAiDefaultSettings { |
| GenAiDefaultSettings: 2 |
| } |
| } |
| })"); |
| |
| url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request), |
| traffic_annotation); |
| url_loader_->AttachStringForUpload(request_body, "application/json"); |
| url_loader_->SetAllowHttpErrorResults(true); |
| |
| auto url_loader_factory = |
| web_contents->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| |
| constexpr size_t kMaxGeminiResponseSizeBytes = 1024 * 1024; // 1 MB |
| url_loader_->DownloadToString( |
| url_loader_factory.get(), |
| base::BindOnce(&ExplainSelectionTrigger::OnGeminiApiResponse, |
| weak_ptr_factory_.GetWeakPtr(), callback, selected_text), |
| kMaxGeminiResponseSizeBytes); |
| } |
| |
| void ExplainSelectionTrigger::OnGeminiApiResponse( |
| StreamUpdateCallback callback, |
| std::string selected_text, |
| std::optional<std::string> response_body) { |
| base::TimeDelta latency = base::TimeTicks::Now() - start_time_; |
| base::UmaHistogramTimes( |
| "OptimizationGuide.ModelExecution.ExplainSelection.TTFT", latency); |
| base::UmaHistogramTimes( |
| "OptimizationGuide.ModelExecution.ExplainSelection.TotalLatency", |
| latency); |
| |
| if (!response_body || response_body->empty()) { |
| callback.Run(/*text=*/"", /*is_complete=*/true, |
| /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE)); |
| return; |
| } |
| |
| std::optional<base::DictValue> parsed = |
| base::JSONReader::ReadDict(*response_body, base::JSON_PARSE_RFC); |
| if (parsed) { |
| const base::ListValue* candidates = parsed->FindList("candidates"); |
| if (candidates && !candidates->empty() && (*candidates)[0].is_dict()) { |
| const base::DictValue& first_candidate = (*candidates)[0].GetDict(); |
| const base::DictValue* content = first_candidate.FindDict("content"); |
| if (content) { |
| const base::ListValue* parts = content->FindList("parts"); |
| if (parts && !parts->empty() && (*parts)[0].is_dict()) { |
| const std::string* text = (*parts)[0].GetDict().FindString("text"); |
| if (text && !text->empty()) { |
| callback.Run(*text, /*is_complete=*/true, /*error_message=*/""); |
| return; |
| } |
| } |
| } |
| } |
| |
| const base::DictValue* error_dict = parsed->FindDict("error"); |
| if (error_dict) { |
| const std::string* err_msg = error_dict->FindString("message"); |
| if (err_msg && !err_msg->empty()) { |
| std::string err_out = "**Gemini API Error:** " + *err_msg; |
| callback.Run(err_out, /*is_complete=*/true, /*error_message=*/""); |
| return; |
| } |
| } |
| } |
| |
| callback.Run(/*text=*/"", /*is_complete=*/true, |
| /*error_message=*/l10n_util::GetStringUTF8(IDS_GLIC_ERROR_NOTICE)); |
| } |
| |
| } // namespace glic |