Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.353"
VERSION = "0.261.354"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
5 changes: 3 additions & 2 deletions application/single_app/functions_document_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from datetime import datetime, timedelta, timezone

from functions_document_status import is_document_processing_failure


DOCUMENT_PLACE_FILTERS = frozenset({
"all", "recent", "shared", "processing", "errors", "untagged",
Expand All @@ -11,8 +13,7 @@


def _document_processing_state(document_item):
status_text = str(document_item.get("status") or "").lower()
if "error" in status_text or "failed" in status_text:
if is_document_processing_failure(document_item.get("status")):
return "error"

percentage = document_item.get("percentage_complete")
Expand Down
32 changes: 32 additions & 0 deletions application/single_app/functions_document_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# functions_document_status.py
"""Document terminal-state predicates, independent of application bootstrap.

Status details can contain filenames, sheet names, or retry diagnostics. Only
lifecycle phrases at the start of a status can declare a terminal outcome.
Keep the browser predicates aligned through document_processing_status_cases.json.
"""

import re


_FAILURE = re.compile(

Check warning on line 12 in application/single_app/functions_document_status.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
r"^(?:(?:error|failed)(?:$|[\s:!]|\.(?:$|\s))"
r"|(?:processing|document processing|approval|content screening) failed(?:$|[\s:;!]|\.(?:$|\s))"
r"|video:\s*(?:blob upload|auth|upload|indexing) failed(?:$|[\s:!]|\.(?:$|\s)))",

Check warning on line 15 in application/single_app/functions_document_status.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
re.IGNORECASE,
)
_COMPLETE = re.compile(

Check warning on line 18 in application/single_app/functions_document_status.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.
r"^(?:(?:ready|complete|completed|processed|finished)[.!]?$"
r"|processing complete(?:$|[.!]$|\s+[-(]))",
re.IGNORECASE,
)


def is_document_processing_failure(status):
"""Recognize terminal failures, not error words in processing details."""
return isinstance(status, str) and _FAILURE.match(status.strip()) is not None


def is_document_processing_complete(status):
"""Recognize final completion, not completion of an intermediate stage."""
return isinstance(status, str) and _COMPLETE.match(status.strip()) is not None
15 changes: 8 additions & 7 deletions application/single_app/functions_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
)
from config import *
from functions_appinsights import log_event
from functions_document_status import is_document_processing_complete, is_document_processing_failure
from functions_document_provenance import (
ORIGIN_FIELD_NAMES,
ORIGIN_KIND_FIELD,
Expand Down Expand Up @@ -3508,10 +3509,10 @@ def calculate_processing_percentage(doc_metadata):
total_chunks_saved = doc_metadata.get('current_file_chunk', 0)

# --- Final States ---
if "processing complete" in status or current_pct == 100:
if is_document_processing_complete(status) or current_pct == 100:
# Ensure it stays 100 if it ever reached it
return 100
if "error" in status or "failed" in status:
if is_document_processing_failure(status):
# Keep the last known percentage on error/failure
return current_pct

Expand Down Expand Up @@ -3694,7 +3695,7 @@ def update_document(**kwargs):
marker = existing_document.get(SCREENING_FIELD)
if marker is not None and not document_is_available(existing_document):
requested_status = str(kwargs.get("status") or "").lower()
if "complete" in requested_status:
if is_document_processing_complete(requested_status):
kwargs["status"] = "Content review required" if marker.get("review_required") else "Content screening pending"
if kwargs.get("percentage_complete") == 100:
kwargs["percentage_complete"] = 99
Expand Down Expand Up @@ -3763,14 +3764,14 @@ def update_document(**kwargs):
elif isinstance(status_lower, dict):
status_lower = json.dumps(status_lower).lower()

if "processing complete" in status_lower:
if is_document_processing_complete(status_lower):
new_percentage = 100
elif "error" in status_lower or "failed" in status_lower:
elif is_document_processing_failure(status_lower):
pass # Percentage already calculated by helper based on 'failed' status

# Ensure percentage doesn't decrease (unless reset on failure or hitting 100)
# Compare against original_percentage fetched *before* any updates in this call
if new_percentage < original_percentage and new_percentage != 0 and "failed" not in status_lower and "error" not in status_lower:
if new_percentage < original_percentage and new_percentage != 0 and not is_document_processing_failure(status_lower):
existing_document['percentage_complete'] = original_percentage
else:
existing_document['percentage_complete'] = new_percentage
Expand Down Expand Up @@ -6881,7 +6882,7 @@ def detect_doc_type(document_id, user_id=None):

def _update_document_for_job(*, operation_guard=None, safe_errors=False, **arguments):
status = arguments.get("status")
if safe_errors and isinstance(status, str) and any(word in status.lower() for word in ("error", "failed")):
if safe_errors and is_document_processing_failure(status):
arguments["status"] = "Document processing failed. Contact an administrator if the problem persists."
if operation_guard is None:
return update_document(**arguments)
Expand Down
5 changes: 3 additions & 2 deletions application/single_app/route_backend_chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
guard_model_callable,
)
from content_screening.contracts import DocumentHeldError, ScreeningConflictError, ScreeningError, document_is_available
from functions_document_status import is_document_processing_complete, is_document_processing_failure
from agent_execution_context import AgentExecutionCancelled, DelegationBudget
from action_credential_contracts import ActionCredentialsRequired
from agent_delegation_runtime import AgentExecution, delegation_citations, delegation_usage, prepare_agent_execution
Expand Down Expand Up @@ -1533,7 +1534,7 @@ def _is_search_ready_chat_upload_workspace_document(document_item):
return False

status = str(document_item.get('status') or '').strip().lower()
if 'error' in status or 'failed' in status:
if is_document_processing_failure(status):
return False

try:
Expand All @@ -1548,7 +1549,7 @@ def _is_search_ready_chat_upload_workspace_document(document_item):
except (TypeError, ValueError):
percentage_complete = 0

return percentage_complete >= 100 or 'processing complete' in status
return percentage_complete >= 100 or is_document_processing_complete(status)


def _get_chat_upload_workspace_document_scope(document_item):
Expand Down
4 changes: 2 additions & 2 deletions application/single_app/static/js/chat/chat-documents.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ function isConversationTaskDocumentReady(taskDocument = {}) {
if (window.ContentScreening?.isHeld(taskDocument)) return false;
const statusText = String(taskDocument.status || '').trim().toLowerCase();
const percentageComplete = Number(taskDocument.percentage_complete || taskDocument.percentageComplete || 0);
if (statusText.includes('error') || statusText.includes('failed')) {
if (window.DocumentStatus.isFailure(statusText)) {
return false;
}
return percentageComplete >= 100 || statusText.includes('processing complete') || statusText.includes('complete');
return percentageComplete >= 100 || window.DocumentStatus.isComplete(statusText);
}

function normalizeConversationTaskDocument(documentInfo = {}, fallbackConversationId = null) {
Expand Down
19 changes: 8 additions & 11 deletions application/single_app/static/js/chat/chat-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,24 +132,21 @@ function getChatWorkspaceProgressValue(value) {
function isChatWorkspaceDocumentComplete(doc) {
const statusText = String(doc?.status || '').toLowerCase();
return getChatWorkspaceProgressValue(doc?.percentage_complete) >= 100
|| statusText.includes('processing complete')
|| statusText.includes('complete')
|| statusText.includes('error')
|| statusText.includes('failed');
|| window.DocumentStatus.isComplete(statusText)
|| window.DocumentStatus.isFailure(statusText);
}

function isChatWorkspaceDocumentFailure(doc) {
const statusText = String(doc?.status || '').toLowerCase();
return statusText.includes('error') || statusText.includes('failed');
return window.DocumentStatus.isFailure(statusText);
}

function isChatWorkspaceDocumentSuccessComplete(doc) {
const statusText = String(doc?.status || '').toLowerCase();
return !isChatWorkspaceDocumentFailure(doc)
&& (
getChatWorkspaceProgressValue(doc?.percentage_complete) >= 100
|| statusText.includes('processing complete')
|| statusText.includes('complete')
|| window.DocumentStatus.isComplete(statusText)
);
}

Expand Down Expand Up @@ -231,9 +228,9 @@ function buildChatWorkspaceAttachmentHtml(attachment) {
const statusText = String(attachment?.status || 'Queued for workspace processing').trim() || 'Queued for workspace processing';
const detailsId = getChatWorkspaceProgressDetailsId(workspaceDocumentId);
const statusLower = statusText.toLowerCase();
const progressClass = statusLower.includes('error') || statusLower.includes('failed')
const progressClass = window.DocumentStatus.isFailure(statusLower)
? 'bg-danger'
: (pct >= 100 || statusLower.includes('complete') ? 'bg-success' : 'progress-bar-striped progress-bar-animated bg-info');
: (pct >= 100 || window.DocumentStatus.isComplete(statusLower) ? 'bg-success' : 'progress-bar-striped progress-bar-animated bg-info');

return `
<div class="chat-workspace-upload-progress mt-2 p-2 border rounded bg-body-tertiary"
Expand Down Expand Up @@ -302,8 +299,8 @@ function updateChatWorkspaceProgressContainer(container, doc) {
const pct = getChatWorkspaceProgressValue(doc?.percentage_complete);
const statusText = String(doc?.status || 'Processing workspace document...').trim() || 'Processing workspace document...';
const statusLower = statusText.toLowerCase();
const hasFailure = statusLower.includes('error') || statusLower.includes('failed');
const hasCompleted = !hasFailure && (pct >= 100 || statusLower.includes('complete'));
const hasFailure = window.DocumentStatus.isFailure(statusLower);
const hasCompleted = !hasFailure && (pct >= 100 || window.DocumentStatus.isComplete(statusLower));
const progressBar = container.querySelector('.progress-bar');
const statusElement = container.querySelector('.chat-workspace-upload-status-text');
const progressElement = container.querySelector('.progress');
Expand Down
16 changes: 16 additions & 0 deletions application/single_app/static/js/document-status.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// document-status.js
// Kept in parity with Python and React through document_processing_status_cases.json.

window.DocumentStatus = (() => {
const failure = /^(?:(?:error|failed)(?:$|[\s:!]|\.(?:$|\s))|(?:processing|document processing|approval|content screening) failed(?:$|[\s:;!]|\.(?:$|\s))|video:\s*(?:blob upload|auth|upload|indexing) failed(?:$|[\s:!]|\.(?:$|\s)))/i;

Check warning on line 5 in application/single_app/static/js/document-status.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
const complete = /^(?:(?:ready|complete|completed|processed|finished)[.!]?$|processing complete(?:$|[.!]$|\s+[-(]))/i;

return Object.freeze({
isFailure(status) {
return typeof status === "string" && failure.test(status.trim());
},
isComplete(status) {
return typeof status === "string" && complete.test(status.trim());
},
});
})();
17 changes: 8 additions & 9 deletions application/single_app/static/js/public/public_workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -1071,9 +1071,8 @@ function getPublicDocumentProcessingState(doc) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || '0');
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || '';
const normalizedStatus = docStatus.toLowerCase();
const hasError = normalizedStatus.includes('error') || normalizedStatus.includes('failed');
const isComplete = pct >= 100 || normalizedStatus.includes('complete') || hasError;
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
return { pct, docStatus, hasError, isComplete };
}

Expand Down Expand Up @@ -1667,8 +1666,8 @@ function renderPublicDocumentRow(doc) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || "0");
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
const hasError = docStatus.toLowerCase().includes("error") || docStatus.toLowerCase().includes("failed");
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const isPendingGeneratedArtifact = isPendingGeneratedArtifactDocument(doc);

let firstTdHtml = "";
Expand Down Expand Up @@ -1896,8 +1895,8 @@ function pollPublicDocumentStatus(documentId) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || "0");
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
const hasError = docStatus.toLowerCase().includes("error") || docStatus.toLowerCase().includes("failed");
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;

if (!isComplete && statusRow) {
// Update progress bar and status text if still processing
Expand Down Expand Up @@ -3374,8 +3373,8 @@ function buildPublicFolderDocumentsTable(docs) {
const pctString = String((doc.percentage_complete ?? doc.percentage) || '0');
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || '';
const isComplete = pct >= 100 || docStatus.toLowerCase().includes('complete') || docStatus.toLowerCase().includes('error');
const hasError = docStatus.toLowerCase().includes('error') || docStatus.toLowerCase().includes('failed');
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;
const isSelected = publicSelectedDocuments.has(doc.id);
let firstColHtml = `<input type="checkbox" class="form-check-input document-checkbox${publicSelectionMode ? '' : ' d-none'}" data-document-id="${doc.id}"${isSelected ? ' checked' : ''}>`;
let actionsHtml = '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,8 @@ function getDocumentProcessingState(doc) {
const pctString = String(doc?.percentage_complete ?? "");
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc?.status || "";
const normalizedStatus = docStatus.toLowerCase();
const hasError = normalizedStatus.includes("error");
const isComplete = pct >= 100 || normalizedStatus.includes("complete") || hasError;
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;

return { pct, docStatus, hasError, isComplete };
}
Expand Down Expand Up @@ -1789,8 +1788,8 @@ function renderDocumentRow(doc) {
const pctString = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
const hasError = docStatus.toLowerCase().includes("error");
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;

const docRow = document.createElement("tr");
docRow.id = `doc-row-${docId}`;
Expand Down Expand Up @@ -2239,8 +2238,8 @@ function pollDocumentStatus(documentId) {
const pctString = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctString) ? parseFloat(pctString) : 0;
const docStatus = doc.status || "";
const isComplete = pct >= 100 || docStatus.toLowerCase().includes("complete") || docStatus.toLowerCase().includes("error");
const hasError = docStatus.toLowerCase().includes("error");
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;

if (!isComplete && statusRow) {
// Update progress bar and status text if still processing
Expand Down
6 changes: 2 additions & 4 deletions application/single_app/static/js/workspace/workspace-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,10 +648,8 @@ function buildFolderDocumentsTable(docs) {
const pctStr = String(doc.percentage_complete);
const pct = /^\d+(\.\d+)?$/.test(pctStr) ? parseFloat(pctStr) : 0;
const docStatus = doc.status || '';
const isComplete = pct >= 100
|| docStatus.toLowerCase().includes('complete')
|| docStatus.toLowerCase().includes('error');
const hasError = docStatus.toLowerCase().includes('error');
const hasError = window.DocumentStatus.isFailure(docStatus);
const isComplete = pct >= 100 || window.DocumentStatus.isComplete(docStatus) || hasError;

const currentUserId = window.current_user_id;
const isOwner = doc.user_id === currentUserId;
Expand Down
1 change: 1 addition & 0 deletions application/single_app/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ <h5 class="modal-title workflow-alert-title" id="workflowAlertModalLabel">Workfl
but it's okay to load it afterwards) -->
<script src="/static/js/chat/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/toast.js') }}?v={{ config['VERSION'] }}"></script>
<script src="{{ url_for('static', filename='js/document-status.js') }}?v={{ config['VERSION'] }}"></script>


<!-- Then DataTables (depends on jQuery) -->
Expand Down
Loading
Loading