Skip to content

Commit bf2072b

Browse files
committed
feat: add support for skipped tasks and warnings in task processing
1 parent f6eaf76 commit bf2072b

7 files changed

Lines changed: 118 additions & 24 deletions

File tree

frontend/app/api/queries/useGetTasksQuery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface TaskFileEntry {
2929
| "running"
3030
| "processing"
3131
| "completed"
32+
| "skipped"
3233
| "failed"
3334
| "error";
3435
result?: unknown;
@@ -57,6 +58,7 @@ export interface Task {
5758
| "running"
5859
| "processing"
5960
| "completed"
61+
| "skipped"
6062
| "failed"
6163
| "error";
6264
total_files?: number;

frontend/components/task-dialog/constants.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { AlertCircle, CheckCircle, Clock, type LucideIcon } from "lucide-react";
1+
import {
2+
AlertCircle,
3+
CheckCircle,
4+
Clock,
5+
type LucideIcon,
6+
TriangleAlert,
7+
} from "lucide-react";
28
import type { TaskFileStatusCategory } from "@/lib/task-utils";
39

410
export const CATEGORY_CHIPS: Array<{
@@ -19,6 +25,12 @@ export const CATEGORY_CHIPS: Array<{
1925
icon: AlertCircle,
2026
iconClassName: "text-destructive",
2127
},
28+
{
29+
id: "warning",
30+
label: "Warning",
31+
icon: TriangleAlert,
32+
iconClassName: "text-brand-amber",
33+
},
2234
{
2335
id: "indexing",
2436
label: "Indexing",

frontend/components/task-error-content.tsx

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import {
1717
} from "@/lib/task-error-display";
1818
import {
1919
getFailedFileCount,
20-
getFailedFileEntries,
2120
getSuccessfulFileCount,
2221
getTaskFileName,
22+
getTaskIssueFileEntries,
23+
getWarningFileEntries,
2324
isCompletedTotalFailure,
25+
isTaskFileWarning,
2426
isTerminalFailedTask,
2527
} from "@/lib/task-utils";
2628
import { formatTaskTimestamp, parseTimestamp } from "@/lib/time-utils";
@@ -48,15 +50,20 @@ export function TaskErrorContent({
4850
);
4951
const isExpanded = accordionValue === "failed-files";
5052

51-
const failedEntries = useMemo(() => getFailedFileEntries(task), [task]);
53+
const issueEntries = useMemo(() => getTaskIssueFileEntries(task), [task]);
5254

5355
const failedCount = getFailedFileCount(task);
56+
const warningCount = getWarningFileEntries(task).length;
5457
const successCount = getSuccessfulFileCount(task);
5558
const timestamp =
5659
parseTimestamp(task.created_at) ?? parseTimestamp(task.updated_at);
5760
const isFailedStatus =
5861
isTerminalFailedTask(task) || isCompletedTotalFailure(task);
59-
const statusLabel = isFailedStatus ? "Failed" : "Complete";
62+
const statusLabel = isFailedStatus
63+
? "Failed"
64+
: warningCount > 0
65+
? "Warning"
66+
: "Complete";
6067
// Pill colors: failed (red) vs partial success (amber/orange), each with IBM tokens or OSS borders.
6168
const statusPillClassName = cn(
6269
"shrink-0 rounded-full px-2 py-1 text-xs",
@@ -69,7 +76,7 @@ export function TaskErrorContent({
6976
: "border border-brand-amber-30 bg-brand-amber-10 text-brand-amber",
7077
);
7178

72-
if (failedCount <= 0 && failedEntries.length === 0) {
79+
if (failedCount <= 0 && issueEntries.length === 0) {
7380
return null;
7481
}
7582

@@ -78,7 +85,9 @@ export function TaskErrorContent({
7885
const accordionSummary = (
7986
<div className="flex min-w-0 flex-1 items-center gap-1">
8087
<span className="text-xs">
81-
{successCount} success · {failedCount} failed
88+
{successCount} success
89+
{warningCount > 0 ? ` · ${warningCount} warning` : ""}
90+
{failedCount > 0 ? ` · ${failedCount} failed` : ""}
8291
</span>
8392
<ChevronDown className="size-4 shrink-0 transition-transform group-data-[state=open]:rotate-180" />
8493
</div>
@@ -174,19 +183,30 @@ export function TaskErrorContent({
174183
{accordionHeader}
175184
<AccordionContent className="w-full p-0 pt-2">
176185
<div className="flex w-full flex-col gap-2">
177-
{failedEntries.map(([filePath, fileInfo], index) => {
186+
{issueEntries.map(([filePath, fileInfo], index) => {
178187
const fileName = getTaskFileName(filePath, fileInfo);
179188
const line = resolveTaskFileError(fileInfo, task.error);
180189
const componentCause = formatApiComponent(fileInfo.component);
190+
const isWarning = isTaskFileWarning(fileInfo);
181191

182192
return (
183193
<div
184194
key={`${task.task_id}-${filePath}-${index}`}
185195
className={cn(
186196
"task-failed-file-card min-w-0",
187197
isCloudBrand
188-
? "flex flex-col items-start gap-2 self-stretch rounded-none rounded-r border-l-[1.5px] border-l-destructive bg-border p-2"
189-
: "flex flex-col gap-1 rounded border-destructive/20 bg-failure-soft py-mmd px-4",
198+
? cn(
199+
"flex flex-col items-start gap-2 self-stretch rounded-none rounded-r border-l-[1.5px] bg-border p-2",
200+
isWarning
201+
? "border-l-brand-amber"
202+
: "border-l-destructive",
203+
)
204+
: cn(
205+
"flex flex-col gap-1 rounded py-mmd px-4",
206+
isWarning
207+
? "border border-brand-amber-30 bg-brand-amber-10"
208+
: "border-destructive/20 bg-failure-soft",
209+
),
190210
)}
191211
>
192212
<p

frontend/lib/task-error-display.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ export function resolveTaskFileError(
137137
fileInfo: TaskFileEntry,
138138
taskError?: string,
139139
): string {
140+
if (
141+
fileInfo.result &&
142+
typeof fileInfo.result === "object" &&
143+
"warning" in fileInfo.result &&
144+
typeof fileInfo.result.warning === "string" &&
145+
fileInfo.result.warning.trim()
146+
) {
147+
return fileInfo.result.warning.trim();
148+
}
140149
if (typeof fileInfo.user_facing_message === "string") {
141150
const message = fileInfo.user_facing_message.trim();
142151
if (message) {

frontend/lib/task-utils.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import {
77
export const ALL_TASK_FILE_TYPES = "__all__";
88
export const ALL_TASK_STATUS_CATEGORIES = "__all__";
99

10-
export type TaskFileStatusCategory = "completed" | "system_error" | "indexing";
10+
export type TaskFileStatusCategory =
11+
| "completed"
12+
| "warning"
13+
| "system_error"
14+
| "indexing";
1115

1216
export type TaskFileNameSort = "asc" | "desc";
1317

@@ -26,6 +30,10 @@ export function isTaskFileFailed(fileInfo: TaskFileEntry): boolean {
2630
return fileInfo.status === "failed" || fileInfo.status === "error";
2731
}
2832

33+
export function isTaskFileWarning(fileInfo: TaskFileEntry): boolean {
34+
return fileInfo.status === "skipped";
35+
}
36+
2937
export function getTaskFileDialogStatusLabel(
3038
fileInfo: TaskFileEntry,
3139
taskError?: string,
@@ -40,6 +48,9 @@ export function getTaskFileDialogStatusLabel(
4048
}
4149
return buildRowStatusLabel("unknown");
4250
}
51+
if (isTaskFileWarning(fileInfo)) {
52+
return "Warning";
53+
}
4354
if (isTaskFileCompleted(fileInfo)) {
4455
return "Complete";
4556
}
@@ -126,6 +137,9 @@ export function getTaskFileStatusCategory(
126137
if (isTaskFileFailed(fileInfo)) {
127138
return "system_error";
128139
}
140+
if (isTaskFileWarning(fileInfo)) {
141+
return "warning";
142+
}
129143

130144
const status = fileInfo.status ?? "pending";
131145
if (status === "pending" || status === "running" || status === "processing") {
@@ -144,6 +158,7 @@ export function countTaskFileEntriesByCategory(
144158
): Record<TaskFileStatusCategory, number> {
145159
const counts: Record<TaskFileStatusCategory, number> = {
146160
completed: 0,
161+
warning: 0,
147162
system_error: 0,
148163
indexing: 0,
149164
};
@@ -218,11 +233,27 @@ export function getFailedFileEntries(
218233
);
219234
}
220235

236+
export function getWarningFileEntries(
237+
task: Task,
238+
): Array<[string, TaskFileEntry]> {
239+
return Object.entries(task.files || {}).filter(([, fileInfo]) =>
240+
isTaskFileWarning(fileInfo),
241+
);
242+
}
243+
244+
export function getTaskIssueFileEntries(
245+
task: Task,
246+
): Array<[string, TaskFileEntry]> {
247+
return Object.entries(task.files || {}).filter(
248+
([, fileInfo]) => isTaskFileFailed(fileInfo) || isTaskFileWarning(fileInfo),
249+
);
250+
}
251+
221252
export function hasFailedFileEntries(task: Task): boolean {
222253
if ((task.failed_files ?? 0) > 0) {
223254
return true;
224255
}
225-
return getFailedFileEntries(task).length > 0;
256+
return getTaskIssueFileEntries(task).length > 0;
226257
}
227258

228259
export function isTerminalFailedTask(task: Task): boolean {

src/models/processors.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -741,12 +741,21 @@ async def process_item(self, upload_task: UploadTask, item: str, file_task: File
741741
)
742742
if await self.check_filename_exists(document.filename, opensearch_client):
743743
if not self.replace_duplicates:
744-
file_task.status = TaskStatus.FAILED
745-
file_task.error = f"File with name '{document.filename}' already exists"
744+
file_task.status = TaskStatus.SKIPPED
745+
file_task.error = None
746+
file_task.result = {
747+
"status": "skipped",
748+
"reason": "duplicate_filename",
749+
"warning": "A file with this name already exists.",
750+
}
746751
file_task.updated_at = time.time()
747-
upload_task.failed_files += 1
752+
upload_task.successful_files += 1
748753
return
749-
await self.delete_document_by_filename(document.filename, opensearch_client)
754+
await self.delete_document_by_filename(
755+
document.filename,
756+
opensearch_client,
757+
owner_user_id=self.user_id,
758+
)
750759

751760
# Create temporary file from document content
752761
suffix = os.path.splitext(document.filename)[1]

tests/unit/test_connector_processor_filename_dedupe.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ async def mock_search(index, body, **kwargs):
9696

9797

9898
@pytest.mark.asyncio
99-
async def test_connector_processor_fails_when_filename_exists_and_replace_false(monkeypatch):
99+
async def test_connector_processor_skips_when_filename_exists_and_replace_false(monkeypatch):
100100
monkeypatch.setattr("config.settings.DISABLE_INGEST_WITH_LANGFLOW", True)
101101
processor = _build_connector_processor(replace_duplicates=False)
102102
document = _make_document()
@@ -108,10 +108,15 @@ async def test_connector_processor_fails_when_filename_exists_and_replace_false(
108108
with patch.object(processor, "process_document_standard", new=AsyncMock()) as mock_process:
109109
await processor.process_item(upload_task, "file-id-1", file_task)
110110

111-
assert file_task.status == TaskStatus.FAILED
112-
assert "already exists" in (file_task.error or "")
113-
assert upload_task.failed_files == 1
114-
assert upload_task.successful_files == 0
111+
assert file_task.status == TaskStatus.SKIPPED
112+
assert file_task.error is None
113+
assert file_task.result == {
114+
"status": "skipped",
115+
"reason": "duplicate_filename",
116+
"warning": "A file with this name already exists.",
117+
}
118+
assert upload_task.failed_files == 0
119+
assert upload_task.successful_files == 1
115120
mock_process.assert_not_called()
116121
opensearch_client.delete_by_query.assert_not_called()
117122

@@ -271,7 +276,7 @@ async def mock_search(index, body, **kwargs):
271276

272277

273278
@pytest.mark.asyncio
274-
async def test_langflow_connector_processor_fails_on_filename_collision(monkeypatch):
279+
async def test_langflow_connector_processor_skips_on_filename_collision(monkeypatch):
275280
monkeypatch.setattr("config.settings.DISABLE_INGEST_WITH_LANGFLOW", False)
276281
processor = _build_langflow_processor(replace_duplicates=False)
277282
document = _make_document()
@@ -282,9 +287,15 @@ async def test_langflow_connector_processor_fails_on_filename_collision(monkeypa
282287

283288
await processor.process_item(upload_task, "file-id-1", file_task)
284289

285-
assert file_task.status == TaskStatus.FAILED
286-
assert "already exists" in (file_task.error or "")
287-
assert upload_task.failed_files == 1
290+
assert file_task.status == TaskStatus.SKIPPED
291+
assert file_task.error is None
292+
assert file_task.result == {
293+
"status": "skipped",
294+
"reason": "duplicate_filename",
295+
"warning": "A file with this name already exists.",
296+
}
297+
assert upload_task.failed_files == 0
298+
assert upload_task.successful_files == 1
288299
processor.connector_service.langflow_service.upload_and_ingest_file.assert_not_called()
289300
opensearch_client.delete_by_query.assert_not_called()
290301

0 commit comments

Comments
 (0)