Skip to content

Commit a76efc8

Browse files
committed
Add search query and summary from Brave Search SERP when open Leo chat
- Add search query and summary from Brave Search SERP to conversation history when summarizer-key tag is present. - Also add a new from_brave_SERP property in completion event to identify the conversation entries from Brave Search SERP. - Modify UI to show the page context toggle when search query and summary are staged. - Staged search query and summary (at the end of the conversation history) will be cleared if user opted out, page was unlinked, conversation became inactive, or user navigated away to other page.
1 parent abc7a4c commit a76efc8

32 files changed

+682
-26
lines changed

browser/ai_chat/ai_chat_render_view_context_menu_browsertest.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class AIChatRenderViewContextMenuBrowserTest : public InProcessBrowserTest {
130130
ASSERT_TRUE(data_callback);
131131
for (const auto& data : received_data) {
132132
auto event = mojom::ConversationEntryEvent::NewCompletionEvent(
133-
mojom::CompletionEvent::New(data));
133+
mojom::CompletionEvent::New(data, false));
134134
data_callback.Run(std::move(event));
135135
}
136136
std::move(callback).Run(completed_result);

browser/ai_chat/ai_chat_ui_browsertest.cc

+124-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* You can obtain one at https://mozilla.org/MPL/2.0/. */
55

66
#include <memory>
7+
#include <optional>
78

89
#include "base/files/file_path.h"
910
#include "base/memory/raw_ptr.h"
@@ -13,6 +14,7 @@
1314
#include "base/test/bind.h"
1415
#include "brave/components/ai_chat/content/browser/ai_chat_tab_helper.h"
1516
#include "brave/components/ai_chat/core/browser/constants.h"
17+
#include "brave/components/ai_chat/core/browser/types.h"
1618
#include "brave/components/constants/brave_paths.h"
1719
#include "brave/components/l10n/common/test/scoped_default_locale.h"
1820
#include "brave/components/text_recognition/common/buildflags/buildflags.h"
@@ -32,8 +34,13 @@
3234
#include "content/public/test/browser_test_utils.h"
3335
#include "content/public/test/content_mock_cert_verifier.h"
3436
#include "net/dns/mock_host_resolver.h"
37+
#include "net/test/embedded_test_server/embedded_test_server.h"
38+
#include "net/test/embedded_test_server/http_request.h"
39+
#include "net/test/embedded_test_server/http_response.h"
3540
#include "printing/buildflags/buildflags.h"
41+
#include "services/network/public/cpp/network_switches.h"
3642
#include "ui/compositor/compositor_switches.h"
43+
#include "url/gurl.h"
3744

3845
#if BUILDFLAG(ENABLE_PRINT_PREVIEW)
3946
#include "chrome/browser/printing/test_print_preview_observer.h"
@@ -42,6 +49,53 @@
4249
namespace {
4350

4451
constexpr char kEmbeddedTestServerDirectory[] = "leo";
52+
53+
std::unique_ptr<net::test_server::HttpResponse> HandleSearchQuerySummaryRequest(
54+
const net::test_server::HttpRequest& request) {
55+
const GURL url = request.GetURL();
56+
if (url.path_piece() != "/api/chatllm/raw_data") {
57+
return nullptr;
58+
}
59+
60+
auto query = url.query_piece();
61+
if (query == "key=test_key") {
62+
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
63+
response->set_code(net::HTTP_OK);
64+
response->set_content_type("application/json");
65+
response->set_content(
66+
R"({"conversation": [{"query": "test query",
67+
"answer": [{"text": "test summary"}]}]})");
68+
return response;
69+
}
70+
71+
if (query == "key=not_object") {
72+
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
73+
response->set_code(net::HTTP_OK);
74+
response->set_content_type("application/json");
75+
response->set_content(R"(["not_object"])");
76+
return response;
77+
}
78+
79+
if (query == "key=empty_conversation") {
80+
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
81+
response->set_code(net::HTTP_OK);
82+
response->set_content_type("application/json");
83+
response->set_content(R"({"conversation": []})");
84+
return response;
85+
}
86+
87+
if (query == "key=empty_answer") {
88+
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
89+
response->set_code(net::HTTP_OK);
90+
response->set_content_type("application/json");
91+
response->set_content(R"({"conversation": [{"query": "test query",
92+
"answer": []}])");
93+
return response;
94+
}
95+
96+
return nullptr;
97+
}
98+
4599
} // namespace
46100

