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

[Odysseus] Overhauling framework in line with backend services #78989

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Overhauling/rewriting Odysseus framework
  • Loading branch information
AllTerrainDeveloper committed Jul 5, 2023
commit 316e346f3cc101301a013f9b667cab81ef2604db
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const EmailListInactiveItem = ( { domain, source }: EmailListInactiveItemProps )
onClick={ () => {
sendNudge( {
nudge: 'email-comparison',
initialMessage: `I see you want to an email provider to your domain ${ domain.name }. I can give you a few tips on how to do that.`,
initialMessage: `I see you want to add an email provider to your domain ${ domain.name }. I can give you a few tips on how to do that.`,
context: {
domain: domain.name,
},
Expand Down
66 changes: 55 additions & 11 deletions client/odysseus/context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,46 @@ export type Nudge = {
initialMessage: string;
context?: Record< string, unknown >;
};

export type Message = {
content: string;
role: 'user' | 'assistant';
};

export type Chat = {
chatId?: string | null;
messages: Message[];
};

interface OdysseusAssistantContextInterface {
currentView: string;
setCurrentView: ( currentView: string ) => void;
addMessage: ( content: string, role: 'user' | 'assistant', chatId?: string | null ) => void;
chat: Chat;
isLoadingChat: boolean;
lastNudge: Nudge | null;
messages: Message[];
sectionName: string;
sendNudge: ( nudge: Nudge ) => void;
lastNudge: Nudge | null;
setChat: ( chat: Chat ) => void;
setIsLoadingChat: ( isLoadingChat: boolean ) => void;
setMessages: ( messages: Message[] ) => void;
}

const defaultCurrentViewContextInterface = {
currentView: '',
setCurrentView: noop,
const defaultContextInterfaceValues = {
addMessage: noop,
chat: { messages: [] },
isLoadingChat: false,
lastNudge: null,
messages: [],
sectionName: '',
sendNudge: noop,
lastNudge: null,
setChat: noop,
setIsLoadingChat: noop,
setMessages: noop,
};

// Create a new context
// Create a default new context
const OdysseusAssistantContext = createContext< OdysseusAssistantContextInterface >(
defaultCurrentViewContextInterface
defaultContextInterfaceValues
);

// Custom hook to access the OdysseusAssistantContext
Expand All @@ -41,12 +62,35 @@ const OdysseusAssistantProvider = ( {
sectionName: string;
children: ReactNode;
} ) => {
const [ currentView, setCurrentView ] = useState( '' );
const [ lastNudge, setLastNudge ] = useState< Nudge | null >( null );
const [ messages, setMessages ] = useState< Message[] >( [] );
const [ chat, setChat ] = useState< Chat >( { messages: [] } );

const addMessage = ( content: string, role: 'user' | 'assistant', chatId?: string | null ) => {
setMessages( ( prevMessages ) => {
const newMessages = [ ...prevMessages, { content, role } as Message ];
setChat( ( prevChat ) => ( {
chatId: chatId ?? prevChat.chatId,
messages: newMessages,
} ) );
return newMessages;
} );
};

return (
<OdysseusAssistantContext.Provider
value={ { currentView, setCurrentView, sectionName, sendNudge: setLastNudge, lastNudge } }
value={ {
addMessage,
chat,
isLoadingChat: false,
lastNudge,
messages,
sectionName,
sendNudge: setLastNudge,
setChat,
setIsLoadingChat: noop,
setMessages,
} }
>
{ children }
</OdysseusAssistantContext.Provider>
Expand Down
55 changes: 33 additions & 22 deletions client/odysseus/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,41 @@ import { useRef, useEffect, useState } from 'react';
import { useSelector } from 'calypso/state';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
import { useOdysseusAssistantContext } from './context';
import { useOddyseusEndpointPost } from './query';
import { useOddyseusSendMessage } from './query';
import WapuuRibbon from './wapuu-ribbon';
import type { Message } from './query';

import './style.scss';

const OdysseusAssistant = () => {
const siteId = useSelector( getSelectedSiteId );
const { lastNudge, sectionName } = useOdysseusAssistantContext();
const { lastNudge, chat, isLoadingChat, addMessage, messages, setMessages } =
useOdysseusAssistantContext();
const [ input, setInput ] = useState( '' );
const [ isVisible, setIsVisible ] = useState( false );
const [ isLoading, setIsLoading ] = useState( false );
const [ isNudging, setIsNudging ] = useState( false );
const { mutateAsync } = useOddyseusEndpointPost( siteId );
const [ messages, setMessages ] = useState< Message[] >( [
{ content: 'Hello, I am Wapuu! Your personal assistant.', role: 'assistant' },
] );
const { mutateAsync: sendOdysseusMessage } = useOddyseusSendMessage( siteId );

const environmentBadge = document.querySelector( 'body > .environment-badge' );

// Clear messages when switching sections
useEffect( () => {
setMessages( [] );
}, [ sectionName ] );
if ( isLoadingChat ) {
setMessages( [ { content: 'Remembering any previous conversation...', role: 'assistant' } ] );
} else if ( ! chat ) {
setMessages( [
{ content: 'Hello, I am Wapuu! Your personal assistant.', role: 'assistant' },
] );
} else if ( chat ) {
setMessages( chat.messages );
}
}, [ chat, isLoadingChat, setMessages ] );

const addMessage = ( content: string, role: 'user' | 'assistant' ) => {
setMessages( ( prevMessages ) => [ ...prevMessages, { content, role } ] );
};
const environmentBadge = document.querySelector( 'body > .environment-badge' );

const messagesEndRef = useRef< HTMLDivElement | null >( null );

useEffect( () => {
messagesEndRef.current?.scrollIntoView( { behavior: 'smooth' } );
}, [ messages ] );

useEffect( () => {
if ( lastNudge ) {
setMessages( [
Expand All @@ -60,7 +61,7 @@ const OdysseusAssistant = () => {
setMessages( [
{ content: 'Hello, I am Wapuu! Your personal assistant.', role: 'assistant' },
] );
}, [ lastNudge ] );
}, [ lastNudge, setMessages ] );

const handleMessageChange = ( text: string ) => {
setInput( text );
Expand All @@ -70,19 +71,19 @@ const OdysseusAssistant = () => {
try {
setIsLoading( true );
addMessage( input, 'user' );

setInput( '' );
const response = await mutateAsync( {
prompt: input,
const response = await sendOdysseusMessage( {
message: { content: input, role: 'user' },
context: lastNudge ?? {
nudge: 'none',
context: {},
initialMessage: 'Hello, I am Wapuu, your personal WordPress assistant',
},
messages,
} );

addMessage( response, 'assistant' );
} catch ( _ ) {
addMessage( response.message.content, 'assistant', response.chatId );
} catch ( e ) {
addMessage(
"Wapuu oopsie! 😺 My bad, but even cool pets goof. Let's laugh it off! 🎉, ask me again as I forgot what you said!",
'assistant'
Expand All @@ -107,6 +108,11 @@ const OdysseusAssistant = () => {

function handleFormSubmit( event: React.FormEvent ) {
event.preventDefault();

if ( isLoading ) {
return;
}

handleSendMessage();
}

Expand Down Expand Up @@ -145,7 +151,12 @@ const OdysseusAssistant = () => {
onChange={ handleMessageChange }
onKeyDown={ handleKeyDown }
/>
<Button onClick={ handleSendMessage } className="chatbox-send-btn" type="button">
<Button
disabled={ isLoading }
onClick={ handleSendMessage }
className="chatbox-send-btn"
type="button"
>
Send
</Button>
</div>
Expand Down
67 changes: 27 additions & 40 deletions client/odysseus/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,47 @@
import { useQuery, useMutation, UseMutationResult } from '@tanstack/react-query';
import { useMutation, UseMutationResult } from '@tanstack/react-query';
import wpcom from 'calypso/lib/wp';
import { Nudge, useOdysseusAssistantContext } from '../context';
import type { Message } from '../context';

function queryValidate( siteId: number | null ) {
const path = `/sites/${ siteId }/odysseus`;
return wpcom.req.get( { path, apiNamespace: 'wpcom/v2' } );
}

export type Message = {
content: string;
role: 'user' | 'assistant';
};

// It just checks that the endpoint is working
export const useOddyseusEndpointSanityCheck = ( siteId: number | null ) => {
return useQuery( {
queryKey: [ 'oddyseus-assistant-validation', siteId ],
queryFn: () => queryValidate( siteId ),
staleTime: 5 * 60 * 1000,
enabled: siteId !== null,
} );
};

function postOddyseus(
function odysseusSendMessage(
siteId: number | null,
prompt: string,
messages: Message[],
context: Nudge,
messages: Message[] = [],
sectionName: string
sectionName: string,
chatId?: string | null
) {
const path = `/sites/${ siteId }/odysseus`;
const path = `/odysseus/send_message`;
return wpcom.req.post( {
path,
apiNamespace: 'wpcom/v2',
body: { prompt, context, messages, sectionName },
body: { messages, context, sectionName, siteId, chatId },
} );
}

export const useOddyseusEndpointPost = (
// It will post a new message using the current chatId
export const useOddyseusSendMessage = (
siteId: number | null
): UseMutationResult<
string,
{ chatId: string; message: Message },
unknown,
{ prompt: string; context: Nudge; messages: Message[] }
{ message: Message; context: Nudge }
> => {
const { sectionName } = useOdysseusAssistantContext();
const { sectionName, chat, messages, setChat } = useOdysseusAssistantContext();

return useMutation( {
mutationFn: ( {
prompt,
context,
messages,
}: {
prompt: string;
context: Nudge;
messages: Message[];
} ) => postOddyseus( siteId, prompt, context, messages, sectionName ),
mutationFn: ( { message, context }: { message: Message; context: Nudge } ) => {
// If chatId is defined, we only send the message to the current chat
// Otherwise we send previous messages and the new one appended to the end to the server
const messagesToSend = chat?.chatId ? [ message ] : [ ...messages, message ];

return odysseusSendMessage( siteId, messagesToSend, context, sectionName, chat.chatId );
},
onSuccess: ( data ) => {
// After a successful mutation, update the chat state with the new chatId
//It should be immediate (eg using the useState parameter of the hook) (only update chatId)
setChat( { ...chat, chatId: data.chatId } );
},
} );
};

// TODO: We will add lately a clear chat to forget the session