Skip to content

Commit 494f3a1

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 ConversationTurn 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, or navigated to a new page.
1 parent bd20b9b commit 494f3a1

38 files changed

+935
-56
lines changed

browser/ai_chat/ai_chat_ui_browsertest.cc

+130-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=%7Btest_key%7D") {
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=%7Bnot_object%7D") {
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=%7Bempty_conversation%7D") {
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=%7Bempty_answer%7D") {
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,54 @@ 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+
// Test empty summarizer-key meta tag, should return null result.
352+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
353+
"document.querySelector('meta[name=summarizer-"
354+
"key').content = '';");
355+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
356+
357+
// Mock search query summary response to test parsing.
358+
// Replace the meta tag value to another key.
359+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
360+
"document.querySelector('meta[name=summarizer-"
361+
"key').content = '{not_object}';");
362+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
363+
364+
// Replace the meta tag value to error case: conversation empty.
365+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
366+
"document.querySelector('meta[name=summarizer-"
367+
"key').content = '{empty_conversation}';");
368+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
369+
370+
// Replace the meta tag value to error case: answer empty.
371+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
372+
"document.querySelector('meta[name=summarizer-"
373+
"key').content = '{empty_answer}';");
374+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
375+
376+
// Test non-brave search SERP URL, should return null result.
377+
NavigateURL(https_server_.GetURL("brave.com", "/search?q=query"));
378+
content::ExecuteScriptAsync(GetActiveWebContents()->GetPrimaryMainFrame(),
379+
"var meta = document.createElement('meta');"
380+
"meta.name = 'summarizer-key';"
381+
"meta.content = '{test_key}';"
382+
"document.head.appendChild(meta);");
383+
FetchSearchQuerySummary(FROM_HERE, std::nullopt);
384+
}

browser/ai_chat/android/ai_chat_utils_android.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ static void JNI_BraveLeoUtils_OpenLeoQuery(
3232
mojom::CharacterType::HUMAN, mojom::ActionType::QUERY,
3333
mojom::ConversationTurnVisibility::VISIBLE,
3434
base::android::ConvertJavaStringToUTF8(query), std::nullopt, std::nullopt,
35-
base::Time::Now(), std::nullopt);
35+
base::Time::Now(), std::nullopt, false);
3636
chat_tab_helper->SubmitHumanConversationEntry(std::move(turn));
3737
#endif
3838
}

browser/ai_chat/page_content_fetcher_browsertest.cc

+47
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,20 @@ class PageContentFetcherBrowserTest : public InProcessBrowserTest {
123123
run_loop.Run();
124124
}
125125

126+
void GetSearchSummarizerKey(const base::Location& location,
127+
const std::optional<std::string>& expected_key) {
128+
SCOPED_TRACE(testing::Message() << location.ToString());
129+
base::RunLoop run_loop;
130+
ai_chat::GetSearchSummarizerKey(
131+
browser()->tab_strip_model()->GetActiveWebContents(),
132+
base::BindLambdaForTesting(
133+
[&run_loop, &expected_key](const std::optional<std::string>& key) {
134+
EXPECT_EQ(expected_key, key);
135+
run_loop.Quit();
136+
}));
137+
run_loop.Run();
138+
}
139+
126140
// Handles returning a .patch file if the user is on a github.com pull request
127141
void SetGithubInterceptor() {
128142
GURL expected_patch_url =
@@ -219,3 +233,36 @@ IN_PROC_BROWSER_TEST_F(PageContentFetcherBrowserTest, FetchPageContentPDF) {
219233
EXPECT_EQ(1, browser()->tab_strip_model()->active_index());
220234
run_loop->Run();
221235
}
236+
237+
IN_PROC_BROWSER_TEST_F(PageContentFetcherBrowserTest, GetSearchSummarizerKey) {
238+
auto remove_first_summarizer_key = [&]() {
239+
content::ExecuteScriptAsync(
240+
GetActiveWebContents()->GetPrimaryMainFrame(),
241+
"document.getElementsByName('summarizer-key')[0].remove()");
242+
};
243+
244+
NavigateURL(https_server_.GetURL("a.com", "/summarizer_key_meta.html"));
245+
GetSearchSummarizerKey(FROM_HERE,
246+
R"({"query":"test","results_hash":"hash"})");
247+
248+
// Test meta in two other formats.
249+
remove_first_summarizer_key();
250+
GetSearchSummarizerKey(FROM_HERE,
251+
R"({"query":"test2","results_hash":"hash"})");
252+
253+
remove_first_summarizer_key();
254+
GetSearchSummarizerKey(FROM_HERE,
255+
R"({"query":"test3","results_hash":"hash"})");
256+
257+
// Test meta with other attribute.
258+
remove_first_summarizer_key();
259+
GetSearchSummarizerKey(FROM_HERE, R"({"test"})");
260+
261+
// Test meta with empty key.
262+
remove_first_summarizer_key();
263+
GetSearchSummarizerKey(FROM_HERE, std::nullopt);
264+
265+
// Test meta with empty key and other atribute.
266+
remove_first_summarizer_key();
267+
GetSearchSummarizerKey(FROM_HERE, std::nullopt);
268+
}

browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ void AIChatUIPageHandler::SubmitHumanConversationEntry(
143143
mojom::ConversationTurnPtr turn = mojom::ConversationTurn::New(
144144
CharacterType::HUMAN, mojom::ActionType::UNSPECIFIED,
145145
ConversationTurnVisibility::VISIBLE, input, std::nullopt, std::nullopt,
146-
base::Time::Now(), std::nullopt);
146+
base::Time::Now(), std::nullopt, false);
147147
active_chat_tab_helper_->SubmitHumanConversationEntry(std::move(turn));
148148
}
149149

chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ void ChromeAutocompleteProviderClient::OpenLeo(const std::u16string& query) {
7979
ai_chat::mojom::ConversationTurnVisibility::VISIBLE,
8080
base::UTF16ToUTF8(query) /* text */, std::nullopt /* selected_text */,
8181
std::nullopt /* events */, base::Time::Now(),
82-
std::nullopt /* edits */);
82+
std::nullopt /* edits */, false /* from_brave_search_SERP */);
8383

8484
chat_tab_helper->SubmitHumanConversationEntry(std::move(turn));
8585

components/ai_chat/content/browser/BUILD.gn

+10
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,7 +24,9 @@ static_library("browser") {
2324
]
2425

2526
deps = [
27+
":generated_brave_search_responses",
2628
"//base",
29+
"//brave/brave_domains",
2730
"//brave/components/ai_chat/core/browser",
2831
"//brave/components/ai_chat/core/common",
2932
"//brave/components/ai_chat/core/common/buildflags",
@@ -51,3 +54,10 @@ static_library("browser") {
5154
"//url",
5255
]
5356
}
57+
58+
generated_types("generated_brave_search_responses") {
59+
sources = [ "brave_search_responses.idl" ]
60+
deps = [ "//base" ]
61+
root_namespace = "ai_chat::%(namespace)s"
62+
visibility = [ ":browser" ]
63+
}

0 commit comments

Comments
 (0)