Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added text streams to AIChat UI #18376

Merged
merged 17 commits into from
Jun 7, 2023
Merged

Added text streams to AIChat UI #18376

merged 17 commits into from
Jun 7, 2023

Conversation

nullhook
Copy link
Contributor

@nullhook nullhook commented May 5, 2023

Resolves brave/brave-browser#29607

This PR adds support for consuming chunks from the Anthropic completion API, which improves the user experience by allowing messages to be sent to the UI as they come in, rather than sending the entire message as a single unit.

The PR follows the rule of two from the security docs. The SimpleURLLoader relies on the network process, while the data decoder relies on the utility process. We only read the values in the browser process.

Submitter Checklist:

  • I confirm that no security/privacy review is needed, or that I have requested one
  • There is a ticket for my issue
  • Used Github auto-closing keywords in the PR description above
  • Wrote a good PR/commit description
  • Squashed any review feedback or "fixup" commits before merge, so that history is a record of what happened in the repo, not your PR
  • Added appropriate labels (QA/Yes or QA/No; release-notes/include or release-notes/exclude; OS/...) to the associated issue
  • Checked the PR locally:
    • npm run test -- brave_browser_tests, npm run test -- brave_unit_tests wiki
    • npm run lint, npm run presubmit wiki, npm run gn_check, npm run tslint
  • Ran git rebase master (if needed)

Reviewer Checklist:

  • A security review is not needed, or a link to one is included in the PR description
  • New files have MPL-2.0 license header
  • Adequate test coverage exists to prevent regressions
  • Major classes, functions and non-trivial code blocks are well-commented
  • Changes in component dependencies are properly reflected in gn
  • Code follows the style guide
  • Test plan is specified in PR before merging

After-merge Checklist:

Test Plan:

This PR enables streaming response by default. However, it's important to test two cases.

Prereqs:

  • Ensure that the internal VPN is enabled.

Steps:

  1. Visit a news article website, preferably The Verge.
  2. Click on an article of your choice.
  3. Open the sidebar and click on the AIChat icon.
  4. Click on the "Summarize" button.

By default, you should observe the messages coming in real-time in chunks. Additionally, you will see a caret similar to a terminal-like interface.

Let's proceed to test the one-shot response by disabling the ai_chat_sse feature flag.

--enable-features=AIChat:ai_chat_sse\/false

Repeat the test with the same article, and this time the message should be received as a whole, which may take some time.

@github-actions github-actions bot added the CI/storybook-url Deploy storybook and provide a unique URL for each build label May 5, 2023
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@nullhook nullhook force-pushed the ai_chat_streams branch from 3a0474f to 52a0957 Compare May 9, 2023 00:00
@nullhook nullhook marked this pull request as ready for review May 9, 2023 00:00
@nullhook nullhook requested a review from a team as a code owner May 9, 2023 00:00
@nullhook nullhook requested review from sangwoo108 and petemill May 9, 2023 00:00
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@nullhook nullhook force-pushed the ai_chat_streams branch from 52a0957 to 37dc493 Compare May 9, 2023 18:17
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@nullhook nullhook force-pushed the ai_chat_streams branch from 321026a to fce99d6 Compare May 9, 2023 21:25
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

Copy link
Contributor

@sangwoo108 sangwoo108 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for delayed response

// new instance of the process for each call, which may result in unordered
// chunks. Thus, we create a single instance of the parser and reuse it for
// consecutive calls
data_decoder_ = std::make_unique<data_decoder::DataDecoder>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find! 👍 I didn't know about data_decoder::DataDecoder