47101
class AIChatUIBrowserTest : public InProcessBrowserTest {
@@ -57,7 +111,9 @@ class AIChatUIBrowserTest : public InProcessBrowserTest {
57111
test_data_dir = base::PathService::CheckedGet(brave::DIR_TEST_DATA);
58112
test_data_dir = test_data_dir.AppendASCII(kEmbeddedTestServerDirectory);
59113
https_server_.ServeFilesFromDirectory(test_data_dir);
60-
ASSERT_TRUE(https_server_.Start());
114+
https_server_.RegisterRequestHandler(
115+
base::BindRepeating(&HandleSearchQuerySummaryRequest));
116+
https_server_.StartAcceptingConnections();
61117

62118
// Set a smaller window size so we can have test data with more pages.
63119
browser()->window()->SetContentsSize(gfx::Size(800, 600));
@@ -71,6 +127,14 @@ class AIChatUIBrowserTest : public InProcessBrowserTest {
71127

72128
void SetUpCommandLine(base::CommandLine* command_line) override {
73129
InProcessBrowserTest::SetUpCommandLine(command_line);
130+
131+
ASSERT_TRUE(https_server_.InitializeAndListen());
132+
// Add a host resolver rule to map all outgoing requests to the test server.
133+
command_line->AppendSwitchASCII(
134+
network::switches::kHostResolverRules,
135+
"MAP * " + https_server_.host_port_pair().ToString() +
136+
",EXCLUDE localhost");
137+
74138
#if BUILDFLAG(ENABLE_TEXT_RECOGNITION)
75139
command_line->AppendSwitch(::switches::kEnablePixelOutputInTests);
76140
#endif
@@ -128,6 +192,20 @@ class AIChatUIBrowserTest : public InProcessBrowserTest {
128192
run_loop.Run();
129193
}
130194

195+
void FetchSearchQuerySummary(const base::Location& location,
196+
const std::optional<ai_chat::SearchQuerySummary>&
197+
expected_search_query_summary) {
198+
SCOPED_TRACE(testing::Message() << location.ToString());
199+
base::RunLoop run_loop;
200+
chat_tab_helper_->FetchSearchQuerySummary(base::BindLambdaForTesting(
201+
[&](const std::optional<ai_chat::SearchQuerySummary>&
202+
search_query_summary) {
203+
EXPECT_EQ(search_query_summary, expected_search_query_summary);
204+
run_loop.Quit();
205+
}));
206+
run_loop.Run();
207+
}
208+
131209
protected:
132210
net::test_server::EmbeddedTestServer https_server_;
133211
raw_ptr<ai_chat::AIChatTabHelper> chat_tab_helper_ = nullptr;
@@ -253,3 +331,48 @@ IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, PrintPreviewDisabled) {
253331
NavigateURL(https_server_.GetURL("docs.google.com", "/long_canvas.html"));
254332
FetchPageContent(FROM_HERE, "");
255333
}
334+
335+
IN_PROC_BROWSER_TEST_F(AIChatUIBrowserTest, FetchSearchQuerySummary) {
336+
NavigateURL(https_server_.GetURL("search.brave.com", "/search?q=query"));
337+
338+
// Test when meta tag is not present, should return null result.
339+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
340+
341+
// Test when summarizer-key meta tag is dynamically inserted, should return
342+
// the search query summary from the mock response.
343+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
344+
"var meta = document.createElement('meta');"
345+
"meta.name = 'summarizer-key';"
346+
"meta.content = 'test_key';"
347+
"document.head.appendChild(meta);");
348+
FetchSearchQuerySummary(
349+
FROM_HERE, ai_chat::SearchQuerySummary("test query", "test summary"));
350+
351+
// Mock search query summary response to test parsing.
352+
// Replace the meta tag value to another key.
353+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
354+
"document.querySelector('meta[name=summarizer-"
355+
"key').content = 'not_object';");
356+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
357+
358+
// Replace the meta tag value to error case: conversation empty.
359+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
360+
"document.querySelector('meta[name=summarizer-"
361+
"key').content = 'empty_conversation';");
362+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
363+
364+
// Replace the meta tag value to error case: answer empty.
365+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
366+
"document.querySelector('meta[name=summarizer-"
367+
"key').content = 'empty_answer';");
368+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
369+
370+
// Test non-brave search SERP URL, should return null result.
371+
NavigateURL(https_server_.GetURL("brave.com", "/search?q=query"));
372+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
373+
"var meta = document.createElement('meta');"
374+
"meta.name = 'summarizer-key';"
375+
"meta.content = 'test_key';"
376+
"document.head.appendChild(meta);");
377+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
378+
}

