Spaces:
Running
Running
Commit ·
0e82360
1
Parent(s): f4970d3
fix: robust OCR/image ingestion parsing and add one-click Supabase document deletion to Search Playground
Browse files- RAG_FULL_APPLICATION_BACKEND/app/routers/ingest.py +5 -5
- RAG_FULL_APPLICATION_BACKEND/app/services/file_parser.py +32 -8
- RAG_FULL_APPLICATION_BACKEND/app/services/supabase_client.py +23 -5
- RAG_FULL_APPLICATION_BACKEND/app/services/vision_service.py +10 -9
- RAG_FULL_APPLICATION_FRONTEND/src/pages/SearchPlaygroundPage.jsx +27 -2
RAG_FULL_APPLICATION_BACKEND/app/routers/ingest.py
CHANGED
|
@@ -69,13 +69,13 @@ async def list_documents(user: dict = Depends(get_current_user)):
|
|
| 69 |
raise HTTPException(status_code=500, detail="Database error")
|
| 70 |
|
| 71 |
@router.delete("/documents/{doc_id}")
|
| 72 |
-
async def delete_document(doc_id: str,
|
| 73 |
try:
|
| 74 |
-
# 1. Database cleanup
|
| 75 |
-
await supabase_service.delete_document(doc_id,
|
| 76 |
-
# 2. BM25 cleanup
|
| 77 |
bm25_service.delete_document(doc_id)
|
| 78 |
-
return {"status": "success", "message": f"Document {doc_id} deleted"}
|
| 79 |
except Exception as e:
|
| 80 |
logger.error(f"Failed to delete document {doc_id}: {e}")
|
| 81 |
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
| 69 |
raise HTTPException(status_code=500, detail="Database error")
|
| 70 |
|
| 71 |
@router.delete("/documents/{doc_id}")
|
| 72 |
+
async def delete_document(doc_id: str, user: dict = Depends(get_current_user)):
|
| 73 |
try:
|
| 74 |
+
# 1. Database cleanup from Supabase (documents + chunks + vectors)
|
| 75 |
+
await supabase_service.delete_document(doc_id, user["id"])
|
| 76 |
+
# 2. Local BM25 cleanup
|
| 77 |
bm25_service.delete_document(doc_id)
|
| 78 |
+
return {"status": "success", "message": f"Document {doc_id} deleted successfully from Supabase"}
|
| 79 |
except Exception as e:
|
| 80 |
logger.error(f"Failed to delete document {doc_id}: {e}")
|
| 81 |
raise HTTPException(status_code=500, detail=str(e))
|
RAG_FULL_APPLICATION_BACKEND/app/services/file_parser.py
CHANGED
|
@@ -47,17 +47,41 @@ async def _parse_pdf(file_path: str, job_id: str, ws_manager: Any, user_id: str)
|
|
| 47 |
return [{"text": text, "metadata": {"source": Path(file_path).name, "page": 1}}]
|
| 48 |
|
| 49 |
async def _parse_image(file_path: str, job_id: str, ws_manager: Any, user_id: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
try:
|
| 51 |
-
await ws_manager.emit(job_id, user_id, {"step": "IMAGE_ANALYZE", "color": "#8B5CF6", "detail": "Qwen-VL analyzing image (Primary)..."})
|
| 52 |
-
description = vision_service.understand_image(file_path)
|
| 53 |
-
return [{"text": description, "metadata": {"source": Path(file_path).name, "page": 1}}]
|
| 54 |
-
except Exception as e:
|
| 55 |
-
logger.warning(f"Vision service failed, falling back to Tesseract: {e}")
|
| 56 |
-
await ws_manager.emit(job_id, user_id, {"step": "FALLBACK", "color": "#F59E0B", "detail": "Vision failed. Falling back to Tesseract OCR..."})
|
| 57 |
import pytesseract
|
| 58 |
from PIL import Image
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
def _parse_docx(file_path: str):
|
| 63 |
doc = Document(file_path)
|
|
|
|
| 47 |
return [{"text": text, "metadata": {"source": Path(file_path).name, "page": 1}}]
|
| 48 |
|
| 49 |
async def _parse_image(file_path: str, job_id: str, ws_manager: Any, user_id: str):
|
| 50 |
+
filename = Path(file_path).name
|
| 51 |
+
extracted_content = []
|
| 52 |
+
|
| 53 |
+
# 1. Tesseract OCR (Fast, Reliable Local OCR)
|
| 54 |
+
ocr_text = ""
|
| 55 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
import pytesseract
|
| 57 |
from PIL import Image
|
| 58 |
+
img = Image.open(file_path)
|
| 59 |
+
ocr_text = pytesseract.image_to_string(img).strip()
|
| 60 |
+
if ocr_text:
|
| 61 |
+
extracted_content.append(f"Visual Text Content Extracted via OCR:\n{ocr_text}")
|
| 62 |
+
except Exception as e:
|
| 63 |
+
logger.warning(f"Tesseract OCR failed: {e}")
|
| 64 |
+
|
| 65 |
+
# 2. Vision Space Analysis (if available)
|
| 66 |
+
try:
|
| 67 |
+
await ws_manager.emit(job_id, user_id, {"step": "IMAGE_ANALYZE", "color": "#8B5CF6", "detail": "Analyzing visual image contents..."})
|
| 68 |
+
description = vision_service.understand_image(file_path)
|
| 69 |
+
if description and "failed to understand" not in description.lower():
|
| 70 |
+
extracted_content.append(f"Visual Scene Description:\n{description}")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.warning(f"Vision space analysis skipped/failed: {e}")
|
| 73 |
+
|
| 74 |
+
# 3. Fallback to image metadata if no text or vision
|
| 75 |
+
if not extracted_content:
|
| 76 |
+
try:
|
| 77 |
+
from PIL import Image
|
| 78 |
+
img = Image.open(file_path)
|
| 79 |
+
extracted_content.append(f"Image Document: {filename}\nResolution: {img.width}x{img.height}\nFormat: {img.format}\nMode: {img.mode}\nStatus: Visual image document indexed in knowledge base.")
|
| 80 |
+
except Exception:
|
| 81 |
+
extracted_content.append(f"Image Document: {filename}\nStatus: Visual image document indexed in knowledge base.")
|
| 82 |
+
|
| 83 |
+
final_text = "\n\n".join(extracted_content)
|
| 84 |
+
return [{"text": final_text, "metadata": {"source": filename, "page": 1}}]
|
| 85 |
|
| 86 |
def _parse_docx(file_path: str):
|
| 87 |
doc = Document(file_path)
|
RAG_FULL_APPLICATION_BACKEND/app/services/supabase_client.py
CHANGED
|
@@ -106,12 +106,30 @@ class SupabaseService:
|
|
| 106 |
return result.data
|
| 107 |
|
| 108 |
async def delete_document(self, document_id: str, user_id: str):
|
| 109 |
-
"""Delete document + chunks + vectors (CASCADE)"""
|
| 110 |
try:
|
| 111 |
-
# 1.
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
except Exception as e:
|
| 116 |
logger.error(f"Failed to delete document {document_id}: {e}")
|
| 117 |
raise
|
|
|
|
| 106 |
return result.data
|
| 107 |
|
| 108 |
async def delete_document(self, document_id: str, user_id: str):
|
| 109 |
+
"""Delete document + chunks + vectors + colbert tokens (CASCADE)"""
|
| 110 |
try:
|
| 111 |
+
# 1. Delete vector embeddings
|
| 112 |
+
try:
|
| 113 |
+
self.client.table("chunk_vectors").delete().eq("document_id", document_id).eq("user_id", user_id).execute()
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.warning(f"chunk_vectors delete notice: {e}")
|
| 116 |
+
|
| 117 |
+
# 2. Delete colbert tokens
|
| 118 |
+
try:
|
| 119 |
+
self.client.table("colbert_tokens").delete().eq("document_id", document_id).execute()
|
| 120 |
+
except Exception as e:
|
| 121 |
+
logger.warning(f"colbert_tokens delete notice: {e}")
|
| 122 |
+
|
| 123 |
+
# 3. Delete chunks
|
| 124 |
+
try:
|
| 125 |
+
self.client.table("chunks").delete().eq("document_id", document_id).eq("user_id", user_id).execute()
|
| 126 |
+
except Exception as e:
|
| 127 |
+
logger.warning(f"chunks delete notice: {e}")
|
| 128 |
+
|
| 129 |
+
# 4. Delete document entry
|
| 130 |
+
result = self.client.table("documents").delete().eq("id", document_id).eq("user_id", user_id).execute()
|
| 131 |
+
logger.info(f"Document {document_id} and all associated embeddings deleted from Supabase.")
|
| 132 |
+
return result
|
| 133 |
except Exception as e:
|
| 134 |
logger.error(f"Failed to delete document {document_id}: {e}")
|
| 135 |
raise
|
RAG_FULL_APPLICATION_BACKEND/app/services/vision_service.py
CHANGED
|
@@ -20,24 +20,25 @@ class VisionService:
|
|
| 20 |
"""
|
| 21 |
Send image to Qwen-VL HF Space for description.
|
| 22 |
"""
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
if not os.path.exists(image_path) or os.path.getsize(image_path) == 0:
|
| 27 |
-
return ""
|
| 28 |
|
|
|
|
| 29 |
self.client.predict(api_name="/clear_conversation_history")
|
| 30 |
file_arg = [handle_file(image_path)]
|
| 31 |
-
prompt = "Please describe the contents of this image in detail."
|
| 32 |
|
| 33 |
result = self.client.predict(
|
| 34 |
input_value={"files": file_arg, "text": prompt},
|
| 35 |
api_name="/add_message"
|
| 36 |
)
|
| 37 |
response_text = result[1]['value'][1]['content'][0]['content']
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
except Exception as e:
|
| 40 |
-
logger.
|
| 41 |
-
|
| 42 |
|
| 43 |
vision_service = VisionService()
|
|
|
|
| 20 |
"""
|
| 21 |
Send image to Qwen-VL HF Space for description.
|
| 22 |
"""
|
| 23 |
+
import os
|
| 24 |
+
if not os.path.exists(image_path) or os.path.getsize(image_path) == 0:
|
| 25 |
+
return ""
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
try:
|
| 28 |
self.client.predict(api_name="/clear_conversation_history")
|
| 29 |
file_arg = [handle_file(image_path)]
|
| 30 |
+
prompt = "Please describe the contents of this image in detail, including all visible text, objects, and layout."
|
| 31 |
|
| 32 |
result = self.client.predict(
|
| 33 |
input_value={"files": file_arg, "text": prompt},
|
| 34 |
api_name="/add_message"
|
| 35 |
)
|
| 36 |
response_text = result[1]['value'][1]['content'][0]['content']
|
| 37 |
+
if response_text and str(response_text).strip():
|
| 38 |
+
return str(response_text).strip()
|
| 39 |
+
raise ValueError("Empty response from vision space")
|
| 40 |
except Exception as e:
|
| 41 |
+
logger.warning(f"Image understanding via space failed: {e}")
|
| 42 |
+
raise
|
| 43 |
|
| 44 |
vision_service = VisionService()
|
RAG_FULL_APPLICATION_FRONTEND/src/pages/SearchPlaygroundPage.jsx
CHANGED
|
@@ -102,6 +102,22 @@ export default function SearchPlaygroundPage() {
|
|
| 102 |
}
|
| 103 |
};
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
return (
|
| 106 |
<div className="max-w-7xl mx-auto px-4 py-8 space-y-8">
|
| 107 |
{/* Header */}
|
|
@@ -159,8 +175,17 @@ export default function SearchPlaygroundPage() {
|
|
| 159 |
}`}
|
| 160 |
>
|
| 161 |
<div className="flex justify-between items-center text-xs font-semibold">
|
| 162 |
-
<span className="truncate max-w-[
|
| 163 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
</div>
|
| 165 |
</div>
|
| 166 |
))}
|
|
|
|
| 102 |
}
|
| 103 |
};
|
| 104 |
|
| 105 |
+
const handleDeleteDoc = async (e, docId) => {
|
| 106 |
+
e.stopPropagation();
|
| 107 |
+
if (!window.confirm('Delete this document and all its embeddings from Supabase database?')) return;
|
| 108 |
+
try {
|
| 109 |
+
await api.delete(`/ingest/documents/${docId}`);
|
| 110 |
+
const updatedDocs = documents.filter(d => d.id !== docId);
|
| 111 |
+
setDocuments(updatedDocs);
|
| 112 |
+
if (selectedDoc?.id === docId) {
|
| 113 |
+
setSelectedDoc(updatedDocs.length > 0 ? updatedDocs[0] : null);
|
| 114 |
+
}
|
| 115 |
+
} catch (error) {
|
| 116 |
+
console.error('Delete failed', error);
|
| 117 |
+
alert('Failed to delete document: ' + (error?.response?.data?.detail || error.message));
|
| 118 |
+
}
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
return (
|
| 122 |
<div className="max-w-7xl mx-auto px-4 py-8 space-y-8">
|
| 123 |
{/* Header */}
|
|
|
|
| 175 |
}`}
|
| 176 |
>
|
| 177 |
<div className="flex justify-between items-center text-xs font-semibold">
|
| 178 |
+
<span className="truncate max-w-[170px] text-white" title={doc.filename}>{doc.filename}</span>
|
| 179 |
+
<div className="flex items-center gap-1.5 shrink-0">
|
| 180 |
+
<span className="text-[9px] font-mono bg-surface-800 px-1.5 py-0.5 rounded text-accent-400 uppercase">{doc.file_type}</span>
|
| 181 |
+
<button
|
| 182 |
+
onClick={(e) => handleDeleteDoc(e, doc.id)}
|
| 183 |
+
className="p-1 text-gray-500 hover:text-red-400 hover:bg-red-500/10 rounded transition-colors"
|
| 184 |
+
title="Delete from Supabase"
|
| 185 |
+
>
|
| 186 |
+
<Trash2 className="w-3.5 h-3.5" />
|
| 187 |
+
</button>
|
| 188 |
+
</div>
|
| 189 |
</div>
|
| 190 |
</div>
|
| 191 |
))}
|