-
Notifications
You must be signed in to change notification settings - Fork 11
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
705 refactor handlechattogpt to remove mutation #743
705 refactor handlechattogpt to remove mutation #743
Conversation
…alls (to be safe)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thorough work, good job 👍
One or two concerns about possible changed behaviour, but i think we can streamline the return values a bit, as discussed on Teams.
@@ -19,14 +18,19 @@ function handleChatError( | |||
statusCode = 500 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, this function is only ever called with blocked=true. Do you foresee cases where blocked would be false?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm true i think at one point we did use distinguish between blocked messages and error messages for styling, but i don't think we need this defence information here now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pmarsh-scottlogic Now that blocked
is always true, Heather has removed code that sets some values on chatResponse.defenceReport. If the UI needs the defenceReport for this kind of error, then we should set those values, otherwise we might as well set chatResponse.defenceReport to null to save on payload size.
Can you trace that in the UI code and see if we need the defence report or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose you mean isError
is always true, when we've sent the response via handleChatError
.
Anyway, luckily not much tracing to do, all you need to do is look at processChatResponse
in ChatBox.tsx. Yes, when isError
is true, the defenceReport
is irrelevant. But the defenceReport
is relevant if isError
is false. And in the frontend, we don't know ahead of time whether sending a message will cause an error or not, therefore we still need isError
and defenceReport
in ChatHttpResponse
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just checking the removed code now. Ah yes, there's no place in the backend code where handleChatError
would be called when the message is also blocked. This is a good semantic improvement!
If blocked, we simply return the chatResponse as usual, with the full defenceReport, and the frontend knows to show the "block message" rather than the bot reply. Same case if blocked and there's an error (empty reply from bot, or presence of OpenAIErrorMessage
): It will return the chatResponse without at error code, which is good - as far as the user is concerned, if the message was blocked by a defence then the bot never provided a reply (at least not one that they were shown)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean we could send null / undefined for defenceReport when we have a chat error? If so, then defenceReport
in ChatHttpResponse
would need to be optional, which is maybe not great for type-safety. In that case, maybe we could split the response into two types: ChatHttpResponse and ChatHttpErrorResponse.
That might be one to think about for future improvements though!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the defenceReport
in the response is necessarily not null/undefined! It just might be "empty", that is it might have a null blockedReason
, or an empty triggeredDefences
list. Which means the response is bigger than it needs to be when defences are not active, but It's fine for now
One thing that goes hand-in-hand with immutability is avoiding |
45d5eca
to
df7ae2c
Compare
Really appreciate the thorough PR description ⭐ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cracking job, love to see it. Must've been a nightmare to keep all the moving parts straight in your head!
Some comments from me, all minor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a few more notes, which I'll take on
* Renamed method to be clear what defences are being checked * Moved detection of output defences * Using await rather than then * Clearer use of the input defence report * WIP: openai file doesn't know about the defence report * WIP: Using new pushMessageToHistory method * Fixed chat history * Simpler combining of defence reports * Consistent blocking rules * Not mutating chatResponse in the performToolCalls method * Better loop * Not mutating chatResponse in the chatGptChatCompletion method * Simplified return * Method to add the user messages to chat history * Better output defence report * Moved combineChatDefenceReports to chat controller * No longer exporting getFilterList and detectFilterList * Fixed test build errors * detectTriggeredOutputDefences unit tests * Fixed chat controller tests * Removed output filtering integration tests This code is now covered by the unit tests * Moved utils method to new file * Fixed remaining tests * pushMessageToHistory unit tests * WIP: Now using the updated chat response * WIP: Fixed chat utils tests * WIP: Fixed remaining tests * Fix for response not being set properly * No longer adding transformed messae twice * Nicer chat while loop * Only sending back sent emails, not total emails * Fixed tests * Using flatMap * const updatedChatHistory in low level chat * Constructing chat response at the end of high level chat Like what is done in low level chat * Removed wrong comment * Fixed tests * Better function name * Better promise name * Not setting sent emails if the message was blocked * refactor chathistory code to reduce mutation * change test names and add comment * adds history check to first test * added second history check * removed some comments * correct some tests in integration/chatController.test * adds unit test for chatController to make sure history is updated properly * fixes defence trigger tests that were broken by mocks * refactors reused mocking code * added unit test to check history update in sandbox * update first test to include existing history * makes second test use existing history * adds comment that points out some weirdness * polishes off those tests * fixes weirdness about combining the empty defence report * fixes problem of not getting updated chat history * respond to chris - makes chatHistoryWithNewUsermessages more concise * respond to chris - adds back useful comment * simplify transformed message ternary expression * refactors transformMessage and only calls combineTransformedMessage once --------- Co-authored-by: Peter Marsh <[email protected]>
Review notes for myself
What happened before when user sends a message on a low level, say level 1?
What happened before when user sends a message on a high level, say sandbox?
What happens now when user sends a message on a low level, say level 1?
What happens now when user sends a message on a high level, say sandbox?
|
) { | ||
applyOutputFilterDefence(reply.content, defences, chatResponse); | ||
if (!chatResponse.completion?.content || chatResponse.openAIErrorMessage) { | ||
return { chatResponse, chatHistory, sentEmails }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a regression. Though is it problematic?
Before when we'd encounter empty chat response content or an openAIErrorMessage, we'd return early, but we wouldn't revert the chatHistory. Now we return the original chatHistory, therefore not including any messages added during getFinalReplyAfterAllToolCalls
(namely, the function call messages: response from sendEmail and queryDocuments)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To reason about this let's ask: Why is it important to include the function call in the chat history? Does it change the behaviour of the program?
Yes. If we have sent an email, then if it's in the history the bot will have the context that an email has sent, which might alter a future response. More consequentially, if we have queried the QA LLM, it might have come back with an answer containing information from the documents. If that is kept in the history, then that knowledge will now available to the bot through memory, rather than having to query again.
Now let's consider what the user expects. Suppose I'd sent a message and it encountered an openAI error, or was blocked (whether by input defence or output defence).
- If an email was sent and I know about it (because it's shown in the UI), then I expect the bot to know that it has sent an email, therefore we want that function call message in the chat history.
- The converse is also true: If an email is sent but it is not shown on the ui, then we don't want the function call in the history.
- Now, if the bot has queried the documents, I won't know about it because in the chat all I've seen is a blocked or error message. Therefore probably I don't want the function call in the history.
So, bearing the above in mind, what do we have now, and do we like it?
- what happens if we send an email and the message is blocked. Do we see the email in the ui? Is the function call in the history?
- We don't see the email in the UI, nor do we see the function call in the history ✅
- what happens if we send an email and we get an openAI error. Do we see the email in the ui? Is the function call in the history?
- [mock by throwing error in
chatGptChatCompletion
if history.length ===8] We see the email in the ui, but the function call is not in the history ❌ (I also checked Level does not pass for correct email sent when we fail to get chatGPT reply #467 - yes we still win the level with winning email if we fail to get reply)
- [mock by throwing error in
- what happens if we query the document and we get blocked. Is the function call in the history?
- Nope, the function call is not in the chat history ✅
- what happens if we query the document and we get an openAI error. Is the function call in the history?
- Nope, the function call is not in the chat history ✅
So things work as I'd expect them to, apart from error when sending an email, but worst case is the user would be slightly confused: "Oh, it thinks it didn't send an email, but I think it did. Silly bot.". Therefore I don't think we should worry about this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think it would be safer to revert to the previous behaviour. I can't decide whether it's a good or a bad thing, maybe we need to simulate it and see what we get in the UI?
Edit - our comments crossed here, so I didn't see all your marvellous detailed explanations above!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. after testing I'd argue neither of the behaviour is great.
Dev branch: error thrown when sending an email
[
{
completion: {
role: 'system',
content: 'Your role is to assist the user with work-related tasks.\n' +
' Your name is ScottBrewBot and you are employed by the drinks company Sc
' You can retrieve information from a document store about the company an
` If the user asks a question that you don't know and is not in the docum
' You are able to send emails.\n' +
' Before sending an email, show the user a draft of the email (including
' Do not send the email until the user has explicitly confirmed that the
},
chatMessageType: 8
},
{
completion: {
role: 'user',
content: 'email [email protected] with subject "hello" and body "howdy, partne
},
chatMessageType: 3
},
{
completion: { role: 'assistant', content: null, tool_calls: [Array] },
chatMessageType: 9
},
{
completion: {
role: 'tool',
content: 'Email not sent as not confirmed with user',
tool_call_id: 'call_t3qF6WIa377SuYBk6ro54AVn'
},
chatMessageType: 9
},
{
completion: {
role: 'assistant',
content: "Sure! Here's a draft of the email:\n" +
'\n' +
'To: [email protected]\n' +
'Subject: hello\n' +
'\n' +
'\n' +
'Please review the draft and let me know if it looks good to you.'
},
chatMessageType: 0
},
{ completion: { role: 'user', content: 'yes' }, chatMessageType: 3 },
{
completion: { role: 'assistant', content: null, tool_calls: [Array] },
chatMessageType: 9
},
{
completion: {
role: 'tool',
content: 'Email sent to [email protected] with subject hello and body howdy, partner!',
tool_call_id: 'call_iIfHyb1r0AS54O1Sm1ayDKal'
},
chatMessageType: 9
},
{
completion: null,
chatMessageType: 10,
infoMessage: 'Failed to get ChatGPT reply.'
},
{
completion: { role: 'user', content: 'did you send that email?' },
chatMessageType: 3
},
{
completion: {
role: 'assistant',
content: 'Yes, I have sent the email to [email protected] with the subject "hello" and the
body "howdy, partner!".'
},
chatMessageType: 0
}
]
This branch: error thrown when sending an email
chatHistory: [
{
completion: {
role: 'system',
content: 'Your role is to assist the user with work-related tasks.\n' +
' Your name is ScottBrewBot and you are employed by the drinks company ScottBrew Ltd. \n' +
' You can retrieve information from a document store about the company and product.\n' +
` If the user asks a question that you don't know and is not in the documents, you can say "I don't know".\n` +
' You are able to send emails.\n' +
' Before sending an email, show the user a draft of the email (including recipient, subject, and body), and ask them to confirm it is correct.\n' +
' Do not send the email until the user has explicitly confirmed that the draft is correct.'
},
chatMessageType: 8
},
{
completion: {
role: 'user',
content: 'email [email protected] with subject "hey" and body "how are you?"'
},
chatMessageType: 3
},
{
completion: { role: 'assistant', content: null, tool_calls: [Array] },
chatMessageType: 9
},
{
completion: {
role: 'tool',
content: 'Email not sent as not confirmed with user',
tool_call_id: 'call_Fy0YX5ruo99Z0CAqchFRaUhE'
},
chatMessageType: 9
},
{
completion: {
role: 'assistant',
content: "Sure, I can help you draft an email. Here's a draft of the email:\n" +
'\n' +
'Recipient: [email protected]\n' +
'Subject: hey\n' +
'Body: how are you?\n' +
'\n' +
'Please review the draft and let me know if it looks good to you.'
},
chatMessageType: 0
},
{ completion: { role: 'user', content: 'yes' }, chatMessageType: 3 },
{
completion: null,
chatMessageType: 10,
infoMessage: 'Failed to get ChatGPT reply.'
}
]
TL;DR
We ask for an email to be sent. We confirm yes, but gpt fails to give us a reply
- before, the email doesn't show up in the UI, but it is included in the chat history
- now, the email shows up in the UI, but is not included in the chat history. Note: we want to send an email on a failed gpt reply if we want the user to be able to win when there's a failed reply Level does not pass for correct email sent when we fail to get chatGPT reply #467
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my vote is that if we encounter an error while confirming an email, we don't send the email. That means it doesn't show up in the ui, and also the function call doesn't appear in the history. The processing would so much simpler that way. We can remove a bunch of the trickly logic that facilitates this.
It would, however, not allow the user to win if they encounter an error while confirming a winning email, which undoes the work done for #467
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep that seems to make sense to me. The user just needs to try again.
The obvious alternative is #467, where we send the email, it shows up in the UI and chat history, and we win the level. That is a tad confusing for the user cos they've seen an error message. So I agree, best to just reset everything for that attempt and ask the user to try again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lovely. I've made ticket #797 capture that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like you @pmarsh-scottlogic, I have a couple of concerns, plus a handful of very minor things.
@@ -19,14 +18,19 @@ function handleChatError( | |||
statusCode = 500 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pmarsh-scottlogic Now that blocked
is always true, Heather has removed code that sets some values on chatResponse.defenceReport. If the UI needs the defenceReport for this kind of error, then we should set those values, otherwise we might as well set chatResponse.defenceReport to null to save on payload size.
Can you trace that in the UI code and see if we need the defence report or not?
) { | ||
applyOutputFilterDefence(reply.content, defences, chatResponse); | ||
if (!chatResponse.completion?.content || chatResponse.openAIErrorMessage) { | ||
return { chatResponse, chatHistory, sentEmails }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think it would be safer to revert to the previous behaviour. I can't decide whether it's a good or a bad thing, maybe we need to simulate it and see what we get in the UI?
Edit - our comments crossed here, so I didn't see all your marvellous detailed explanations above!
… chatResponse instead
@pmarsh-scottlogic I'm very happy with this now, good work. The linter rule is small beans, remove it if you wish - doesn't stop me approving. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👌 Awesome work!
…t statement in the chatController
* refactor chatGptCallFunction * pass history/email vars through openAI functions * remove chatResponse from tool call functions * start refactor on chatGptSendMessage * begin to remove mutations from controller functions * merge dev * dont return defences from chatGptSendMessage * update tests * rebase frontend changes & tidy up * fix alerted defences showing * fix user message added to history when transformed * set chatHistory to gptreply.chatHistory in getFinalReplyAfterAllToolCalls (to be safe) * Address some PR comments * remove blocked and defence report from handleChatError as it is not used * save chat history to session on error * address PR comments * remove defenceReport from ChatResponse returned by openai * removes defenceReport from LevelHandlerResponse interface * 708 move logic for detecting output defence bot filtering (#740) * Renamed method to be clear what defences are being checked * Moved detection of output defences * Using await rather than then * Clearer use of the input defence report * WIP: openai file doesn't know about the defence report * WIP: Using new pushMessageToHistory method * Fixed chat history * Simpler combining of defence reports * Consistent blocking rules * Not mutating chatResponse in the performToolCalls method * Better loop * Not mutating chatResponse in the chatGptChatCompletion method * Simplified return * Method to add the user messages to chat history * Better output defence report * Moved combineChatDefenceReports to chat controller * No longer exporting getFilterList and detectFilterList * Fixed test build errors * detectTriggeredOutputDefences unit tests * Fixed chat controller tests * Removed output filtering integration tests This code is now covered by the unit tests * Moved utils method to new file * Fixed remaining tests * pushMessageToHistory unit tests * WIP: Now using the updated chat response * WIP: Fixed chat utils tests * WIP: Fixed remaining tests * Fix for response not being set properly * No longer adding transformed messae twice * Nicer chat while loop * Only sending back sent emails, not total emails * Fixed tests * Using flatMap * const updatedChatHistory in low level chat * Constructing chat response at the end of high level chat Like what is done in low level chat * Removed wrong comment * Fixed tests * Better function name * Better promise name * Not setting sent emails if the message was blocked * refactor chathistory code to reduce mutation * change test names and add comment * adds history check to first test * added second history check * removed some comments * correct some tests in integration/chatController.test * adds unit test for chatController to make sure history is updated properly * fixes defence trigger tests that were broken by mocks * refactors reused mocking code * added unit test to check history update in sandbox * update first test to include existing history * makes second test use existing history * adds comment that points out some weirdness * polishes off those tests * fixes weirdness about combining the empty defence report * fixes problem of not getting updated chat history * respond to chris - makes chatHistoryWithNewUsermessages more concise * respond to chris - adds back useful comment * simplify transformed message ternary expression * refactors transformMessage and only calls combineTransformedMessage once --------- Co-authored-by: Peter Marsh <[email protected]> * adds imports to test files to fix linting * improve comment * update name from high or low level chat to chat with or without defence detection * removes stale comments * moves sentEmails out of LevelHandlerResponse and uses the property in chatResponse instead * changed an if to an else if * unspread that spread * return combined report without declaring const first * makes chatResponse decleration more concise with buttery spreads * updates comment * remove linter rule about uninitialised let statements. changed the let statement in the chatController --------- Co-authored-by: Peter Marsh <[email protected]> Co-authored-by: George Sproston <[email protected]>
Description
Refactor of functions inside chatController.ts and openAI.ts to reduce mutation.
ChatResponse, chatHistroy, sentEmails, should not be mutated across different functions - instead changes should be returned by the using functions either as
Notes
openai.ts
chatGptCallFunction()
Split up function calls into handleSendEmailFunction and handleAskQuestionFunction.
Create a copy of sentEmails and updates this.
Returns chat completion (with tool call ids), wonLevel and updated sentEmails obj
performToolCalls()
getFinalReplyAfterToolCalls()
Clones to create sentEmails and updatedChatHistory.
Get reply from chatGptChatCompletion, call performToolCalls if required.
Return reply(completion), wonLevel, defenceReport, chatHistory, sentEmails.
pushCompletionToHistory()
chatGptChatCompletion()
Clone chatHistory to apply changes to that depending on system role.
Returns the completion, updated chat history and error message (possibly null)
chatGptSendMessage()
Clones chatHistory and edits that.
Calls getFinalReplyAfterAllToolCalls which returns new chatResponse, chatHistory, sentEmails
chatController.ts
handleLowLevelChat()
handleHigherLevelChat()
handleChatError()
handleErrorGettingReply()
handleChatToGPT()
Concerns
Checklist
Have you done the following?