mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-20 04:37:09 +02:00
Feature/indexing errors (#2148)
* backend changes to handle partial completion of index attempts * typo fix * Display partial success in UI * make log timing more readable by limiting printed precision to milliseconds * forgot alembic * initial cut at "completed with errors" indexing * remove and reorganize unused imports * show view errors while indexing is in progress * code review fixes
This commit is contained in:
@@ -18,7 +18,8 @@ import { PageSelector } from "@/components/PageSelector";
|
||||
import { localizeAndPrettify } from "@/lib/time";
|
||||
import { getDocsProcessedPerMinute } from "@/lib/indexAttempt";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { CheckmarkIcon, CopyIcon } from "@/components/icons/icons";
|
||||
import { CheckmarkIcon, CopyIcon, SearchIcon } from "@/components/icons/icons";
|
||||
import Link from "next/link";
|
||||
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
|
||||
|
||||
const NUM_IN_PAGE = 8;
|
||||
@@ -50,7 +51,7 @@ export function IndexingAttemptsTable({ ccPair }: { ccPair: CCPairFullInfo }) {
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>New Doc Cnt</TableHeaderCell>
|
||||
<TableHeaderCell>Total Doc Cnt</TableHeaderCell>
|
||||
<TableHeaderCell>Error Msg</TableHeaderCell>
|
||||
<TableHeaderCell>Error Message</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -93,9 +94,31 @@ export function IndexingAttemptsTable({ ccPair }: { ccPair: CCPairFullInfo }) {
|
||||
<TableCell>{indexAttempt.total_docs_indexed}</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<Text className="flex flex-wrap whitespace-normal">
|
||||
{indexAttempt.error_msg || "-"}
|
||||
</Text>
|
||||
{indexAttempt.error_count > 0 && (
|
||||
<Link
|
||||
className="cursor-pointer my-auto"
|
||||
href={`/admin/indexing/${indexAttempt.id}`}
|
||||
>
|
||||
<Text className="flex flex-wrap text-link whitespace-normal">
|
||||
<SearchIcon />
|
||||
View Errors
|
||||
</Text>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{indexAttempt.status === "success" && (
|
||||
<Text className="flex flex-wrap whitespace-normal">
|
||||
{"-"}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{indexAttempt.status === "failed" &&
|
||||
indexAttempt.error_msg && (
|
||||
<Text className="flex flex-wrap whitespace-normal">
|
||||
{indexAttempt.error_msg}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{indexAttempt.full_exception_trace && (
|
||||
<div
|
||||
onClick={() => {
|
||||
|
189
web/src/app/admin/indexing/[id]/IndexAttemptErrorsTable.tsx
Normal file
189
web/src/app/admin/indexing/[id]/IndexAttemptErrorsTable.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { CheckmarkIcon, CopyIcon } from "@/components/icons/icons";
|
||||
import { localizeAndPrettify } from "@/lib/time";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Text,
|
||||
} from "@tremor/react";
|
||||
import { useState } from "react";
|
||||
import { IndexAttemptError } from "./types";
|
||||
|
||||
const NUM_IN_PAGE = 8;
|
||||
|
||||
export function CustomModal({
|
||||
isVisible,
|
||||
onClose,
|
||||
title,
|
||||
content,
|
||||
showCopyButton = false,
|
||||
}: {
|
||||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
content: string;
|
||||
showCopyButton?: boolean;
|
||||
}) {
|
||||
const [copyClicked, setCopyClicked] = useState(false);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width="w-4/6"
|
||||
className="h-5/6 overflow-y-hidden flex flex-col"
|
||||
title={title}
|
||||
onOutsideClick={onClose}
|
||||
>
|
||||
<div className="overflow-y-auto mb-6">
|
||||
{showCopyButton && (
|
||||
<div className="mb-6">
|
||||
{!copyClicked ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(content);
|
||||
setCopyClicked(true);
|
||||
setTimeout(() => setCopyClicked(false), 2000);
|
||||
}}
|
||||
className="flex w-fit cursor-pointer hover:bg-hover-light p-2 border-border border rounded"
|
||||
>
|
||||
Copy full content
|
||||
<CopyIcon className="ml-2 my-auto" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-fit hover:bg-hover-light p-2 border-border border rounded cursor-default">
|
||||
Copied to clipboard
|
||||
<CheckmarkIcon
|
||||
className="my-auto ml-2 flex flex-shrink-0 text-success"
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="whitespace-pre-wrap">{content}</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function IndexAttemptErrorsTable({
|
||||
indexAttemptErrors,
|
||||
}: {
|
||||
indexAttemptErrors: IndexAttemptError[];
|
||||
}) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [modalData, setModalData] = useState<{
|
||||
id: number | null;
|
||||
title: string;
|
||||
content: string;
|
||||
} | null>(null);
|
||||
const closeModal = () => setModalData(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalData && (
|
||||
<CustomModal
|
||||
isVisible={!!modalData}
|
||||
onClose={closeModal}
|
||||
title={modalData.title}
|
||||
content={modalData.content}
|
||||
showCopyButton
|
||||
/>
|
||||
)}
|
||||
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Timestamp</TableHeaderCell>
|
||||
<TableHeaderCell>Batch Number</TableHeaderCell>
|
||||
<TableHeaderCell>Document Summaries</TableHeaderCell>
|
||||
<TableHeaderCell>Error Message</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{indexAttemptErrors
|
||||
.slice(NUM_IN_PAGE * (page - 1), NUM_IN_PAGE * page)
|
||||
.map((indexAttemptError) => {
|
||||
return (
|
||||
<TableRow key={indexAttemptError.id}>
|
||||
<TableCell>
|
||||
{indexAttemptError.time_created
|
||||
? localizeAndPrettify(indexAttemptError.time_created)
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell>{indexAttemptError.batch_number}</TableCell>
|
||||
<TableCell>
|
||||
{indexAttemptError.doc_summaries && (
|
||||
<div
|
||||
onClick={() =>
|
||||
setModalData({
|
||||
id: indexAttemptError.id,
|
||||
title: "Document Summaries",
|
||||
content: JSON.stringify(
|
||||
indexAttemptError.doc_summaries,
|
||||
null,
|
||||
2
|
||||
),
|
||||
})
|
||||
}
|
||||
className="mt-2 text-link cursor-pointer select-none"
|
||||
>
|
||||
View Document Summaries
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<Text className="flex flex-wrap whitespace-normal">
|
||||
{indexAttemptError.error_msg || "-"}
|
||||
</Text>
|
||||
{indexAttemptError.traceback && (
|
||||
<div
|
||||
onClick={() =>
|
||||
setModalData({
|
||||
id: indexAttemptError.id,
|
||||
title: "Exception Traceback",
|
||||
content: indexAttemptError.traceback!,
|
||||
})
|
||||
}
|
||||
className="mt-2 text-link cursor-pointer select-none"
|
||||
>
|
||||
View Full Trace
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{indexAttemptErrors.length > NUM_IN_PAGE && (
|
||||
<div className="mt-3 flex">
|
||||
<div className="mx-auto">
|
||||
<PageSelector
|
||||
totalPages={Math.ceil(indexAttemptErrors.length / NUM_IN_PAGE)}
|
||||
currentPage={page}
|
||||
onPageChange={(newPage) => {
|
||||
setPage(newPage);
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
3
web/src/app/admin/indexing/[id]/lib.ts
Normal file
3
web/src/app/admin/indexing/[id]/lib.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function buildIndexingErrorsUrl(id: string | number) {
|
||||
return `/api/manage/admin/indexing-errors/${id}`;
|
||||
}
|
58
web/src/app/admin/indexing/[id]/page.tsx
Normal file
58
web/src/app/admin/indexing/[id]/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { Title } from "@tremor/react";
|
||||
import useSWR from "swr";
|
||||
import { IndexAttemptErrorsTable } from "./IndexAttemptErrorsTable";
|
||||
import { buildIndexingErrorsUrl } from "./lib";
|
||||
import { IndexAttemptError } from "./types";
|
||||
|
||||
function Main({ id }: { id: number }) {
|
||||
const {
|
||||
data: indexAttemptErrors,
|
||||
isLoading,
|
||||
error,
|
||||
} = useSWR<IndexAttemptError[]>(
|
||||
buildIndexingErrorsUrl(id),
|
||||
errorHandlingFetcher
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <ThreeDotsLoader />;
|
||||
}
|
||||
|
||||
if (error || !indexAttemptErrors) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle={`Failed to fetch errors for attempt ID ${id}`}
|
||||
errorMsg={error?.info?.detail || error.toString()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackButton />
|
||||
<div className="mt-6">
|
||||
<div className="flex">
|
||||
<Title>Indexing Errors for Attempt {id}</Title>
|
||||
</div>
|
||||
<IndexAttemptErrorsTable indexAttemptErrors={indexAttemptErrors} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page({ params }: { params: { id: string } }) {
|
||||
const id = parseInt(params.id);
|
||||
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<Main id={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
15
web/src/app/admin/indexing/[id]/types.ts
Normal file
15
web/src/app/admin/indexing/[id]/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface IndexAttemptError {
|
||||
id: number;
|
||||
index_attempt_id: number;
|
||||
batch_number: number;
|
||||
doc_summaries: DocumentErrorSummary[];
|
||||
error_msg: string;
|
||||
traceback: string;
|
||||
time_created: string;
|
||||
}
|
||||
|
||||
export interface DocumentErrorSummary {
|
||||
id: string;
|
||||
semantic_id: string;
|
||||
section_link: string;
|
||||
}
|
@@ -42,6 +42,26 @@ export function IndexAttemptStatus({
|
||||
} else {
|
||||
badge = icon;
|
||||
}
|
||||
} else if (status === "completed_with_errors") {
|
||||
const icon = (
|
||||
<Badge size={size} color="yellow" icon={FiAlertTriangle}>
|
||||
Completed with errors
|
||||
</Badge>
|
||||
);
|
||||
badge = (
|
||||
<HoverPopup
|
||||
mainContent={<div className="cursor-pointer">{icon}</div>}
|
||||
popupContent={
|
||||
<div className="w-64 p-2 break-words overflow-hidden whitespace-normal">
|
||||
The indexing attempt completed, but some errors were encountered
|
||||
during the run.
|
||||
<br />
|
||||
<br />
|
||||
Click View Errors for more details.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else if (status === "success") {
|
||||
badge = (
|
||||
<Badge size={size} color="green" icon={FiCheckCircle}>
|
||||
|
@@ -38,6 +38,7 @@ export interface MinimalUserSnapshot {
|
||||
export type ValidInputTypes = "load_state" | "poll" | "event";
|
||||
export type ValidStatuses =
|
||||
| "success"
|
||||
| "completed_with_errors"
|
||||
| "failed"
|
||||
| "in_progress"
|
||||
| "not_started";
|
||||
@@ -59,6 +60,7 @@ export interface IndexAttemptSnapshot {
|
||||
docs_removed_from_index: number;
|
||||
total_docs_indexed: number;
|
||||
error_msg: string | null;
|
||||
error_count: number;
|
||||
full_exception_trace: string | null;
|
||||
time_started: string | null;
|
||||
time_updated: string;
|
||||
|
Reference in New Issue
Block a user