components/ai_chat/ai_chat_api.h Outdated Show resolved Hide resolved
components/ai_chat/ai_chat_api.h Outdated Show resolved Hide resolved
components/ai_chat/ai_chat_api.h Outdated Show resolved Hide resolved
components/ai_chat/ai_chat_api.h Outdated Show resolved Hide resolved
components/ai_chat/ai_chat_api.cc Outdated Show resolved Hide resolved
components/ai_chat/ai_chat_api.cc Outdated Show resolved Hide resolved
std::string response = result.body();
const base::Value::Dict* dict = result.value_body().GetIfDict();
is_request_in_progress_ = false;
current_request_->reset(nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resetting the SimpleURLLoader doesn't seem to be enough, as the entity in APIRequestHelper::url_loaders_ will remain. I think we should do what APIRequestHelper::Cancel() does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I just had to run the cancel after running the callback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, could you remove this line?

Copy link
Contributor Author

@nullhook nullhook May 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current_request_ is a ::Ticket type which is not a unique_ptr itself but an iterator. When we Cancel() isn't it just removing from the list and the caller is suppose to explicitly reset or reassign to nullptr?

Copy link
Contributor

@sangwoo108 sangwoo108 May 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an entity of list is erased, the entity's desturctor should be called. In this case, the entity is unique_ptr, so it'd free the space for us, isn't it?

Copy link
Contributor

@sangwoo108 sangwoo108 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for delayed response

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

Copy link
Contributor

@sangwoo108 sangwoo108 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with a nit

std::string response = result.body();
const base::Value::Dict* dict = result.value_body().GetIfDict();
is_request_in_progress_ = false;
current_request_->reset(nullptr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, could you remove this line?

components/ai_chat/ai_chat_api.cc Outdated Show resolved Hide resolved
Copy link
Member

@goodov goodov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEPS lgtm with a nit.

@@ -1,4 +1,5 @@
include_rules = [
"+services/data_decoder/public",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls sort

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

Copy link
Member

@petemill petemill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work getting it working! Few comments...

@@ -111,6 +110,18 @@ class APIRequestHelper {
const APIRequestOptions& request_options = {},
ResponseConversionCallback conversion_callback = base::NullCallback());

Ticket Request(const std::string& method,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public function definition could do with comment

Comment on lines 127 to 136
current_request_ = api_request_helper_.Request(
"POST", api_url, CreateJSONRequestBody(dict), "application/json", this,
headers, {},
base::BindOnce(&AIChatAPI::OnResponseStarted,
weak_ptr_factory_.GetWeakPtr()),
base::BindRepeating(&AIChatAPI::OnDownloadProgress,
weak_ptr_factory_.GetWeakPtr()));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the benefit of APIRequestHelper seems to be boilerplate for handling the response where it parses JSON and handles various situations. It doesn't seem worth using it anymore if we're just bypassing it. Wouldn't it be almost just as much code to make a request here via SimpleURLLoader? Or, perhaps more prefably, to put the event-stream handling inside APIRequestHelper. Since it's part of the regular http spec, it seems like it should go there where any consumers can benefit - it already has JSON parsing. What do you think? We could just detect the response header content-type: text/event-stream and do what you have here instead.

Comment on lines 60 to 61
ResponseCallback response_callback_;
CompletionCallback completion_callback_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why store these callbacks here and not pass them through as arguments bound event handlers? Seems like we're deepening the current architectural issue where request and callback state is not cleared when the conversation is changed as a result of a navigation event.

std::move(callback).Run(response, success);
DCHECK(response_callback_);

if (const std::string* completion = result->FindStringKey("completion")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if an error occurs as there is no completion key, shouldn't the callback get notified?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The safe thing to do here is to drop the callback I'd say and debug why there's no completion key.

The callback this function reports to is critical because on the other end it consumes the message which updates the UI.

Comment on lines 222 to 235
void AIChatAPI::OnResponseStarted(
const GURL& final_url,
const network::mojom::URLResponseHead& response_head) {
is_request_in_progress_ = true;
}

void AIChatAPI::OnDownloadProgress(uint64_t current) {
is_request_in_progress_ = true;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not set is_request_in_progress_ to true inside the QueryPrompt function when creating the request?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been handled in a new way in the tab helper itself.

@@ -279,8 +298,8 @@ void AIChatTabHelper::DistillViaAlgorithm(const ui::AXTree& tree) {
void AIChatTabHelper::CleanUp() {
chat_history_.clear();
article_summary_.clear();
SetRequestInProgress(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this line from cleanup deepens the issue where navigating away does not clean up some state. Right now, that problem manifests by any in-progress requests showing their result on another page. Now that problem will also have the UI show as in-progress. If we're going to have AIChatAPI store state and be single-request-only, then perhaps it's time in this PR to add a Cancel() method to it.

DVLOG(1) << __func__ << " Using model: " << model_name;
void AIChatAPI::OnDataReceived(base::StringPiece string_piece,
base::OnceClosure resume) {
std::vector<std::string> stream_data = base::SplitString(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check for a success/error http response code like APIRequestResult can (or re-use that code). Additionally, a request could still come back with the Content-Type response header of anything, e.g. application/json if it's an error. We should check for text/event-stream before assuming.

}
}, [props.list.length, props.isLoading])
}, [props.list.length, props.isLoading, lastConversationEntryElementRef.current?.clientHeight])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning on depending on a ref's child attribute? Changing the value of that won't trigger a react render.

Copy link
Contributor Author

@nullhook nullhook May 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The effect logic here aims to interact with the browser's scroll event rather than triggering a re-render. React will re-render to update the DOM, which can result in changes to the height of the <div>. When such changes occur, the effect should respond by triggering the browser's scroll event.

Perhaps, this can be better using a resize observer.

components/ai_chat/BUILD.gn Outdated Show resolved Hide resolved
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@nullhook nullhook requested a review from petemill June 6, 2023 19:49
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@@ -253,32 +397,45 @@ void APIRequestHelper::OnResponse(
raw_body = converted_body.value();
}

data_decoder::DataDecoder::ParseJsonIsolated(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nullhook
APIRequestHelper is used in multiple components.
That change (ParseJsonIsolated => ParseJson) results in reusing DataDecoder instances in some cases (not only for ai_chat_*) and reduces sanitizing security.

Has that change passed through a sec-review?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we did not review this AFAIK

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, didn't create a sec-review for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI/storybook-url Deploy storybook and provide a unique URL for each build feature/web3/wallet/core feature/web3/wallet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement real-time text streaming in AI Chat
8 participants