From 8b55b9dec23ac71f9ebe583ea0ea198ba5a93dc2 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Wed, 13 May 2026 17:06:34 +0530 Subject: [PATCH 1/7] fix: improve error handling and logging across multiple components --- .../API/KernelMemory/KernelMemory.cs | 9 ++-- .../API/UserInterface/UserInterface.cs | 2 +- .../Helpers/TelemetryHelper.cs | 12 +++++- .../Microsoft.GS.DPS/API/ChatHost/ChatHost.cs | 22 +++++----- .../API/KernelMemory/KernelMemory.cs | 23 +++++++---- .../API/UserInterface/DataCacheManager.cs | 7 +--- .../Storage/AISearch/TagUpdater.cs | 2 + .../BusinessTransactionRepository.cs | 2 +- .../Storage/Documents/DocumentRepository.cs | 4 +- .../src/components/chat/chatRoom.tsx | 41 +------------------ .../src/components/chat/modelSwitch.tsx | 3 -- .../documentViewer/documentViewer.tsx | 2 +- .../src/components/searchBox/searchBox.tsx | 31 +------------- .../src/components/searchResult/old.tsx | 1 - .../src/components/sidecarCopilot/sidecar.tsx | 25 +++-------- App/frontend-app/src/pages/home/home.tsx | 10 ++--- Deployment/validate_bicep_params.py | 2 + 17 files changed, 67 insertions(+), 131 deletions(-) diff --git a/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs b/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs index 128750a7..fd95c81d 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/API/KernelMemory/KernelMemory.cs @@ -49,6 +49,12 @@ ILogger logger try { + if (file == null) + { + logger.LogWarning("[{RequestId}] No file provided in request", requestId); + return Results.BadRequest(new DocumentImportedResult() { DocumentId = string.Empty }); + } + var fileStream = file.OpenReadStream(); //Set Stream Position to 0 fileStream.Seek(0, SeekOrigin.Begin); @@ -457,8 +463,6 @@ ILogger logger //Creating While Loop with 10 mins timeout var timeout = DateTime.UtcNow.AddMinutes(10); - var completeFlag = false; - var status = await kmClient.GetDocumentStatusAsync(documentId); while (DateTime.UtcNow < timeout) @@ -474,7 +478,6 @@ ILogger logger if (status.RemainingSteps.Count == 0) { - completeFlag = true; break; } var totalSteps = status.Steps.Count; diff --git a/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs b/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs index d2311fff..e7b55643 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/API/UserInterface/UserInterface.cs @@ -11,7 +11,7 @@ namespace Microsoft.GS.DPSHost.API { public class UserInterface { - private static Dictionary thumbnails = new Dictionary(); + private static readonly Dictionary thumbnails = new Dictionary(); // Static method to register APIs public static void AddAPIs(WebApplication app) diff --git a/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs b/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs index 6a6bd40a..d8845667 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs @@ -50,10 +50,12 @@ public void TrackEvent(string eventName, Dictionary? properties { _telemetryClient.TrackEvent(eventName, properties, metrics); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to track event: {EventName}", eventName); } + #pragma warning restore CA1031 } /// @@ -73,10 +75,12 @@ public void TrackException(Exception exception, Dictionary? prop { _telemetryClient.TrackException(exception, properties, metrics); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to track exception"); } + #pragma warning restore CA1031 } /// @@ -96,12 +100,14 @@ public void TrackDependency(string dependencyName, string commandName, DateTimeO try { - _telemetryClient.TrackDependency(dependencyName, commandName, startTime, duration, success); + _telemetryClient.TrackDependency("Other", dependencyName, commandName, startTime, duration, success); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to track dependency: {DependencyName}", dependencyName); } + #pragma warning restore CA1031 } /// @@ -143,10 +149,12 @@ public void SetActivityTag(string key, string value) { Activity.Current?.SetTag(key, value); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to set activity tag: {Key}", key); } + #pragma warning restore CA1031 } /// @@ -163,10 +171,12 @@ public void Flush() { _telemetryClient.Flush(); } + #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) { _logger.LogError(ex, "Failed to flush telemetry client"); } + #pragma warning restore CA1031 } } } diff --git a/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs b/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs index ee90e8b9..5a2f3c75 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs @@ -28,17 +28,17 @@ internal static class JsonSerializationOptionsCache public class ChatHost(MemoryWebClient kmClient, Kernel kernel, API.KernelMemory kernelMemory, ChatSessionRepository chatSessions) { - private MemoryWebClient _kmClient = kmClient; - private Kernel _kernel = kernel; - private API.KernelMemory _kernelMemory = kernelMemory; - private IChatCompletionService _chatCompletionService = kernel.GetRequiredService(); - private ChatSessionRepository _chatSessions = chatSessions; - private static string s_systemPrompt; - private static string s_assistancePrompt; - private static string s_additionalPrompt; + private readonly MemoryWebClient _kmClient = kmClient; + private readonly Kernel _kernel = kernel; + private readonly API.KernelMemory _kernelMemory = kernelMemory; + private readonly IChatCompletionService _chatCompletionService = kernel.GetRequiredService(); + private readonly ChatSessionRepository _chatSessions = chatSessions; + private static readonly string s_systemPrompt; + private static readonly string s_assistancePrompt; + private static readonly string s_additionalPrompt; - string sessionId = string.Empty; + readonly string sessionId = string.Empty; ChatHistory chatHistory = null; ChatSession chatSession = null; @@ -49,7 +49,7 @@ static ChatHost() var assemblyLocation = Assembly.GetExecutingAssembly().Location; var assemblyDirectory = System.IO.Path.GetDirectoryName(assemblyLocation); // binding assembly directory with file path (Prompts/Chat_SystemPrompt.txt) - var systemPromptFilePath = System.IO.Path.Combine(assemblyDirectory, "Prompts", "Chat_SystemPrompt.txt"); + var systemPromptFilePath = System.IO.Path.Join(assemblyDirectory, "Prompts", "Chat_SystemPrompt.txt"); ChatHost.s_systemPrompt = System.IO.File.ReadAllText(systemPromptFilePath); ChatHost.s_assistancePrompt = @" @@ -201,6 +201,7 @@ public async Task Chat(ChatRequest chatRequest) Content = "Sorry, your request couldn't be processed as it may contain sensitive or restricted content. Please rephrase your query and try again." }; } + #pragma warning disable CA1031 // Top-level chat-completion safety net: convert any failure to a user-facing fallback response catch(Exception ex) { Console.WriteLine($"unexpected error: {ex.Message}"); @@ -211,6 +212,7 @@ public async Task Chat(ChatRequest chatRequest) }; } + #pragma warning restore CA1031 if (returnedChatMessageContent == null) { returnedChatMessageContent = new ChatMessageContent diff --git a/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs b/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs index 425947b7..ce456271 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs @@ -23,11 +23,11 @@ namespace Microsoft.GS.DPS.API { public class KernelMemory { - private MemoryWebClient _kmClient; - private DocumentRepository _documentRepository; - private DataCacheManager _dataCache; - private TagUpdater _tagUpdator; - private static string keywordExtractorPrompt = ""; + private readonly MemoryWebClient _kmClient; + private readonly DocumentRepository _documentRepository; + private readonly DataCacheManager _dataCache; + private readonly TagUpdater _tagUpdator; + private static readonly string keywordExtractorPrompt = ""; static KernelMemory() { @@ -35,7 +35,7 @@ static KernelMemory() var assemblyLocation = Assembly.GetExecutingAssembly().Location; var assemblyDirectory = System.IO.Path.GetDirectoryName(assemblyLocation); // binding assembly directory with file path (Prompts/KeywordExtract_SystemPrompt.txt) - var systemPromptFilePath = System.IO.Path.Combine(assemblyDirectory, "Prompts", "KeywordExtract_SystemPrompt.txt"); + var systemPromptFilePath = System.IO.Path.Join(assemblyDirectory, "Prompts", "KeywordExtract_SystemPrompt.txt"); KernelMemory.keywordExtractorPrompt = System.IO.File.ReadAllText(systemPromptFilePath); } @@ -139,7 +139,8 @@ private async Task getSummary(string documentId, string fileName) var summaryFile = await _kmClient.ExportFileAsync(documentId, summaryFileName); var summaryFileStream = await summaryFile.GetStreamAsync(); // Read Stream to string - return await new StreamReader(summaryFileStream).ReadToEndAsync(); + using var reader = new StreamReader(summaryFileStream); + return await reader.ReadToEndAsync(); } @@ -151,7 +152,11 @@ private async Task getSummary(string documentId, string fileName) var keywordFile = await _kmClient.ExportFileAsync(documentId, keywordFileName); var keywordFileStream = await keywordFile.GetStreamAsync(); // Read Stream to string - string? keywordContent = await new StreamReader(keywordFileStream).ReadToEndAsync(); + string? keywordContent; + using (var reader = new StreamReader(keywordFileStream)) + { + keywordContent = await reader.ReadToEndAsync(); + } if (string.IsNullOrEmpty(keywordContent)) { @@ -196,10 +201,12 @@ private async Task getSummary(string documentId, string fileName) return keywordDict; } + #pragma warning disable CA1031 // LLM keyword-extraction output may be malformed; fall back to empty result rather than failing the import catch (Exception) { return new Dictionary(); } + #pragma warning restore CA1031 } } diff --git a/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs b/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs index 379298a3..ec2d52b1 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/UserInterface/DataCacheManager.cs @@ -53,12 +53,9 @@ public async Task RefreshCacheAsync() var values = keywordDict.Value.Split(',').Select(v => v.Trim()).ToArray(); - foreach (var value in values) + foreach (var value in values.Where(v => !consolidatedKeywords[keywordDict.Key].Contains(v))) { - if (!consolidatedKeywords[keywordDict.Key].Contains(value)) - { - consolidatedKeywords[keywordDict.Key].Add(value); - } + consolidatedKeywords[keywordDict.Key].Add(value); } consolidatedKeywords[keywordDict.Key] = consolidatedKeywords[keywordDict.Key].OrderBy(v => v).ToList(); diff --git a/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs b/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs index 8c7cb4b4..664d4a53 100644 --- a/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs +++ b/App/backend-api/Microsoft.GS.DPS/Storage/AISearch/TagUpdater.cs @@ -49,10 +49,12 @@ public async Task UpdateTags(string documentId, List updatingTags) var response = await _searchClient.MergeOrUploadDocumentsAsync(new[] { updateDocument }); Console.WriteLine($"Document with ID {document["id"]} updated successfully. - {response.GetRawResponse()}"); } + #pragma warning disable CA1031 // Tag update is best-effort; log and continue with the next document catch (Exception ex) { Console.Error.WriteLine($"Error updating document with ID {document["id"]}: {ex.Message}"); } + #pragma warning restore CA1031 } } } diff --git a/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs b/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs index d12b3092..bdb850cb 100644 --- a/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs +++ b/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs @@ -50,7 +50,7 @@ public async Task> FindAllAsync(ISpecification spe { var collection = _database.GetCollection(typeof(TEntity).Name.ToLowerInvariant()); - GenericSpecification genericSpecification = specification as GenericSpecification; + GenericSpecification genericSpecification = (GenericSpecification)specification; if (genericSpecification.OrderBy == null) { diff --git a/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs b/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs index 413aa26c..e361f114 100644 --- a/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs +++ b/App/backend-api/Microsoft.GS.DPS/Storage/Documents/DocumentRepository.cs @@ -187,9 +187,9 @@ async public Task FindByDocumentIdsAsync(string[] documentIds, if (endDate.HasValue) { - endDate = endDate?.Date.AddHours(23).AddMinutes(59).AddSeconds(59); + var endOfDay = endDate.Value.Date.AddHours(23).AddMinutes(59).AddSeconds(59); var timeFilter = Builders.Filter.Gte(x => x.ImportedTime, startDate ?? DateTime.Now) & - Builders.Filter.Lte(x => x.ImportedTime, endDate.Value); + Builders.Filter.Lte(x => x.ImportedTime, endOfDay); filterDefinition &= timeFilter; } diff --git a/App/frontend-app/src/components/chat/chatRoom.tsx b/App/frontend-app/src/components/chat/chatRoom.tsx index 84bcc84b..f8ba94e0 100644 --- a/App/frontend-app/src/components/chat/chatRoom.tsx +++ b/App/frontend-app/src/components/chat/chatRoom.tsx @@ -9,7 +9,6 @@ import { DialogSurface, DialogTitle, Tag, - makeStyles, } from "@fluentui/react-components"; import { DocDialog } from "../documentViewer/documentViewer"; import { Textarea } from "@fluentai/textarea"; @@ -19,7 +18,7 @@ import { ChatAdd24Regular } from "@fluentui/react-icons"; import styles from "./chatRoom.module.scss"; import { CopilotProvider, Suggestion } from "@fluentai/react-copilot"; //import { getDocument } from "../../api/documentsService"; -import { Completion, PostFeedback } from "../../api/chatService"; +import { Completion } from "../../api/chatService"; import { FeedbackForm } from "./FeedbackForm"; import { Document } from "../../api/apiTypes/documentResults"; import { AppContext } from "../../AppContext"; @@ -29,12 +28,6 @@ import { renderToStaticMarkup } from "react-dom/server"; import { marked } from 'marked'; const DefaultChatModel = "chat_4o"; -const useStyles = makeStyles({ - tooltipContent: { - maxWidth: "500px", - }, -}); - interface ChatRoomProps { searchResultDocuments: Document[]; disableOptionsPanel?: boolean; @@ -52,7 +45,7 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc const [disableSources, setDisableSources] = useState(false); const [model, setModel] = useState("chat_35"); const [source, setSource] = useState("rag"); - const [temperature, setTemperature] = useState(0.8); + const [temperature] = useState(0.8); const [maxTokens] = useState(750); const [selectedDocument, setSelectedDocument] = useState(chatWithDocument); const [button, setButton] = useState(""); @@ -136,31 +129,6 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc currentSessionId = newSessionId; // Immediately use the new session ID in this function } - const markdownToHtmlString = (markdown: string) => { - return renderToStaticMarkup({markdown}); - }; - const markdown = `| Data Point | Value | Document Name | Page Number | -|----------------|-----------|-------------------|------------------| -| Households with accessibility needs | 23.1 million | Accessibility in Housing Report | Page 1 | -| Households with mobility-related disabilities | 19% of U.S. households | Accessibility in Housing Report | Page 1 | -| Households without entry-level bedroom or full bathroom planning to add features | 1% | Accessibility in Housing Report | Page 3 | -| Households planning to make homes more accessible | 5% | Accessibility in Housing Report | Page 3 | -| Households with someone using mobility devices | 13% | Accessibility in Housing Report | Page 1 | -| Households with serious difficulty hearing | 12% | Accessibility in Housing Report | Page 24 | -| Households with serious difficulty seeing | 12% | Accessibility in Housing Report | Page 24 | -| Households with difficulty walking or climbing stairs | 12% | Accessibility in Housing Report | Page 24 | -| Households with difficulty dressing or bathing | 12% | Accessibility in Housing Report | Page 24 | -| Households with difficulty doing errands alone | 12% | Accessibility in Housing Report | Page 24 | -| Households with full bathrooms on entry level | 58% | Accessibility in Housing Report | Page 11 | -| Households with bedrooms on entry level | 46% | Accessibility in Housing Report | Page 11 | -| Total single-family loans acquired by Fannie Mae in 2021 | $2.6 trillion | Annual Housing Report 2022 | Page 36 | -| Total single-family loans acquired by Freddie Mac in 2021 | $2.6 trillion | Annual Housing Report 2022 | Page 36 | -| Percentage of loans with LTV > 95% | 13.5% | Annual Housing Report 2022 | Page 44 | -| Percentage of loans with LTV <= 60% | 15.6% | Annual Housing Report 2022 | Page 44 | -| Total NPLs sold by Enterprises through December 2023 | 168,364 | FHFA Non-Performing Loan Sales Report | Page 2 | -| Average delinquency of NPLs sold | 2.8 years | FHFA Non-Performing Loan Sales Report | Page 2 | -| Average current mark-to-market LTV ratio of NPLs | 83% | FHFA Non-Performing Loan Sales Report | Page 2 |`; - setConversationAnswers((prevAnswers) => [ ...prevAnswers, @@ -285,11 +253,6 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc // } // }; - const handleOpenFeedbackForm = (sources: Reference[]) => { - setReferencesForFeedbackForm(sources); - setIsFeedbackFormOpen(true); - }; - const handleDialogClose = () => { setIsDialogOpen(false); }; diff --git a/App/frontend-app/src/components/chat/modelSwitch.tsx b/App/frontend-app/src/components/chat/modelSwitch.tsx index b494b1e7..60bd00a6 100644 --- a/App/frontend-app/src/components/chat/modelSwitch.tsx +++ b/App/frontend-app/src/components/chat/modelSwitch.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { useTranslation } from "react-i18next"; interface ModelSwitchProps { @@ -6,8 +5,6 @@ interface ModelSwitchProps { } export function ModelSwitch({ onSwitchChange }: ModelSwitchProps) { - const GPT4O = "chat_4o"; - return (
{/*
diff --git a/App/frontend-app/src/components/documentViewer/documentViewer.tsx b/App/frontend-app/src/components/documentViewer/documentViewer.tsx index eb0d0ba3..9959487e 100644 --- a/App/frontend-app/src/components/documentViewer/documentViewer.tsx +++ b/App/frontend-app/src/components/documentViewer/documentViewer.tsx @@ -69,7 +69,7 @@ export function DocDialog( const [pageMetadata] = useState(null); const [iframeKey, setIframeKey] = useState(0); const [isExpanded, setIsExpanded] = useState(false); - const [clearedChatFlag, setClearChatFlag] = useState(clearChatFlag); + const [, setClearChatFlag] = useState(clearChatFlag); const [iframeSrc, setIframeSrc] = useState(undefined); // const [aiKnowledgeMetadata, setAIKnowledgeMetadata] = useState(null); diff --git a/App/frontend-app/src/components/searchBox/searchBox.tsx b/App/frontend-app/src/components/searchBox/searchBox.tsx index 391344b3..69060166 100644 --- a/App/frontend-app/src/components/searchBox/searchBox.tsx +++ b/App/frontend-app/src/components/searchBox/searchBox.tsx @@ -1,13 +1,9 @@ import React, { forwardRef, useImperativeHandle, ChangeEvent, KeyboardEvent, useRef, useState } from "react"; import { Input } from "@fluentui/react-input"; import { useTranslation } from "react-i18next"; -import { Button, InputOnChangeData, Tooltip, useId } from "@fluentui/react-components"; +import { InputOnChangeData, Tooltip, useId } from "@fluentui/react-components"; import { useDebouncedCallback } from "use-debounce"; -import { - Keyboard24Regular, - SearchVisual24Regular, - Search24Regular -} from "@fluentui/react-icons"; +import { Search24Regular } from "@fluentui/react-icons"; import "./searchInput.scss"; import { UploadMultipleFiles } from "../../api/storageService"; @@ -29,29 +25,6 @@ interface SearchBoxProps { const UploadButton = () => { const fileInputRef = useRef(null); - const uploadDocuments = async () => { - if (fileInputRef.current?.files?.length) { - const files = Array.from(fileInputRef.current.files); - const formData = new FormData(); - - files.forEach((file, index) => { - formData.append(`file[${index}]`, file); - }); - - try { - const response = await UploadMultipleFiles(files); - if (!response) { - throw new Error("Error uploading files"); - } - - alert("Files uploaded successfully"); - } catch (error) { - console.error("Error:", error); - alert("Error uploading files"); - } - } - }; - return ( <> {/* diff --git a/App/frontend-app/src/components/searchResult/old.tsx b/App/frontend-app/src/components/searchResult/old.tsx index b9acbc99..9e58f9d3 100644 --- a/App/frontend-app/src/components/searchResult/old.tsx +++ b/App/frontend-app/src/components/searchResult/old.tsx @@ -5,7 +5,6 @@ import { getFileTypeIconProps } from "@fluentui/react-file-type-icons"; import { DocDialog } from "../documentViewer/documentViewer"; import { useEffect, useState } from "react"; import { Document, Tokens } from "../../api/apiTypes/documentResults"; -import { downloadFile } from "../../api/storageService"; interface SearchResultCardProps { document: Document; diff --git a/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx b/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx index 58a6965c..b536dad9 100644 --- a/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx +++ b/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx @@ -7,7 +7,7 @@ import { Document } from "../../api/apiTypes/documentResults"; import { Button } from "@fluentui/react-components"; import { ChatAdd24Regular } from "@fluentui/react-icons"; import { Textarea } from "@fluentai/textarea"; -import { ChatApiResponse, ChatOptions, ChatRequest, History } from "../../api/apiTypes/chatTypes"; +import { ChatApiResponse, ChatRequest, History } from "../../api/apiTypes/chatTypes"; import { Completion } from "../../api/chatService"; import styles from "./sidecar.module.scss"; import { useTranslation } from "react-i18next"; @@ -30,13 +30,11 @@ export function SidecarCopilot({ const [isLoading, setIsLoading] = useState(false); const { conversationAnswers, setConversationAnswers } = useContext(AppContext); - const [model, setModel] = useState("chat_35"); - const [source, setSource] = useState("rag"); - const [button, setButton] = useState(""); - const [temperature] = useState(0.8); - const [maxTokens] = useState(750); + const [, setModel] = useState("chat_35"); + const [, setSource] = useState("rag"); + const [, setButton] = useState(""); const [disableSources, setDisableSources] = useState(false); - const [selectedDocument, setSelectedDocument] = useState([]); + const [, setSelectedDocument] = useState([]); useEffect(() => { setSelectedDocument(chatWithDocument); @@ -108,19 +106,6 @@ export function SidecarCopilot({ } }; - const history: History = conversationAnswers - .map(([prompt, response, userTimestamp, answerTimestamp]) => { - if (response) { - return [ - { role: "user", content: prompt, datetime: userTimestamp }, - { role: "assistant", content: response.answer, datetime: answerTimestamp }, - ]; - } else { - return []; - } - }) - .flat(); - const handleModelChange = (model: string) => { setModel(model); }; diff --git a/App/frontend-app/src/pages/home/home.tsx b/App/frontend-app/src/pages/home/home.tsx index 5b174d36..dcf321be 100644 --- a/App/frontend-app/src/pages/home/home.tsx +++ b/App/frontend-app/src/pages/home/home.tsx @@ -30,7 +30,7 @@ interface HomeProps { isSearchResultsPage?: boolean; } export function Home({ isSearchResultsPage }: HomeProps) { - const [filter, setFilter] = useState({ + const [, setFilter] = useState({ option: null, startDate: null, endDate: null, @@ -68,7 +68,7 @@ export function Home({ isSearchResultsPage }: HomeProps) { const [inOrderBy, setInOrderBy] = useState(""); const [searchResultDocuments, setSearchResultDocuments] = useState([]); - const [selectedDocument, setSelectedDocument] = useState([]); + const [, setSelectedDocument] = useState([]); const { query, setQuery, filters: persistedFilters, setFilters: setPersistedFilters } = useContext(AppContext); const [selectedDateFilter] = useState(null); // const tempFilter = new SearchFacet @@ -247,13 +247,9 @@ export function Home({ isSearchResultsPage }: HomeProps) { setChatWidth(rightWidth); }; - const handleSortSelected = (sort: string) => { - setInOrderBy(sort); - }; - const headerMenuTabsRef = useRef(null); - const [widthClass, setWidthClass] = useState(window.innerWidth > 2000 ? "w-[165%]" : "w-[125%]"); + const [, setWidthClass] = useState(window.innerWidth > 2000 ? "w-[165%]" : "w-[125%]"); useEffect(() => { const filtersFromUrl = searchParams.get("filters"); diff --git a/Deployment/validate_bicep_params.py b/Deployment/validate_bicep_params.py index 467478ba..fcd0b206 100644 --- a/Deployment/validate_bicep_params.py +++ b/Deployment/validate_bicep_params.py @@ -111,6 +111,8 @@ def parse_parameters_env_vars(json_path: Path) -> dict[str, list[str]]: data = json.loads(sanitized) params = data.get("parameters", {}) except json.JSONDecodeError: + # Fall back to an empty params map if JSON parsing fails so the + # caller can still proceed with the regex-based scan over raw text. pass # Walk each top-level parameter and scan its entire serialized value From 53dc28881a8a9ec767c4cf9825c0e77d386fdcbd Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Wed, 13 May 2026 19:02:24 +0530 Subject: [PATCH 2/7] fix: add type check for specification in BusinessTransactionRepository and remove unused import in SearchBox and Sidecar components --- .../Storage/Components/BusinessTransactionRepository.cs | 7 ++++++- App/frontend-app/src/components/searchBox/searchBox.tsx | 1 - App/frontend-app/src/components/sidecarCopilot/sidecar.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs b/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs index bdb850cb..5a38a725 100644 --- a/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs +++ b/App/backend-api/Microsoft.GS.DPS/Storage/Components/BusinessTransactionRepository.cs @@ -50,7 +50,12 @@ public async Task> FindAllAsync(ISpecification spe { var collection = _database.GetCollection(typeof(TEntity).Name.ToLowerInvariant()); - GenericSpecification genericSpecification = (GenericSpecification)specification; + if (specification is not GenericSpecification genericSpecification) + { + throw new ArgumentException( + $"Expected specification of type {nameof(GenericSpecification)}.", + nameof(specification)); + } if (genericSpecification.OrderBy == null) { diff --git a/App/frontend-app/src/components/searchBox/searchBox.tsx b/App/frontend-app/src/components/searchBox/searchBox.tsx index 69060166..054a3138 100644 --- a/App/frontend-app/src/components/searchBox/searchBox.tsx +++ b/App/frontend-app/src/components/searchBox/searchBox.tsx @@ -5,7 +5,6 @@ import { InputOnChangeData, Tooltip, useId } from "@fluentui/react-components"; import { useDebouncedCallback } from "use-debounce"; import { Search24Regular } from "@fluentui/react-icons"; import "./searchInput.scss"; -import { UploadMultipleFiles } from "../../api/storageService"; export interface SearchBoxHandle { setValue(decodedQuery: string): unknown; diff --git a/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx b/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx index b536dad9..a4861ce4 100644 --- a/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx +++ b/App/frontend-app/src/components/sidecarCopilot/sidecar.tsx @@ -7,7 +7,7 @@ import { Document } from "../../api/apiTypes/documentResults"; import { Button } from "@fluentui/react-components"; import { ChatAdd24Regular } from "@fluentui/react-icons"; import { Textarea } from "@fluentai/textarea"; -import { ChatApiResponse, ChatRequest, History } from "../../api/apiTypes/chatTypes"; +import { ChatApiResponse, ChatRequest } from "../../api/apiTypes/chatTypes"; import { Completion } from "../../api/chatService"; import styles from "./sidecar.module.scss"; import { useTranslation } from "react-i18next"; From 171b9c53d84f98cea9f8155bb94fa01559c7e397 Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Thu, 14 May 2026 12:25:15 +0530 Subject: [PATCH 3/7] fix: address Copilot review comments on PR #634 - Remove unused fileInputRef in searchBox.tsx (UploadMultipleFiles already removed) - Drop dead selectedDocument state and its setter call in home.tsx - Drop dead widthClass state and its resize useEffect in home.tsx - Use locally computed sessionId when constructing new ChatSession; remove unused field - Log exception in getKeywords catch block instead of swallowing silently - Use TrackDependency overload that maps wrapper params to telemetry dependencyName/data correctly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/TelemetryHelper.cs | 4 +++- .../Microsoft.GS.DPS/API/ChatHost/ChatHost.cs | 3 +-- .../API/KernelMemory/KernelMemory.cs | 8 ++++++-- .../src/components/searchBox/searchBox.tsx | 6 ++---- App/frontend-app/src/pages/home/home.tsx | 17 ----------------- 5 files changed, 12 insertions(+), 26 deletions(-) diff --git a/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs b/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs index d8845667..fe8a7065 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/Helpers/TelemetryHelper.cs @@ -100,7 +100,9 @@ public void TrackDependency(string dependencyName, string commandName, DateTimeO try { - _telemetryClient.TrackDependency("Other", dependencyName, commandName, startTime, duration, success); + // Overload signature: TrackDependency(dependencyTypeName, target, dependencyName, data, startTime, duration, success) + // Map the wrapper's dependencyName -> telemetry dependencyName (and target), and commandName -> data. + _telemetryClient.TrackDependency("Other", dependencyName, dependencyName, commandName, startTime, duration, resultCode: success ? "0" : "1", success); } #pragma warning disable CA1031 // Telemetry must never fail the calling code path catch (Exception ex) diff --git a/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs b/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs index 5a2f3c75..6d78a50b 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/ChatHost/ChatHost.cs @@ -38,7 +38,6 @@ public class ChatHost(MemoryWebClient kmClient, Kernel kernel, API.KernelMemory private static readonly string s_additionalPrompt; - readonly string sessionId = string.Empty; ChatHistory chatHistory = null; ChatSession chatSession = null; @@ -84,7 +83,7 @@ private async Task makeNewSession(string? chatSessionId) //Create a new ChatSession Entity for Saving into Azure Cosmos return new ChatSession() { - SessionId = this.sessionId, // New Session ID + SessionId = sessionId, // New Session ID StartTime = DateTime.UtcNow // Session Created Time }; diff --git a/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs b/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs index ce456271..f25520a4 100644 --- a/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs +++ b/App/backend-api/Microsoft.GS.DPS/API/KernelMemory/KernelMemory.cs @@ -1,4 +1,5 @@ using DnsClient.Internal; +using Microsoft.Extensions.Logging; using Microsoft.GS.DPS.Images; using Microsoft.GS.DPS.Model.KernelMemory; using Microsoft.GS.DPS.Storage.Document; @@ -27,6 +28,7 @@ public class KernelMemory private readonly DocumentRepository _documentRepository; private readonly DataCacheManager _dataCache; private readonly TagUpdater _tagUpdator; + private readonly ILogger? _logger; private static readonly string keywordExtractorPrompt = ""; static KernelMemory() @@ -39,12 +41,13 @@ static KernelMemory() KernelMemory.keywordExtractorPrompt = System.IO.File.ReadAllText(systemPromptFilePath); } - public KernelMemory(MemoryWebClient kmClient, DocumentRepository documentRepository, DataCacheManager dataCache, TagUpdater tagUpdator) + public KernelMemory(MemoryWebClient kmClient, DocumentRepository documentRepository, DataCacheManager dataCache, TagUpdater tagUpdator, ILogger? logger = null) { _kmClient = kmClient; _documentRepository = documentRepository; _dataCache = dataCache; _tagUpdator = tagUpdator; + _logger = logger; } public async Task ImportDocument(Stream documentStream, @@ -202,8 +205,9 @@ private async Task getSummary(string documentId, string fileName) return keywordDict; } #pragma warning disable CA1031 // LLM keyword-extraction output may be malformed; fall back to empty result rather than failing the import - catch (Exception) + catch (Exception ex) { + _logger?.LogWarning(ex, "Failed to extract keywords for document {DocumentId} ({FileName}); returning empty keyword set.", documentId, fileName); return new Dictionary(); } #pragma warning restore CA1031 diff --git a/App/frontend-app/src/components/searchBox/searchBox.tsx b/App/frontend-app/src/components/searchBox/searchBox.tsx index 054a3138..e0cecad4 100644 --- a/App/frontend-app/src/components/searchBox/searchBox.tsx +++ b/App/frontend-app/src/components/searchBox/searchBox.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useImperativeHandle, ChangeEvent, KeyboardEvent, useRef, useState } from "react"; +import React, { forwardRef, useImperativeHandle, ChangeEvent, KeyboardEvent, useState } from "react"; import { Input } from "@fluentui/react-input"; import { useTranslation } from "react-i18next"; import { InputOnChangeData, Tooltip, useId } from "@fluentui/react-components"; @@ -22,11 +22,9 @@ interface SearchBoxProps { } const UploadButton = () => { - const fileInputRef = useRef(null); - return ( <> - {/* + {/*
} size="large" From 27dc9f887637406acbdec82d1b022c57d1666f9d Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Thu, 4 Jun 2026 11:40:16 +0530 Subject: [PATCH 5/7] feat: Enhance chat functionality with error handling and request tracking --- .../public/locales/en/translation.json | 1 + .../src/api/apiTypes/chatTypes.ts | 3 + .../src/components/chat/chatRoom.tsx | 58 +++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/App/frontend-app/public/locales/en/translation.json b/App/frontend-app/public/locales/en/translation.json index 5eacacba..b8a4cb0b 100644 --- a/App/frontend-app/public/locales/en/translation.json +++ b/App/frontend-app/public/locales/en/translation.json @@ -28,6 +28,7 @@ "Search Results": "Search Results", "selected-documents": "Selected Documents", "fetching-answer": "Fetching answer, please wait...", + "error-fetching-answer": "Sorry, something went wrong while fetching the answer. Please try again.", "test-markdown": "# Dog Breed Comparison: Fluffy Golden Dog vs. Beagle\n\n## Fluffy Golden Dog\n- **Appearance**: Small, light-colored with a fluffy golden coat. Notable features include large brown eyes and prominent ears, one perked and one flopped.\n- **Temperament**: Curious and attentive, suggesting a friendly and engaging personality. The dog's posture indicates comfort and familiarity with its environment.\n- **Collar**: Wears a blue and purple collar with a red identification tag, indicating it is a pet with a caring owner.\n- **Environment**: Typically found in cozy indoor settings, reflecting a strong bond with human companions.\n\n## Beagle\n- **Appearance**: Medium-sized dog with a short, smooth coat that can come in various colors, including tri-color (black, white, and brown). Beagles have long ears and a distinctively expressive face.\n- **Temperament**: Known for being friendly, curious, and energetic. Beagles are often social and enjoy being part of family activities.\n- **Collar**: Commonly wear collars for identification, but styles vary widely.\n- **Environment**: Adaptable to both indoor and outdoor settings, Beagles thrive in active households where they can explore and play.\n\n## Summary\nWhile the fluffy golden dog is characterized by its small size, fluffy coat, and cozy indoor demeanor, the Beagle is a medium-sized, energetic breed known for its short coat and love for outdoor activities. Both breeds exhibit friendly and curious temperaments, making them great companions, but they differ in size, coat type, and typical living environments.", "new-topic": "New Topic", "input-placeholder": "Ask a question or request (ctrl + enter to submit)" diff --git a/App/frontend-app/src/api/apiTypes/chatTypes.ts b/App/frontend-app/src/api/apiTypes/chatTypes.ts index 78c54af9..900f3f48 100644 --- a/App/frontend-app/src/api/apiTypes/chatTypes.ts +++ b/App/frontend-app/src/api/apiTypes/chatTypes.ts @@ -29,6 +29,9 @@ export type ChatApiResponse = { documentIds: string[]; suggestingQuestions: string[]; keywords: string[]; + requestId?: string; + pending?: boolean; + error?: boolean; } export type Reference = { diff --git a/App/frontend-app/src/components/chat/chatRoom.tsx b/App/frontend-app/src/components/chat/chatRoom.tsx index f8ba94e0..c4734dbb 100644 --- a/App/frontend-app/src/components/chat/chatRoom.tsx +++ b/App/frontend-app/src/components/chat/chatRoom.tsx @@ -66,6 +66,10 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc const [allChunkTexts] = useState([]); const { conversationAnswers, setConversationAnswers } = useContext(AppContext); + const hasPendingRequest = conversationAnswers.some( + ([, response]) => response && response.pending === true + ); + const inputDisabled = isLoading || hasPendingRequest; const [isSticky, setIsSticky] = useState(false); const optionsBottom = useRef(null); @@ -130,12 +134,16 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc } + const requestId = uuidv4(); + setConversationAnswers((prevAnswers) => [ ...prevAnswers, [question, { answer: t('components.chat.fetching-answer'), suggestingQuestions: [], documentIds: [], - keywords: [] + keywords: [], + requestId, + pending: true, }], ]); @@ -180,18 +188,56 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc - // Update the conversation with the formatted answer setConversationAnswers((prevAnswers) => { const newAnswers = [...prevAnswers]; - newAnswers[newAnswers.length - 1] = [question, { ...response, answer: chatResp }, userTimestamp, answerTimestamp]; + const idx = newAnswers.findIndex( + ([, r]) => r && r.requestId === requestId + ); + if (idx === -1) { + return prevAnswers; + } + newAnswers[idx] = [ + question, + { ...response, answer: chatResp, requestId, pending: false }, + userTimestamp, + answerTimestamp, + ]; return newAnswers; }); + } else { + throw new Error("Empty response from chat API"); } } catch (error) { console.error("Error parsing response body:", error); + throw error; } } catch (error) { console.error("Error in makeApiRequest:", error); + const answerTimestamp = new Date(); + setConversationAnswers((prevAnswers) => { + const newAnswers = [...prevAnswers]; + const idx = newAnswers.findIndex( + ([, r]) => r && r.requestId === requestId + ); + if (idx === -1) { + return prevAnswers; + } + newAnswers[idx] = [ + question, + { + answer: t('components.chat.error-fetching-answer'), + suggestingQuestions: [], + documentIds: [], + keywords: [], + requestId, + pending: false, + error: true, + }, + userTimestamp, + answerTimestamp, + ]; + return newAnswers; + }); } finally { setIsLoading(false); setTimeout(() => { @@ -367,7 +413,7 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc className="mr-auto" progress={{ value: undefined }} // key={`${index}-chat`} - isLoading={index === conversationAnswers.length - 1 && isLoading} + isLoading={response && response.pending === true} >
From 5cbaaf268febecfa00b597308be9e2cea38b493a Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Thu, 4 Jun 2026 17:58:40 +0530 Subject: [PATCH 6/7] fix: Update ChatApiResponse to ChatUiResponse for improved client state management --- App/frontend-app/src/AppContext.tsx | 10 +++++----- App/frontend-app/src/api/apiTypes/chatTypes.ts | 6 ++++++ App/frontend-app/src/components/chat/chatRoom.tsx | 12 ++++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/App/frontend-app/src/AppContext.tsx b/App/frontend-app/src/AppContext.tsx index cf9526f3..19fa6919 100644 --- a/App/frontend-app/src/AppContext.tsx +++ b/App/frontend-app/src/AppContext.tsx @@ -1,12 +1,12 @@ import { ReactNode, createContext, useState } from 'react'; -import { ChatApiResponse } from './api/apiTypes/chatTypes'; +import { ChatUiResponse } from './api/apiTypes/chatTypes'; export interface IAppContext { - conversationAnswers: [prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][]; + conversationAnswers: [prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][]; setConversationAnswers: ( value: ( - prevState: [prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][] - ) => [prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][] + prevState: [prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][] + ) => [prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][] ) => void; query: string; setQuery: (value: string) => void; @@ -17,7 +17,7 @@ export interface IAppContext { export const AppContext = createContext({} as IAppContext); export const AppContextProvider = ({ children }: { children?: ReactNode }) => { - const [conversationAnswers, setConversationAnswers] = useState<[prompt: string, response: ChatApiResponse, userTimestamp?: Date, answerTimestamp?: Date][]>([]); + const [conversationAnswers, setConversationAnswers] = useState<[prompt: string, response: ChatUiResponse, userTimestamp?: Date, answerTimestamp?: Date][]>([]); const [query, setQuery] = useState(""); const [filters, setFilters] = useState<{ [key: string]: string[] }>({}); // Change this line diff --git a/App/frontend-app/src/api/apiTypes/chatTypes.ts b/App/frontend-app/src/api/apiTypes/chatTypes.ts index 900f3f48..f22141e9 100644 --- a/App/frontend-app/src/api/apiTypes/chatTypes.ts +++ b/App/frontend-app/src/api/apiTypes/chatTypes.ts @@ -29,6 +29,12 @@ export type ChatApiResponse = { documentIds: string[]; suggestingQuestions: string[]; keywords: string[]; +} + +// UI-side wrapper around ChatApiResponse used to track per-message client state +// (e.g., overlapping-request prevention, error rendering). Server responses do +// not include these fields; they are populated and consumed by the chat UI only. +export type ChatUiResponse = ChatApiResponse & { requestId?: string; pending?: boolean; error?: boolean; diff --git a/App/frontend-app/src/components/chat/chatRoom.tsx b/App/frontend-app/src/components/chat/chatRoom.tsx index c4734dbb..09fc1c62 100644 --- a/App/frontend-app/src/components/chat/chatRoom.tsx +++ b/App/frontend-app/src/components/chat/chatRoom.tsx @@ -205,7 +205,11 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc return newAnswers; }); } else { - throw new Error("Empty response from chat API"); + throw new Error( + `Empty response from chat API (received: ${ + response === undefined ? "undefined" : JSON.stringify(response) + })` + ); } } catch (error) { console.error("Error parsing response body:", error); @@ -413,7 +417,7 @@ export function ChatRoom({ searchResultDocuments, selectedDocuments, chatWithDoc className="mr-auto" progress={{ value: undefined }} // key={`${index}-chat`} - isLoading={response && response.pending === true} + isLoading={!!response?.pending} >
( { - if (!isLoading) { + if (!inputDisabled) { handleFollowUpQuestion(followUp); } }} From 1256cc6c6ce4bd47cbae4e79c95dd9e70a33f6da Mon Sep 17 00:00:00 2001 From: Ayaz-Microsoft Date: Thu, 4 Jun 2026 18:13:51 +0530 Subject: [PATCH 7/7] remove comments --- App/frontend-app/src/api/apiTypes/chatTypes.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/App/frontend-app/src/api/apiTypes/chatTypes.ts b/App/frontend-app/src/api/apiTypes/chatTypes.ts index f22141e9..49d7d6e1 100644 --- a/App/frontend-app/src/api/apiTypes/chatTypes.ts +++ b/App/frontend-app/src/api/apiTypes/chatTypes.ts @@ -31,9 +31,6 @@ export type ChatApiResponse = { keywords: string[]; } -// UI-side wrapper around ChatApiResponse used to track per-message client state -// (e.g., overlapping-request prevention, error rendering). Server responses do -// not include these fields; they are populated and consumed by the chat UI only. export type ChatUiResponse = ChatApiResponse & { requestId?: string; pending?: boolean;