diff --git a/assistants/prospector-assistant/assistant/agents/document_agent.py b/assistants/prospector-assistant/assistant/agents/document_agent.py
index 9ed800ca..c2db86fc 100644
--- a/assistants/prospector-assistant/assistant/agents/document_agent.py
+++ b/assistants/prospector-assistant/assistant/agents/document_agent.py
@@ -761,7 +761,7 @@ async def _gc_attachment_check(
) -> tuple[Status, StepName | None]:
method_metadata_key = "document_agent_gc_response"
- gc_convo_config: GuidedConversationAgentConfigModel = GCAttachmentCheckConfigModel()
+ gc_conversation_config: GuidedConversationAgentConfigModel = GCAttachmentCheckConfigModel()
# get attachment filenames for context
filenames = await self._attachments_extension.get_attachment_filenames(
context, config=config.agents_config.attachment_agent
@@ -769,13 +769,13 @@ async def _gc_attachment_check(
filenames_str = ", ".join(filenames)
filenames_str = "Filenames already attached: " + filenames_str
- gc_convo_config.context = gc_convo_config.context + "\n\n" + filenames_str
+ gc_conversation_config.context = gc_conversation_config.context + "\n\n" + filenames_str
try:
response_message, conversation_status, next_step_name = await GuidedConversationAgent.step_conversation(
config=config,
openai_client=openai_client.create_client(config.service_config),
- agent_config=gc_convo_config,
+ agent_config=gc_conversation_config,
conversation_context=context,
last_user_message=message.content,
)
@@ -990,15 +990,6 @@ async def _draft_content(
context, config=config.agents_config.attachment_agent
)
- # get outline related info
- outline: str | None = None
- content: str | None = None
- # path = _get_document_agent_conversation_storage_path(context)
- if path.exists(storage_directory_for_context(context) / "document_agent/outline.txt"):
- outline = (storage_directory_for_context(context) / "document_agent/outline.txt").read_text()
- if path.exists(storage_directory_for_context(context) / "document_agent/content.txt"):
- content = (storage_directory_for_context(context) / "document_agent/content.txt").read_text()
-
# create chat completion messages
chat_completion_messages: list[ChatCompletionMessageParam] = []
chat_completion_messages.append(_draft_content_main_system_message())
@@ -1006,12 +997,20 @@ async def _draft_content(
_chat_history_system_message(conversation.messages, participants_list.participants)
)
chat_completion_messages.extend(attachment_messages)
- if outline is not None:
- chat_completion_messages.append(_outline_system_message(outline))
- if content is not None: # only grabs previously written content, not all yet.
- chat_completion_messages.append(_content_system_message(content))
+
+ # get outline related info
+ if path.exists(storage_directory_for_context(context) / "document_agent/outline.txt"):
+ document_outline = (storage_directory_for_context(context) / "document_agent/outline.txt").read_text()
+ if document_outline is not None:
+ chat_completion_messages.append(_outline_system_message(document_outline))
+
+ if path.exists(storage_directory_for_context(context) / "document_agent/content.txt"):
+ document_content = (storage_directory_for_context(context) / "document_agent/content.txt").read_text()
+ if document_content is not None: # only grabs previously written content, not all yet.
+ chat_completion_messages.append(_content_system_message(document_content))
# make completion call to openai
+ content: str | None = None
async with openai_client.create_client(config.service_config) as client:
try:
completion_args = {
@@ -1031,21 +1030,22 @@ async def _draft_content(
)
_on_error_metadata_update(metadata, method_metadata_key, config, chat_completion_messages, e)
- # store only latest version for now (will keep all versions later as need arises)
- (storage_directory_for_context(context) / "document_agent/content.txt").write_text(content)
+ if content is not None:
+ # store only latest version for now (will keep all versions later as need arises)
+ (storage_directory_for_context(context) / "document_agent/content.txt").write_text(content)
- # send the response to the conversation only if from a command. Otherwise return info to caller.
- message_type = MessageType.chat
- if message.message_type == MessageType.command:
- message_type = MessageType.command
+ # send the response to the conversation only if from a command. Otherwise return info to caller.
+ message_type = MessageType.chat
+ if message.message_type == MessageType.command:
+ message_type = MessageType.command
- await context.send_messages(
- NewConversationMessage(
- content=content,
- message_type=message_type,
- metadata=metadata,
+ await context.send_messages(
+ NewConversationMessage(
+ content=content,
+ message_type=message_type,
+ metadata=metadata,
+ )
)
- )
return Status.USER_COMPLETED, None
diff --git a/workbench-app/pnpm-lock.yaml b/workbench-app/pnpm-lock.yaml
index 8dedd2cd..ec4dc329 100644
--- a/workbench-app/pnpm-lock.yaml
+++ b/workbench-app/pnpm-lock.yaml
@@ -9205,7 +9205,7 @@ snapshots:
babel-plugin-macros@3.1.0:
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.26.0
cosmiconfig: 7.1.0
resolve: 1.22.8
@@ -9757,7 +9757,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.26.0
csstype: 3.1.3
dompurify@3.1.6: {}
@@ -11609,7 +11609,7 @@ snapshots:
react-error-boundary@3.1.4(react@18.3.1):
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.26.0
react: 18.3.1
react-is@16.13.1: {}
@@ -11682,7 +11682,7 @@ snapshots:
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.26.0
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
@@ -11736,7 +11736,7 @@ snapshots:
redux@4.2.1:
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.26.0
reflect.getprototypeof@1.0.6:
dependencies:
@@ -11764,7 +11764,7 @@ snapshots:
regenerator-transform@0.15.2:
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.26.0
regexp.prototype.flags@1.5.2:
dependencies:
@@ -11871,7 +11871,7 @@ snapshots:
rtl-css-js@1.16.1:
dependencies:
- '@babel/runtime': 7.25.6
+ '@babel/runtime': 7.26.0
run-parallel@1.2.0:
dependencies:
diff --git a/workbench-app/src/Root.tsx b/workbench-app/src/Root.tsx
index 6e328a4c..9d0b7a77 100644
--- a/workbench-app/src/Root.tsx
+++ b/workbench-app/src/Root.tsx
@@ -1,10 +1,11 @@
-import { Toaster } from '@fluentui/react-components';
+import { Link, Popover, PopoverSurface, PopoverTrigger, Toaster } from '@fluentui/react-components';
import debug from 'debug';
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Constants } from './Constants';
import useDragAndDrop from './libs/useDragAndDrop';
import { useKeySequence } from './libs/useKeySequence';
+import { useNotify } from './libs/useNotify';
import { useAppDispatch, useAppSelector } from './redux/app/hooks';
import { setIsDraggingOverBody, toggleDevMode } from './redux/features/app/appSlice';
@@ -29,6 +30,38 @@ export const Root: React.FC = () => {
],
() => dispatch(toggleDevMode()),
);
+ const { notifyError } = useNotify();
+
+ const globalErrorHandler = React.useCallback(
+ (event: PromiseRejectionEvent) => {
+ log('Unhandled promise rejection', event.reason);
+ notifyError({
+ id: ['unhandledrejection', event.reason.message, event.reason.stack].join(':'),
+ title: 'Unhandled error',
+ message: event.reason.message,
+ additionalActions: [
+
+
+ More info
+
+
+ {event.reason.stack}
+
+ ,
+ ],
+ });
+ },
+ [notifyError],
+ );
+
+ React.useEffect(() => {
+ // add a global error handler to catch unhandled promise rejections
+ window.addEventListener('unhandledrejection', globalErrorHandler);
+
+ return () => {
+ window.removeEventListener('unhandledrejection', globalErrorHandler);
+ };
+ }, [globalErrorHandler]);
// ignore file drop events at the document level as this prevents the browser from
// opening the file in the window if the drop event is not handled or the user misses
@@ -44,7 +77,7 @@ export const Root: React.FC = () => {
return (
<>
-
+
>
);
};
diff --git a/workbench-app/src/components/Conversations/SpeechButton.tsx b/workbench-app/src/components/Conversations/SpeechButton.tsx
index b8f17571..c5113852 100644
--- a/workbench-app/src/components/Conversations/SpeechButton.tsx
+++ b/workbench-app/src/components/Conversations/SpeechButton.tsx
@@ -20,7 +20,7 @@ interface SpeechButtonProps {
export const SpeechButton: React.FC = (props) => {
const { disabled, onListeningChange, onSpeechRecognizing, onSpeechRecognized } = props;
const [recognizer, setRecognizer] = React.useState();
- const [isFetching, setIsFetching] = React.useState(false);
+ const [isInitialized, setIsInitialized] = React.useState(false);
const [isListening, setIsListening] = React.useState(false);
const [lastSpeechResultTimestamp, setLastSpeechResultTimestamp] = React.useState(0);
@@ -115,15 +115,17 @@ export const SpeechButton: React.FC = (props) => {
}, [getAzureSpeechTokenAsync, onSpeechRecognized, onSpeechRecognizing]);
React.useEffect(() => {
- // If the recognizer is already available or we are fetching it, do nothing
- if (recognizer || isFetching) return;
+ // If the recognizer is already initialized, return
+ if (isInitialized) return;
- // Indicate that we are fetching the recognizer to prevent multiple fetches
- setIsFetching(true);
+ // Set the recognizer as initialized
+ setIsInitialized(true);
- // Fetch the recognizer, then indicate that we are no longer fetching even if the fetch fails
- getRecognizer().finally(() => setIsFetching(false));
- }, [getRecognizer, isFetching, recognizer]);
+ (async () => {
+ // Fetch the recognizer
+ await getRecognizer();
+ })();
+ }, [getRecognizer, isInitialized, recognizer]);
React.useEffect(() => {
onListeningChange(isListening);
diff --git a/workbench-app/src/libs/useNotify.tsx b/workbench-app/src/libs/useNotify.tsx
index be94e99f..b0acdcb3 100644
--- a/workbench-app/src/libs/useNotify.tsx
+++ b/workbench-app/src/libs/useNotify.tsx
@@ -15,7 +15,7 @@ interface NotifyOptions {
id: string;
title?: string;
message: string;
- details?: string;
+ subtitle?: string;
action?: Slot<'div'> | string;
additionalActions?: React.ReactElement[];
timeout?: number;
@@ -27,7 +27,7 @@ export const useNotify = (toasterId: string = Constants.app.globalToasterId) =>
const notify = React.useCallback(
(options: NotifyOptions) => {
- const { id, title, message, details, action, additionalActions, timeout, intent } = options;
+ const { id, title, message, subtitle, action, additionalActions, timeout, intent } = options;
const getAction = () => {
if (typeof action === 'string') {
@@ -43,7 +43,7 @@ export const useNotify = (toasterId: string = Constants.app.globalToasterId) =>
dispatchToast(
{title}
- {message}
+ {message}
{additionalActions && {additionalActions}}
,
{
diff --git a/workbench-app/src/main.tsx b/workbench-app/src/main.tsx
index a997a763..5e83c404 100644
--- a/workbench-app/src/main.tsx
+++ b/workbench-app/src/main.tsx
@@ -6,7 +6,7 @@ import { initializeFileTypeIcons } from '@fluentui/react-file-type-icons';
import debug from 'debug';
import React from 'react';
import ReactDOM from 'react-dom/client';
-import { Provider } from 'react-redux';
+import { Provider as ReduxProvider } from 'react-redux';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { Constants } from './Constants';
import { Root } from './Root';
@@ -154,7 +154,7 @@ document.addEventListener('DOMContentLoaded', () => {
const root = ReactDOM.createRoot(container);
const app = (
-
+
@@ -167,7 +167,7 @@ document.addEventListener('DOMContentLoaded', () => {
-
+
);
// NOTE: React.StrictMode is used to help catch common issues in the app but will also double-render
diff --git a/workbench-service/semantic_workbench_service/azure_speech.py b/workbench-service/semantic_workbench_service/azure_speech.py
index 0843fb0f..b7ffde47 100644
--- a/workbench-service/semantic_workbench_service/azure_speech.py
+++ b/workbench-service/semantic_workbench_service/azure_speech.py
@@ -1,14 +1,22 @@
+import logging
+
from azure.identity import DefaultAzureCredential
from . import settings
+logger = logging.getLogger(__name__)
+
def get_token() -> dict[str, str]:
if settings.azure_speech.resource_id == "" or settings.azure_speech.region == "":
return {}
credential = DefaultAzureCredential()
- token = credential.get_token("https://cognitiveservices.azure.com/.default").token
+ try:
+ token = credential.get_token("https://cognitiveservices.azure.com/.default").token
+ except Exception as e:
+ logger.error(f"Failed to get token: {e}")
+ return {}
return {
"token": f"aad#{settings.azure_speech.resource_id}#{token}",