[AIChat] Add NEAR attestation verification (#32370)
* [AIChat] Add NEAR attestation verification * Remove `NEARVerifier`, get verification status from response header * Update to use `near_verification_status` * Address follow-ups
This commit is contained in:
@@ -1247,80 +1247,93 @@ void ConversationHandler::UpdateOrCreateLastAssistantEntry(
|
||||
|
||||
auto& entry = chat_history_.back();
|
||||
auto& event = result.event;
|
||||
if (event->is_completion_event()) {
|
||||
if (!engine_->SupportsDeltaTextResponses() || entry->events->size() == 0 ||
|
||||
!entry->events->back()->is_completion_event()) {
|
||||
// The start of completion responses needs whitespace trim
|
||||
// TODO(petemill): This should happen server-side?
|
||||
event->get_completion_event()->completion = base::TrimWhitespaceASCII(
|
||||
event->get_completion_event()->completion, base::TRIM_LEADING);
|
||||
|
||||
// Only update if verification status is pending, or did not already fail.
|
||||
if (result.is_near_verified.has_value() &&
|
||||
(!entry->near_verification_status ||
|
||||
entry->near_verification_status->verified)) {
|
||||
entry->near_verification_status =
|
||||
mojom::NEARVerificationStatus::New(*result.is_near_verified);
|
||||
}
|
||||
|
||||
if (event) {
|
||||
if (event->is_completion_event()) {
|
||||
if (!engine_->SupportsDeltaTextResponses() ||
|
||||
entry->events->size() == 0 ||
|
||||
!entry->events->back()->is_completion_event()) {
|
||||
// The start of completion responses needs whitespace trim
|
||||
// TODO(petemill): This should happen server-side?
|
||||
event->get_completion_event()->completion = base::TrimWhitespaceASCII(
|
||||
event->get_completion_event()->completion, base::TRIM_LEADING);
|
||||
}
|
||||
|
||||
// Optimize by merging with previous completion events if delta updates
|
||||
// are supported or otherwise replacing the previous event.
|
||||
if (entry->events->size() > 0) {
|
||||
auto& last_event = entry->events->back();
|
||||
if (last_event->is_completion_event()) {
|
||||
// Merge completion events
|
||||
if (engine_->SupportsDeltaTextResponses()) {
|
||||
event->get_completion_event()->completion =
|
||||
base::StrCat({last_event->get_completion_event()->completion,
|
||||
event->get_completion_event()->completion});
|
||||
}
|
||||
// Remove the last event because we'll replace in both delta and
|
||||
// non-delta cases
|
||||
entry->events->pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(petemill): Remove ConversationTurn.text backwards compatibility
|
||||
// when all UI is updated to instead use ConversationEntryEvent items.
|
||||
entry->text = event->get_completion_event()->completion;
|
||||
}
|
||||
|
||||
// Optimize by merging with previous completion events if delta updates
|
||||
// are supported or otherwise replacing the previous event.
|
||||
if (entry->events->size() > 0) {
|
||||
if (event->is_tool_use_event() && entry->events->size() > 0) {
|
||||
// Tool use events can be partial and may need to be combined with the
|
||||
// previous event.
|
||||
auto& last_event = entry->events->back();
|
||||
if (last_event->is_completion_event()) {
|
||||
// Merge completion events
|
||||
if (engine_->SupportsDeltaTextResponses()) {
|
||||
event->get_completion_event()->completion =
|
||||
base::StrCat({last_event->get_completion_event()->completion,
|
||||
event->get_completion_event()->completion});
|
||||
}
|
||||
// Remove the last event because we'll replace in both delta and
|
||||
// non-delta cases
|
||||
entry->events->pop_back();
|
||||
auto& tool_use_event = event->get_tool_use_event();
|
||||
|
||||
DVLOG(2) << __func__
|
||||
<< " Got event for tool use: " << tool_use_event->tool_name
|
||||
<< " is empty? " << tool_use_event->tool_name.empty()
|
||||
<< " with input: " << tool_use_event->arguments_json;
|
||||
|
||||
if (last_event->is_tool_use_event() &&
|
||||
tool_use_event->tool_name.empty()) {
|
||||
last_event->get_tool_use_event()->arguments_json =
|
||||
base::StrCat({last_event->get_tool_use_event()->arguments_json,
|
||||
tool_use_event->arguments_json});
|
||||
// TODO(petemill): Don't clone
|
||||
OnHistoryUpdate(entry.Clone());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(petemill): Remove ConversationTurn.text backwards compatibility when
|
||||
// all UI is updated to instead use ConversationEntryEvent items.
|
||||
entry->text = event->get_completion_event()->completion;
|
||||
}
|
||||
|
||||
if (event->is_tool_use_event() && entry->events->size() > 0) {
|
||||
// Tool use events can be partial and may need to be combined with the
|
||||
// previous event.
|
||||
auto& last_event = entry->events->back();
|
||||
auto& tool_use_event = event->get_tool_use_event();
|
||||
|
||||
DVLOG(2) << __func__
|
||||
<< " Got event for tool use: " << tool_use_event->tool_name
|
||||
<< " is empty? " << tool_use_event->tool_name.empty()
|
||||
<< " with input: " << tool_use_event->arguments_json;
|
||||
|
||||
if (last_event->is_tool_use_event() && tool_use_event->tool_name.empty()) {
|
||||
last_event->get_tool_use_event()->arguments_json =
|
||||
base::StrCat({last_event->get_tool_use_event()->arguments_json,
|
||||
tool_use_event->arguments_json});
|
||||
// TODO(petemill): Don't clone
|
||||
OnHistoryUpdate(entry.Clone());
|
||||
if (event->is_conversation_title_event()) {
|
||||
OnConversationTitleChanged(event->get_conversation_title_event()->title);
|
||||
// Don't add this event to history
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->is_conversation_title_event()) {
|
||||
OnConversationTitleChanged(event->get_conversation_title_event()->title);
|
||||
// Don't add this event to history
|
||||
return;
|
||||
}
|
||||
if (event->is_selected_language_event()) {
|
||||
OnSelectedLanguageChanged(
|
||||
event->get_selected_language_event()->selected_language);
|
||||
// Don't add this event to history
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->is_selected_language_event()) {
|
||||
OnSelectedLanguageChanged(
|
||||
event->get_selected_language_event()->selected_language);
|
||||
// Don't add this event to history
|
||||
return;
|
||||
}
|
||||
if (event->is_content_receipt_event()) {
|
||||
OnConversationTokenInfoChanged(
|
||||
event->get_content_receipt_event()->total_tokens,
|
||||
event->get_content_receipt_event()->trimmed_tokens);
|
||||
// Don't add this event to history
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->is_content_receipt_event()) {
|
||||
OnConversationTokenInfoChanged(
|
||||
event->get_content_receipt_event()->total_tokens,
|
||||
event->get_content_receipt_event()->trimmed_tokens);
|
||||
// Don't add this event to history
|
||||
return;
|
||||
entry->events->push_back(std::move(event));
|
||||
}
|
||||
|
||||
entry->events->push_back(std::move(event));
|
||||
// Update clients for partial entries but not observers, who will get notified
|
||||
// when we know this is a complete entry.
|
||||
OnHistoryUpdate(entry.Clone());
|
||||
@@ -1573,8 +1586,9 @@ void ConversationHandler::OnEngineCompletionComplete(
|
||||
// Handle success, which might mean do nothing much since all data was passed
|
||||
// in the streaming "received" callback.
|
||||
DVLOG(2) << __func__ << ": With value";
|
||||
if (result->event && result->event->is_completion_event() &&
|
||||
!result->event->get_completion_event()->completion.empty()) {
|
||||
if ((result->event && result->event->is_completion_event() &&
|
||||
!result->event->get_completion_event()->completion.empty()) ||
|
||||
result->is_near_verified.has_value()) {
|
||||
UpdateOrCreateLastAssistantEntry(std::move(*result));
|
||||
} else {
|
||||
// This is a workaround for any occasions where the engine returns
|
||||
|
||||
@@ -542,6 +542,72 @@ TEST_F(ConversationHandlerUnitTest, SubmitSelectedText) {
|
||||
ExpectConversationHistoryEquals(FROM_HERE, history, expected_history, false);
|
||||
}
|
||||
|
||||
TEST_F(ConversationHandlerUnitTest, SubmitSelectedText_WithNEARVerification) {
|
||||
MockEngineConsumer* engine = static_cast<MockEngineConsumer*>(
|
||||
conversation_handler_->GetEngineForTesting());
|
||||
|
||||
std::string selected_text = "Verified content.";
|
||||
std::string expected_turn_text =
|
||||
l10n_util::GetStringUTF8(IDS_AI_CHAT_QUESTION_SUMMARIZE_SELECTED_TEXT);
|
||||
const std::string expected_response = "This is verified.";
|
||||
|
||||
EXPECT_CALL(*engine, GenerateAssistantResponse(
|
||||
_, LastTurnHasSelectedText(selected_text), StrEq(""),
|
||||
_, _, _, _, _, _))
|
||||
.WillOnce(::testing::DoAll(
|
||||
base::test::RunOnceCallback<7>(EngineConsumer::GenerationResultData(
|
||||
mojom::ConversationEntryEvent::NewCompletionEvent(
|
||||
mojom::CompletionEvent::New(expected_response)),
|
||||
std::nullopt /* model_key */, true /* is_near_verified */)),
|
||||
base::test::RunOnceCallback<8>(
|
||||
base::ok(EngineConsumer::GenerationResultData(
|
||||
nullptr, std::nullopt /* model_key */,
|
||||
true /* is_near_verified */)))));
|
||||
|
||||
conversation_handler_->associated_content_manager()->ClearContent();
|
||||
|
||||
std::vector<mojom::ConversationTurnPtr> expected_history;
|
||||
|
||||
expected_history.push_back(mojom::ConversationTurn::New(
|
||||
std::nullopt, mojom::CharacterType::HUMAN,
|
||||
mojom::ActionType::SUMMARIZE_SELECTED_TEXT, expected_turn_text,
|
||||
std::nullopt, selected_text, std::nullopt, base::Time::Now(),
|
||||
std::nullopt, std::nullopt, nullptr /* skill */, false,
|
||||
std::nullopt /* model_key */, nullptr /* near_verification_status */));
|
||||
|
||||
std::vector<mojom::ConversationEntryEventPtr> response_events;
|
||||
response_events.push_back(mojom::ConversationEntryEvent::NewCompletionEvent(
|
||||
mojom::CompletionEvent::New(expected_response)));
|
||||
expected_history.push_back(mojom::ConversationTurn::New(
|
||||
std::nullopt, mojom::CharacterType::ASSISTANT,
|
||||
mojom::ActionType::RESPONSE, expected_response, std::nullopt,
|
||||
std::nullopt, std::move(response_events), base::Time::Now(), std::nullopt,
|
||||
std::nullopt, nullptr /* skill */, false, std::nullopt /* model_key */,
|
||||
mojom::NEARVerificationStatus::New(true)));
|
||||
|
||||
NiceMock<MockConversationHandlerClient> client(conversation_handler_.get());
|
||||
EXPECT_CALL(client, OnAPIRequestInProgress(true)).Times(1);
|
||||
EXPECT_CALL(client, OnConversationHistoryUpdate(
|
||||
TurnEq(mojom::ConversationTurnPtr().get())))
|
||||
.Times(1);
|
||||
EXPECT_CALL(client,
|
||||
OnConversationHistoryUpdate(TurnEq(expected_history[1].get())))
|
||||
.Times(3);
|
||||
EXPECT_CALL(client, OnAPIRequestInProgress(false)).Times(1);
|
||||
EXPECT_CALL(*engine, SanitizeInput(StrEq(selected_text)));
|
||||
EXPECT_CALL(*engine, SanitizeInput(StrEq(expected_turn_text)));
|
||||
|
||||
conversation_handler_->SubmitSelectedText(
|
||||
selected_text, mojom::ActionType::SUMMARIZE_SELECTED_TEXT);
|
||||
|
||||
task_environment_.RunUntilIdle();
|
||||
testing::Mock::VerifyAndClearExpectations(&client);
|
||||
|
||||
EXPECT_TRUE(conversation_handler_->HasAnyHistory());
|
||||
const auto& history = conversation_handler_->GetConversationHistory();
|
||||
ExpectConversationHistoryEquals(FROM_HERE, history, expected_history, false);
|
||||
}
|
||||
|
||||
TEST_F(ConversationHandlerUnitTest, SubmitSelectedText_WithAssociatedContent) {
|
||||
// Test with page contents.
|
||||
MockEngineConsumer* engine = static_cast<MockEngineConsumer*>(
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
#include <vector>
|
||||
|
||||
#include "base/check.h"
|
||||
#include "base/command_line.h"
|
||||
#include "base/containers/checked_iterators.h"
|
||||
#include "base/containers/fixed_flat_map.h"
|
||||
#include "base/containers/flat_map.h"
|
||||
#include "base/containers/map_util.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/json/json_writer.h"
|
||||
#include "base/logging.h"
|
||||
@@ -31,11 +31,11 @@
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/types/expected.h"
|
||||
#include "base/values.h"
|
||||
#include "brave/brave_domains/service_domains.h"
|
||||
#include "brave/components/ai_chat/core/browser/ai_chat_credential_manager.h"
|
||||
#include "brave/components/ai_chat/core/browser/engine/conversation_api_parsing.h"
|
||||
#include "brave/components/ai_chat/core/browser/engine/oai_parsing.h"
|
||||
#include "brave/components/ai_chat/core/browser/model_service.h"
|
||||
#include "brave/components/ai_chat/core/browser/utils.h"
|
||||
#include "brave/components/ai_chat/core/common/buildflags/buildflags.h"
|
||||
#include "brave/components/ai_chat/core/common/features.h"
|
||||
#include "brave/components/ai_chat/core/common/mojom/ai_chat.mojom.h"
|
||||
@@ -63,10 +63,6 @@ constexpr char kRemotePath[] = "v1/conversation";
|
||||
|
||||
constexpr char kAllowedWebSourceFaviconHost[] = "imgs.search.brave.com";
|
||||
|
||||
#if !defined(OFFICIAL_BUILD)
|
||||
constexpr char kAIChatServerUrl[] = "ai-chat-server-url";
|
||||
#endif
|
||||
|
||||
net::NetworkTrafficAnnotationTag GetNetworkTrafficAnnotationTag() {
|
||||
return net::DefineNetworkTrafficAnnotation("ai_chat", R"(
|
||||
semantics {
|
||||
@@ -192,33 +188,6 @@ base::Value::List ConversationEventsToList(
|
||||
return events;
|
||||
}
|
||||
|
||||
GURL GetEndpointUrl(bool premium, const std::string& path) {
|
||||
CHECK(!path.starts_with("/"));
|
||||
|
||||
#if !defined(OFFICIAL_BUILD)
|
||||
// If a runtime AI Chat URL is provided, use it.
|
||||
std::string ai_chat_url =
|
||||
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
|
||||
kAIChatServerUrl);
|
||||
if (!ai_chat_url.empty()) {
|
||||
GURL url = GURL(base::StrCat({ai_chat_url, "/", path}));
|
||||
CHECK(url.is_valid()) << "Invalid API Url: " << url.spec();
|
||||
return url;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto* prefix = premium ? "ai-chat-premium.bsg" : "ai-chat.bsg";
|
||||
auto hostname = brave_domains::GetServicesDomain(
|
||||
prefix, brave_domains::ServicesEnvironment::DEV);
|
||||
|
||||
GURL url{base::StrCat(
|
||||
{url::kHttpsScheme, url::kStandardSchemeSeparator, hostname, "/", path})};
|
||||
|
||||
CHECK(url.is_valid()) << "Invalid API Url: " << url.spec();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConversationAPIClient::ConversationEvent::ConversationEvent(
|
||||
@@ -409,6 +378,7 @@ void ConversationAPIClient::OnQueryCompleted(
|
||||
if (success) {
|
||||
std::string completion = "";
|
||||
std::optional<std::string> model_key = std::nullopt;
|
||||
std::optional<bool> is_near_verified = std::nullopt;
|
||||
mojom::ConversationEntryEventPtr completion_event = nullptr;
|
||||
// We're checking for a value body in case for non-streaming API results.
|
||||
// TODO(petemill): server should provide parseable history events even for
|
||||
@@ -428,10 +398,16 @@ void ConversationAPIClient::OnQueryCompleted(
|
||||
}
|
||||
}
|
||||
|
||||
const auto& headers = result.headers();
|
||||
if (const auto* header_value =
|
||||
base::FindOrNull(headers, kBraveNearVerifiedHeader)) {
|
||||
is_near_verified = *header_value == "true";
|
||||
}
|
||||
|
||||
completion_event = mojom::ConversationEntryEvent::NewCompletionEvent(
|
||||
mojom::CompletionEvent::New(completion));
|
||||
GenerationResultData data(std::move(completion_event),
|
||||
std::move(model_key));
|
||||
GenerationResultData data(std::move(completion_event), std::move(model_key),
|
||||
is_near_verified);
|
||||
std::move(callback).Run(base::ok(std::move(data)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ namespace ai_chat {
|
||||
class AIChatCredentialManager;
|
||||
struct CredentialCacheEntry;
|
||||
|
||||
inline constexpr char kBraveNearVerifiedHeader[] = "brave-near-verified";
|
||||
|
||||
// Performs remote request to the remote HTTP Brave Conversation API.
|
||||
class ConversationAPIClient {
|
||||
public:
|
||||
|
||||
@@ -582,6 +582,7 @@ TEST_F(ConversationAPIUnitTest, PerformRequest_PremiumHeaders) {
|
||||
mojom::ConversationEntryEvent::NewCompletionEvent(
|
||||
mojom::CompletionEvent::New("")),
|
||||
std::nullopt));
|
||||
EXPECT_FALSE(result->is_near_verified.has_value());
|
||||
});
|
||||
|
||||
// Begin request
|
||||
@@ -1348,6 +1349,70 @@ TEST_F(ConversationAPIUnitTest,
|
||||
testing::Mock::VerifyAndClearExpectations(mock_request_helper);
|
||||
}
|
||||
|
||||
TEST_F(ConversationAPIUnitTest, PerformRequest_NEARVerification) {
|
||||
std::string expected_completion_response = "Verified response";
|
||||
auto events_and_body = GetMockEventsAndExpectedEventsBody();
|
||||
std::vector<ConversationAPIClient::ConversationEvent> events =
|
||||
std::move(events_and_body.first);
|
||||
|
||||
MockAPIRequestHelper* mock_request_helper =
|
||||
client_->GetMockAPIRequestHelper();
|
||||
testing::StrictMock<MockCallbacks> mock_callbacks;
|
||||
base::RunLoop run_loop;
|
||||
|
||||
EXPECT_CALL(*mock_request_helper, RequestSSE(_, _, _, _, _, _, _, _))
|
||||
.WillOnce([&](const std::string& method, const GURL& url,
|
||||
const std::string& body, const std::string& content_type,
|
||||
DataReceivedCallback data_received_callback,
|
||||
ResultCallback result_callback,
|
||||
const base::flat_map<std::string, std::string>& headers,
|
||||
const api_request_helper::APIRequestOptions& options) {
|
||||
base::Value result(base::Value::Type::DICT);
|
||||
result.GetDict().Set("type", "completion");
|
||||
result.GetDict().Set("model", "llama-3-8b-instruct");
|
||||
result.GetDict().Set("completion", expected_completion_response);
|
||||
data_received_callback.Run(base::ok(std::move(result)));
|
||||
|
||||
base::flat_map<std::string, std::string> response_headers;
|
||||
response_headers[kBraveNearVerifiedHeader] = "true";
|
||||
std::move(result_callback)
|
||||
.Run(api_request_helper::APIRequestResult(200, {}, response_headers,
|
||||
net::OK, GURL()));
|
||||
run_loop.Quit();
|
||||
return Ticket();
|
||||
});
|
||||
|
||||
EXPECT_CALL(mock_callbacks, OnDataReceived(_))
|
||||
.WillOnce([&](EngineConsumer::GenerationResultData result) {
|
||||
ASSERT_TRUE(result.event);
|
||||
EXPECT_TRUE(result.event->is_completion_event());
|
||||
EXPECT_EQ(result.event->get_completion_event()->completion,
|
||||
expected_completion_response);
|
||||
EXPECT_FALSE(result.is_near_verified);
|
||||
});
|
||||
|
||||
EXPECT_CALL(mock_callbacks, OnCompleted(_))
|
||||
.WillOnce([](EngineConsumer::GenerationResult result) {
|
||||
ASSERT_TRUE(result.has_value());
|
||||
EXPECT_TRUE(result.value().is_near_verified.has_value());
|
||||
EXPECT_TRUE(result.value().is_near_verified.value());
|
||||
});
|
||||
|
||||
client_->PerformRequest(
|
||||
std::move(events), "" /* selected_language */,
|
||||
std::nullopt, /* oai_tool_definitions */
|
||||
std::nullopt, /* preferred_tool_name */
|
||||
mojom::ConversationCapability::CONTENT_AGENT,
|
||||
base::BindRepeating(&MockCallbacks::OnDataReceived,
|
||||
base::Unretained(&mock_callbacks)),
|
||||
base::BindOnce(&MockCallbacks::OnCompleted,
|
||||
base::Unretained(&mock_callbacks)));
|
||||
|
||||
run_loop.Run();
|
||||
testing::Mock::VerifyAndClearExpectations(client_.get());
|
||||
testing::Mock::VerifyAndClearExpectations(mock_request_helper);
|
||||
}
|
||||
|
||||
TEST_F(ConversationAPIUnitTest, FailNoConversationEvents) {
|
||||
// Tests handling invalid request parameters
|
||||
std::vector<ConversationAPIClient::ConversationEvent> events;
|
||||
|
||||
@@ -17,8 +17,11 @@ namespace ai_chat {
|
||||
|
||||
EngineConsumer::GenerationResultData::GenerationResultData(
|
||||
mojom::ConversationEntryEventPtr event,
|
||||
std::optional<std::string>&& model_key)
|
||||
: event(std::move(event)), model_key(std::move(model_key)) {}
|
||||
std::optional<std::string>&& model_key,
|
||||
std::optional<bool> is_near_verified)
|
||||
: event(std::move(event)),
|
||||
model_key(std::move(model_key)),
|
||||
is_near_verified(is_near_verified) {}
|
||||
|
||||
EngineConsumer::GenerationResultData::GenerationResultData(
|
||||
GenerationResultData&& other) = default;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -43,7 +44,8 @@ class EngineConsumer {
|
||||
|
||||
struct GenerationResultData {
|
||||
GenerationResultData(mojom::ConversationEntryEventPtr event,
|
||||
std::optional<std::string>&& model_key);
|
||||
std::optional<std::string>&& model_key,
|
||||
std::optional<bool> is_near_verified = std::nullopt);
|
||||
~GenerationResultData();
|
||||
|
||||
GenerationResultData(GenerationResultData&&);
|
||||
@@ -55,6 +57,7 @@ class EngineConsumer {
|
||||
|
||||
mojom::ConversationEntryEventPtr event;
|
||||
std::optional<std::string> model_key;
|
||||
std::optional<bool> is_near_verified;
|
||||
};
|
||||
|
||||
using GenerationResult =
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
#include <vector>
|
||||
|
||||
#include "base/check.h"
|
||||
#include "base/command_line.h"
|
||||
#include "base/containers/flat_map.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/time/time.h"
|
||||
#include "brave/brave_domains/service_domains.h"
|
||||
@@ -253,4 +255,31 @@ SkBitmap ScaleDownBitmap(const SkBitmap& bitmap) {
|
||||
return scaled_bitmap;
|
||||
}
|
||||
|
||||
GURL GetEndpointUrl(bool premium, const std::string& path) {
|
||||
CHECK(!path.starts_with("/"));
|
||||
|
||||
#if !defined(OFFICIAL_BUILD)
|
||||
// If a runtime AI Chat URL is provided, use it.
|
||||
std::string ai_chat_url =
|
||||
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
|
||||
"ai-chat-server-url");
|
||||
if (!ai_chat_url.empty()) {
|
||||
GURL url = GURL(base::StrCat({ai_chat_url, "/", path}));
|
||||
CHECK(url.is_valid()) << "Invalid API Url: " << url.spec();
|
||||
return url;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto* prefix = premium ? "ai-chat-premium.bsg" : "ai-chat.bsg";
|
||||
auto hostname = brave_domains::GetServicesDomain(
|
||||
prefix, brave_domains::ServicesEnvironment::DEV);
|
||||
|
||||
GURL url{base::StrCat(
|
||||
{url::kHttpsScheme, url::kStandardSchemeSeparator, hostname, "/", path})};
|
||||
|
||||
CHECK(url.is_valid()) << "Invalid API Url: " << url.spec();
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
} // namespace ai_chat
|
||||
|
||||
@@ -48,6 +48,8 @@ EngineConsumer::GenerationDataCallback BindParseRewriteReceivedData(
|
||||
// 1024x768
|
||||
SkBitmap ScaleDownBitmap(const SkBitmap& bitmap);
|
||||
|
||||
GURL GetEndpointUrl(bool premium, const std::string& path);
|
||||
|
||||
} // namespace ai_chat
|
||||
|
||||
#endif // BRAVE_COMPONENTS_AI_CHAT_CORE_BROWSER_UTILS_H_
|
||||
|
||||
+2
@@ -20,6 +20,7 @@ type RatingStatus = (typeof statuses)[number]
|
||||
interface ContextActionsAssistantProps {
|
||||
turnUuid?: string
|
||||
turnModelKey?: string
|
||||
turnNEARVerified?: boolean
|
||||
onEditAnswerClicked?: () => void
|
||||
onCopyTextClicked?: () => void
|
||||
}
|
||||
@@ -121,6 +122,7 @@ export default function ContextActionsAssistant(
|
||||
onRegenerate={handleRegenerateAnswer}
|
||||
leoModels={leoModels}
|
||||
turnModelKey={props.turnModelKey}
|
||||
turnNEARVerified={props.turnNEARVerified}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+3
@@ -379,6 +379,9 @@ function ConversationEntries() {
|
||||
<ContextActionsAssistant
|
||||
turnUuid={firstEntryEdit.uuid}
|
||||
turnModelKey={turnModelKey}
|
||||
turnNEARVerified={
|
||||
group.at(-1)?.nearVerificationStatus?.verified
|
||||
}
|
||||
onEditAnswerClicked={
|
||||
canEditEntry
|
||||
? () => setEditInputId(index)
|
||||
|
||||
+12
-2
@@ -13,6 +13,7 @@ import * as Mojom from '../../../common/mojom'
|
||||
import {
|
||||
ModelMenuItem, //
|
||||
} from '../../../page/components/model_menu_item/model_menu_item'
|
||||
import { NearLabel } from '../../../page/components/near_label/near_label'
|
||||
import styles from './style.module.scss'
|
||||
|
||||
interface Props {
|
||||
@@ -22,11 +23,19 @@ interface Props {
|
||||
onRegenerate: (selectedModelKey: string) => void
|
||||
leoModels: Mojom.Model[]
|
||||
turnModelKey: string
|
||||
turnNEARVerified?: boolean
|
||||
}
|
||||
|
||||
export function RegenerateAnswerMenu(props: Props) {
|
||||
const { isOpen, onOpen, onClose, onRegenerate, leoModels, turnModelKey } =
|
||||
props
|
||||
const {
|
||||
isOpen,
|
||||
onOpen,
|
||||
onClose,
|
||||
onRegenerate,
|
||||
leoModels,
|
||||
turnModelKey,
|
||||
turnNEARVerified,
|
||||
} = props
|
||||
|
||||
const modelDisplayName =
|
||||
leoModels.find((model) => model.key === turnModelKey)?.displayName ?? ''
|
||||
@@ -68,6 +77,7 @@ export function RegenerateAnswerMenu(props: Props) {
|
||||
>
|
||||
<div className={styles.anchorButtonContent}>
|
||||
<span className={styles.anchorButtonText}>{modelDisplayName}</span>
|
||||
{turnNEARVerified && <NearLabel />}
|
||||
<Icon
|
||||
name='carat-down'
|
||||
className={classnames({
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@
|
||||
|
||||
.anchorButtonContent {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
align-items: center;
|
||||
gap: var(--leo-spacing-s);
|
||||
overflow: hidden;
|
||||
|
||||
Reference in New Issue
Block a user