diff --git a/assistants/explorer-assistant/assistant/chat.py b/assistants/explorer-assistant/assistant/chat.py index 909a6c9f..05caf06c 100644 --- a/assistants/explorer-assistant/assistant/chat.py +++ b/assistants/explorer-assistant/assistant/chat.py @@ -297,7 +297,10 @@ async def respond_to_conversation( content = completion.choices[0].message.content # get the total tokens used for the completion - completion_total_tokens = completion.usage.total_tokens if completion.usage else None + completion_total_tokens = completion.usage.total_tokens if completion.usage else 0 + footer_items = [ + _get_token_usage_message(config.request_config.max_tokens, completion_total_tokens), + ] # add the completion to the metadata for debugging deepmerge.always_merger.merge( @@ -312,7 +315,8 @@ async def respond_to_conversation( }, "response": completion.model_dump() if completion else "[no response from openai]", }, - } + }, + "footer_items": footer_items, }, ) @@ -420,6 +424,32 @@ async def respond_to_conversation( # +# TODO: move to a common module, such as either the openai_client or attachment module for easy re-use in other assistants +def _get_token_usage_message( + max_tokens: int, + completion_total_tokens: int, +) -> str: + """ + Generate a display friendly message for the token usage, to be added to the footer items. + """ + + def get_display_count(tokens: int) -> str: + # if less than 1k, return the number of tokens + # if greater than or equal to 1k, return the number of tokens in k + # use 1 decimal place for k + # drop the decimal place if the number of tokens in k is a whole number + if tokens < 1000: + return str(tokens) + else: + tokens_in_k = tokens / 1000 + if tokens_in_k.is_integer(): + return f"{int(tokens_in_k)}k" + else: + return f"{tokens_in_k:.1f}k" + + return f"Tokens used: {get_display_count(completion_total_tokens)} of {get_display_count(max_tokens)} ({int(completion_total_tokens / max_tokens * 100)}%)" + + def _format_message(message: ConversationMessage, participants: list[ConversationParticipant]) -> str: """ Format a conversation message for display. diff --git a/workbench-app/docs/MESSAGE_METADATA.md b/workbench-app/docs/MESSAGE_METADATA.md index 53f3fdf3..b615a1de 100644 --- a/workbench-app/docs/MESSAGE_METADATA.md +++ b/workbench-app/docs/MESSAGE_METADATA.md @@ -8,6 +8,8 @@ The app has built-in support for a few metadata child properties, which can be u - `debug`: A dictionary that can contain additional information that can be used for debugging purposes. If included, it will cause the app to display a button that will allow the user to see the contents of the dictionary in a popup for further inspection. +- `footer_items`: A list of strings that will be displayed in the footer of the message. The intent is to allow the sender to include additional information that is not part of the message body, but is still relevant to the message. + Example: ```json @@ -39,7 +41,10 @@ Example: "content": ... } } - } + }, + "footer_items": [ + "6.8k of 50k (13%) tokens used for request", + ] } } ``` diff --git a/workbench-app/src/components/App/AppView.tsx b/workbench-app/src/components/App/AppView.tsx index 7a9b1ac5..5230baa7 100644 --- a/workbench-app/src/components/App/AppView.tsx +++ b/workbench-app/src/components/App/AppView.tsx @@ -8,6 +8,9 @@ import { AppFooter } from './AppFooter'; import { AppHeader } from './AppHeader'; const useClasses = makeStyles({ + body: { + backgroundImage: `url('/assets/background-1.jpg')`, + }, root: { display: 'grid', gridTemplateRows: 'auto 1fr auto', @@ -48,6 +51,13 @@ export const AppView: React.FC = (props) => { const { completedFirstRun } = useAppSelector((state) => state.app); const navigate = useNavigate(); + React.useLayoutEffect(() => { + document.body.className = classes.body; + return () => { + document.body.className = ''; + }; + }, [classes.body]); + React.useEffect(() => { if (!completedFirstRun?.app && window.location.pathname !== '/terms') { navigate('/terms'); diff --git a/workbench-app/src/components/Conversations/InteractMessage.tsx b/workbench-app/src/components/Conversations/InteractMessage.tsx index 54e82492..8f14b444 100644 --- a/workbench-app/src/components/Conversations/InteractMessage.tsx +++ b/workbench-app/src/components/Conversations/InteractMessage.tsx @@ -103,6 +103,14 @@ const useClasses = makeStyles({ alignItems: 'center', ...shorthands.padding(tokens.spacingVerticalXXS, 0, tokens.spacingVerticalXXS, tokens.spacingHorizontalS), }, + footer: { + display: 'flex', + color: tokens.colorNeutralForeground3, + flexDirection: 'row', + gap: tokens.spacingHorizontalS, + alignItems: 'center', + ...shorthands.padding(tokens.spacingVerticalXS, 0, tokens.spacingVerticalXS, tokens.spacingHorizontalS), + }, userContent: { alignItems: 'end', }, @@ -296,31 +304,41 @@ export const InteractMessage: React.FC = (props) => { ); + let footerItems: React.ReactNode | null = null; + if (message.metadata?.['footer_items']) { + // may either be a string or an array of strings + const footerItemsArray = Array.isArray(message.metadata['footer_items']) + ? message.metadata['footer_items'] + : [message.metadata['footer_items']]; + + footerItems = ( + <> + {footerItemsArray.map((item) => ( + + {item} + + ))} + + ); + } + const footerContent = ( +
+ {aiGeneratedDisclaimer} + {footerItems} +
+ ); + return ( <>
{(message.messageType !== 'notice' || (message.messageType === 'notice' && !isUser)) && actions}
{renderedContent}
- {aiGeneratedDisclaimer} + {footerContent} {attachmentList} ); - }, [ - actions, - classes.actions, - classes.attachments, - classes.generated, - classes.innerContent, - classes.noteContent, - classes.noticeContent, - content, - contentClassName, - isUser, - message.filenames, - message.messageType, - message.metadata, - ]); + }, [actions, classes, content, contentClassName, isUser, message.filenames, message.messageType, message.metadata]); const renderedContent = getRenderedMessage(); diff --git a/workbench-app/src/index.css b/workbench-app/src/index.css index 0a65164e..1732f612 100644 --- a/workbench-app/src/index.css +++ b/workbench-app/src/index.css @@ -9,7 +9,6 @@ body { margin: 0; overscroll-behavior: none; - background-image: url('/assets/background-1.jpg'); background-size: cover; background-repeat: no-repeat; background-position: center;