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 all commits
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
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
72 changes: 61 additions & 11 deletions client/odysseus/context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,52 @@ export type Nudge = {
initialMessage: string;
context?: Record< string, unknown >;
};

export type MessageRole = 'user' | 'bot';
Copy link
Contributor

Choose a reason for hiding this comment

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

Updated this to match the enum declaration on the server
https://github.com/Automattic/ai-services/blob/trunk/odysseus/app/schemas.py#L15-L18


export type MessageType = 'message' | 'action' | 'meta' | 'error';
Copy link
Contributor

Choose a reason for hiding this comment

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


export type Message = {
content: string;
role: MessageRole;
type: MessageType;
chatId?: string | null;
};
Comment on lines +17 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

updated type definition


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

interface OdysseusAssistantContextInterface {
currentView: string;
setCurrentView: ( currentView: string ) => void;
addMessage: ( message: Message ) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Simplified params to use Message type

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 +68,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 = ( message: Message ) => {
setMessages( ( prevMessages ) => {
const newMessages = [ ...prevMessages, message ];
setChat( ( prevChat ) => ( {
chatId: message.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
83 changes: 54 additions & 29 deletions client/odysseus/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,50 @@ 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: 'bot', type: 'message' },
] );
} else if ( ! chat ) {
setMessages( [
{ content: 'Hello, I am Wapuu! Your personal assistant.', role: 'bot', type: 'message' },
] );
} 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( [
{
content: lastNudge.initialMessage,
role: 'assistant',
role: 'bot',
type: 'message',
},
] );

Expand All @@ -58,9 +62,9 @@ const OdysseusAssistant = () => {
}

setMessages( [
{ content: 'Hello, I am Wapuu! Your personal assistant.', role: 'assistant' },
{ content: 'Hello, I am Wapuu! Your personal assistant.', role: 'bot', type: 'message' },
] );
}, [ lastNudge ] );
}, [ lastNudge, setMessages ] );

const handleMessageChange = ( text: string ) => {
setInput( text );
Expand All @@ -69,24 +73,35 @@ const OdysseusAssistant = () => {
const handleSendMessage = async () => {
try {
setIsLoading( true );
addMessage( input, 'user' );
addMessage( {
content: input,
role: 'user',
type: 'message',
} );

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

addMessage( response, 'assistant' );
} catch ( _ ) {
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'
);
addMessage( {
content: response.message.content,
role: 'bot',
type: 'message',
chatId: response.chatId,
} );
} catch ( e ) {
addMessage( {
content:
"Wapuu oopsie! 😺 My bad, but even cool pets goof. Let's laugh it off! 🎉, ask me again as I forgot what you said!",
role: 'bot',
type: 'message',
} );
} finally {
setIsLoading( false );
}
Expand All @@ -107,6 +122,11 @@ const OdysseusAssistant = () => {

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

if ( isLoading ) {
return;
}

handleSendMessage();
}

Expand Down Expand Up @@ -145,7 +165,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
65 changes: 25 additions & 40 deletions client/odysseus/query/index.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,45 @@
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 ) => {
setChat( { ...chat, chatId: data.chatId } );
},
} );
};

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