components/ai_chat/content/browser/BUILD.gn

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import("//brave/components/ai_chat/core/common/buildflags/buildflags.gni")
77
import("//brave/components/text_recognition/common/buildflags/buildflags.gni")
8+
import("//tools/json_schema_compiler/json_schema_api.gni")
89

910
assert(enable_ai_chat)
1011

@@ -23,6 +24,7 @@ static_library("browser") {
2324
]
2425

2526
deps = [
27+
":generated_brave_search_responses",
2628
"//base",
2729
"//brave/components/ai_chat/core/browser",
2830
"//brave/components/ai_chat/core/common",
@@ -51,3 +53,10 @@ static_library("browser") {
5153
"//url",
5254
]
5355
}
56+
57+
generated_types("generated_brave_search_responses") {
58+
sources = [ "brave_search_responses.idl" ]
59+
deps = [ "//base" ]
60+
root_namespace = "ai_chat::%(namespace)s"
61+
visibility = [ ":browser" ]
62+
}

components/ai_chat/content/browser/ai_chat_tab_helper.cc

+96
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@
1717
#include "base/notreached.h"
1818
#include "base/ranges/algorithm.h"
1919
#include "base/strings/string_util.h"
20+
#include "base/values.h"
21+
#include "brave/components/ai_chat/content/browser/brave_search_responses.h"
2022
#include "brave/components/ai_chat/content/browser/model_service_factory.h"
2123
#include "brave/components/ai_chat/content/browser/page_content_fetcher.h"
2224
#include "brave/components/ai_chat/content/browser/pdf_utils.h"
2325
#include "brave/components/ai_chat/core/browser/ai_chat_metrics.h"
2426
#include "brave/components/ai_chat/core/browser/constants.h"
27+
#include "brave/components/ai_chat/core/browser/types.h"
28+
#include "brave/components/ai_chat/core/browser/utils.h"
2529
#include "brave/components/ai_chat/core/common/features.h"
2630
#include "brave/components/ai_chat/core/common/mojom/page_content_extractor.mojom.h"
2731
#include "brave/components/ai_chat/core/common/pref_names.h"
@@ -37,6 +41,8 @@
3741
#include "content/public/browser/storage_partition.h"
3842
#include "content/public/browser/web_contents.h"
3943
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
44+
#include "net/base/url_util.h"
45+
#include "net/traffic_annotation/network_traffic_annotation.h"
4046
#include "pdf/buildflags.h"
4147
#include "ui/accessibility/ax_mode.h"
4248
#include "ui/accessibility/ax_updates_and_events.h"
@@ -45,8 +51,46 @@
4551
namespace ai_chat {
4652

4753
namespace {
54+
4855
std::optional<uint32_t> g_max_page_content_length_for_testing;
4956

57+
net::NetworkTrafficAnnotationTag
58+
GetSearchQuerySummaryNetworkTrafficAnnotationTag() {
59+
return net::DefineNetworkTrafficAnnotation("ai_chat_tab_helper", R"(
60+
semantics {
61+
sender: "Brave Leo AI Chat"
62+
description:
63+
"This sender is used to get search query summary from Brave search."
64+
trigger:
65+
"Triggered by uses of Brave Leo AI Chat on Brave Search SERP."
66+
data:
67+
"User's search query and the corresponding summary."
68+
destination: WEBSITE
69+
}
70+
policy {
71+
cookies_allowed: NO
72+
policy_exception_justification:
73+
"Not implemented."
74+
}
75+
)");
76+
}
77+
78+
std::optional<SearchQuerySummary> ParseSearchQuerySummaryResponse(
79+
const base::Value& value) {
80+
auto search_query_response =
81+
brave_search_responses::QuerySummaryResponse::FromValue(value);
82+
if (!search_query_response || search_query_response->conversation.empty()) {
83+
return std::nullopt;
84+
}
85+
86+
const auto& query_summary = search_query_response->conversation[0];
87+
if (query_summary.answer.empty()) {
88+
return std::nullopt;
89+
}
90+
91+
return SearchQuerySummary(query_summary.query, query_summary.answer[0].text);
92+
}
93+
5094
} // namespace
5195

5296
AIChatTabHelper::PDFA11yInfoLoadObserver::PDFA11yInfoLoadObserver(
@@ -345,6 +389,58 @@ void AIChatTabHelper::CheckPDFA11yTree() {
345389
OnPDFA11yInfoLoaded();
346390
}
347391

392+
void AIChatTabHelper::FetchSearchQuerySummary(
393+
FetchSearchQuerySummaryCallback callback) {
394+
if (!IsBraveSearchSERP(GetPageURL())) {
395+
std::move(callback).Run(std::nullopt);
396+
return;
397+
}
398+
399+
auto internal_callback =
400+
base::BindOnce(&AIChatTabHelper::OnSearchSummarizerKeyFetched,
401+
weak_ptr_factory_.GetWeakPtr(), std::move(callback));
402+
GetSearchSummarizerKey(web_contents(), std::move(internal_callback));
403+
}
404+
405+
void AIChatTabHelper::OnSearchSummarizerKeyFetched(
406+
FetchSearchQuerySummaryCallback callback,
407+
const std::optional<std::string>& key) {
408+
if (!key) {
409+
std::move(callback).Run(std::nullopt);
410+
return;
411+
}
412+
413+
if (!api_request_helper_) {
414+
api_request_helper_ =
415+
std::make_unique<api_request_helper::APIRequestHelper>(
416+
GetSearchQuerySummaryNetworkTrafficAnnotationTag(),
417+
web_contents()
418+
->GetBrowserContext()
419+
->GetDefaultStoragePartition()
420+
->GetURLLoaderFactoryForBrowserProcess());
421+
}
422+
423+
GURL base_url("https://search.brave.com/api/chatllm/raw_data");
424+
GURL url = net::AppendQueryParameter(base_url, "key", *key);
425+
426+
auto internal_callback =
427+
base::BindOnce(&AIChatTabHelper::OnSearchQuerySummaryFetched,
428+
weak_ptr_factory_.GetWeakPtr(), std::move(callback));
429+
api_request_helper_->Request("GET", url, "", "application/json",
430+
std::move(internal_callback), {}, {});
431+
}
432+
433+
void AIChatTabHelper::OnSearchQuerySummaryFetched(
434+
FetchSearchQuerySummaryCallback callback,
435+
api_request_helper::APIRequestResult result) {
436+
if (!result.Is2XXResponseCode()) {
437+
std::move(callback).Run(std::nullopt);
438+
return;
439+
}
440+
441+
std::move(callback).Run(ParseSearchQuerySummaryResponse(result.value_body()));
442+
}
443+
348444
WEB_CONTENTS_USER_DATA_KEY_IMPL(AIChatTabHelper);
349445

350446
} // namespace ai_chat

components/ai_chat/content/browser/ai_chat_tab_helper.h

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#define BRAVE_COMPONENTS_AI_CHAT_CONTENT_BROWSER_AI_CHAT_TAB_HELPER_H_
88

99
#include <memory>
10+
#include <optional>
1011
#include <string>
1112
#include <vector>
1213

@@ -16,6 +17,7 @@
1617
#include "brave/components/ai_chat/core/browser/engine/engine_consumer.h"
1718
#include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h"
1819
#include "brave/components/ai_chat/core/common/mojom/page_content_extractor.mojom.h"
20+
#include "brave/components/api_request_helper/api_request_helper.h"
1921
#include "components/favicon/core/favicon_driver_observer.h"
2022
#include "components/prefs/pref_change_registrar.h"
2123
#include "content/public/browser/navigation_handle.h"
@@ -124,6 +126,13 @@ class AIChatTabHelper : public content::WebContentsObserver,
124126
std::string_view invalidation_token) override;
125127
void PrintPreviewFallback(GetPageContentCallback callback) override;
126128
std::u16string GetPageTitle() const override;
129+
void FetchSearchQuerySummary(
130+
FetchSearchQuerySummaryCallback callback) override;
131+
132+
void OnSearchSummarizerKeyFetched(FetchSearchQuerySummaryCallback callback,
133+
const std::optional<std::string>& key);
134+
void OnSearchQuerySummaryFetched(FetchSearchQuerySummaryCallback callback,
135+
api_request_helper::APIRequestResult result);
127136

128137
void BindPageContentExtractorReceiver(
129138
mojo::PendingAssociatedReceiver<mojom::PageContentExtractorHost>
@@ -146,6 +155,9 @@ class AIChatTabHelper : public content::WebContentsObserver,
146155
// A scoper only used for PDF viewing.
147156
std::unique_ptr<content::ScopedAccessibilityMode> scoped_accessibility_mode_;
148157

158+
// Used for fetching search query summary.
159+
std::unique_ptr<api_request_helper::APIRequestHelper> api_request_helper_;
160+
149161
mojo::AssociatedReceiver<mojom::PageContentExtractorHost>
150162
page_content_extractor_receiver_{this};
151163

0 commit comments

Comments
 (0)