diff --git a/server/app/routes/convert.py b/server/app/routes/convert.py index 8472dc29..1451b0d7 100644 --- a/server/app/routes/convert.py +++ b/server/app/routes/convert.py @@ -4,12 +4,14 @@ import os import aiohttp from pathlib import Path +from urllib.parse import urljoin from azure.ai.documentintelligence.models import AnalyzeDocumentRequest, AnalyzeResult, DocumentContentFormat from azure.ai.documentintelligence import DocumentIntelligenceClient from azure.core.credentials import AzureKeyCredential import asyncio from concurrent.futures import ThreadPoolExecutor from dotenv import load_dotenv +import base64 from docling.datamodel.base_models import InputFormat from docling.document_converter import DocumentConverter, PdfFormatOption @@ -48,8 +50,63 @@ def process_document_with_azure(file_path: str, endpoint: str, key: str) -> str: return f"Error processing document: {str(e)}" @router.post("/api/convert-documents") -async def convert_documents(files: List[UploadFile] = File(...), use_docetl_server: str = "false"): +async def convert_documents( + files: List[UploadFile] = File(...), + use_docetl_server: str = "false", + custom_docling_url: Optional[str] = Header(None) +): use_docetl_server = use_docetl_server.lower() == "true" # TODO: make this a boolean + + + # If custom Docling URL is provided, forward the request there + if custom_docling_url: + try: + async with aiohttp.ClientSession() as session: + results = [] + for file in files: + # Read file content and encode as base64 + content = await file.read() + base64_content = base64.b64encode(content).decode('utf-8') + + # Prepare request payload according to Docling server spec + payload = { + "file_source": { + "base64_string": base64_content, + "filename": file.filename + }, + "options": { + "output_docling_document": False, + "output_markdown": True, + "output_html": False, + "do_ocr": True, + "do_table_structure": True, + "include_images": False + } + } + + async with session.post( + urljoin(custom_docling_url, 'convert'), + json=payload, + timeout=120 + ) as response: + if response.status == 200: + result = await response.json() + if result["status"] in ("success", '4'): + results.append({ + "filename": file.filename, + "markdown": result["document"]["markdown"] + }) + else: + return {"error": f"Docling server failed to convert {file.filename}: {result.get('errors', [])}"} + else: + error_msg = await response.text() + return {"error": f"Custom Docling server returned error for {file.filename}: {error_msg}"} + + return {"documents": results} + + except Exception as e: + print(f"Custom Docling server failed: {str(e)}. Falling back to local processing...") + # Only try Modal endpoint if use_docetl_server is true and there are no txt files all_txt_files = all(file.filename.lower().endswith('.txt') or file.filename.lower().endswith('.md') for file in files) if use_docetl_server and not all_txt_files: @@ -58,6 +115,8 @@ async def convert_documents(files: List[UploadFile] = File(...), use_docetl_serv # Prepare files for multipart upload data = aiohttp.FormData() for file in files: + # Reset file position since we might have read it in the custom Docling attempt + await file.seek(0) data.add_field('files', await file.read(), filename=file.filename, diff --git a/website/src/app/api/convertDocuments/route.ts b/website/src/app/api/convertDocuments/route.ts index bce02769..e589a022 100644 --- a/website/src/app/api/convertDocuments/route.ts +++ b/website/src/app/api/convertDocuments/route.ts @@ -19,12 +19,7 @@ export async function POST(request: NextRequest) { // Get Azure credentials from headers if they exist const azureEndpoint = request.headers.get("azure-endpoint"); const azureKey = request.headers.get("azure-key"); - - // Determine which endpoint to use - const endpoint = - azureEndpoint && azureKey - ? "/api/azure-convert-documents" - : "/api/convert-documents"; + const customDoclingUrl = request.headers.get("custom-docling-url"); // Prepare headers for the backend request const headers: HeadersInit = {}; @@ -32,6 +27,9 @@ export async function POST(request: NextRequest) { headers["azure-endpoint"] = azureEndpoint; headers["azure-key"] = azureKey; } + if (customDoclingUrl) { + headers["custom-docling-url"] = customDoclingUrl; + } // Create FormData since FastAPI expects multipart/form-data const backendFormData = new FormData(); @@ -39,17 +37,25 @@ export async function POST(request: NextRequest) { backendFormData.append("files", file); } - // Forward the request to the Python backend - const response = await fetch( - `${FASTAPI_URL}${endpoint}?use_docetl_server=${ - conversionMethod === "docetl" ? "true" : "false" - }`, - { - method: "POST", - body: backendFormData, - headers, - } - ); + // Determine which endpoint to use and construct the URL + let targetUrl: string; + if (azureEndpoint && azureKey) { + targetUrl = `${FASTAPI_URL}/api/azure-convert-documents`; + } else if (customDoclingUrl) { + targetUrl = `${FASTAPI_URL}/api/convert-documents`; + } else { + targetUrl = `${FASTAPI_URL}/api/convert-documents${ + conversionMethod === "docetl" ? "?use_docetl_server=true" : "" + }`; + } + + + // Forward the request to the appropriate backend + const response = await fetch(targetUrl, { + method: "POST", + body: backendFormData, + headers, + }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); @@ -66,10 +72,7 @@ export async function POST(request: NextRequest) { console.error("Error converting documents:", error); return NextResponse.json( { - error: - error instanceof Error - ? error.message - : "Failed to convert documents", + error: error instanceof Error ? error.message : String(error), }, { status: 500 } ); diff --git a/website/src/components/FileExplorer.tsx b/website/src/components/FileExplorer.tsx index 6026a910..d56aa8d9 100644 --- a/website/src/components/FileExplorer.tsx +++ b/website/src/components/FileExplorer.tsx @@ -147,7 +147,7 @@ async function getAllFiles(entry: FileSystemEntry): Promise { return files; } -type ConversionMethod = "local" | "azure" | "docetl"; +type ConversionMethod = "local" | "azure" | "docetl" | "custom-docling"; async function validateJsonDataset(file: Blob): Promise { const text = await file.text(); @@ -216,6 +216,7 @@ export const FileExplorer: React.FC = ({ useState("local"); const [azureEndpoint, setAzureEndpoint] = useState(""); const [azureKey, setAzureKey] = useState(""); + const [customDoclingUrl, setCustomDoclingUrl] = useState(""); const { uploadingFiles, uploadDataset } = useDatasetUpload({ namespace, @@ -358,6 +359,8 @@ export const FileExplorer: React.FC = ({ if (conversionMethod === "azure") { headers["azure-endpoint"] = azureEndpoint; headers["azure-key"] = azureKey; + } else if (conversionMethod === "custom-docling") { + headers["custom-docling-url"] = customDoclingUrl; } // Then proceed with conversion @@ -368,7 +371,8 @@ export const FileExplorer: React.FC = ({ }); if (!response.ok) { - throw new Error("Failed to convert documents"); + const errorData = await response.json(); + throw new Error(errorData.error || "Internal Server Error"); } const result = await response.json(); @@ -430,7 +434,7 @@ export const FileExplorer: React.FC = ({ toast({ variant: "destructive", title: "Error", - description: "Failed to process files. Please try again.", + description: error instanceof Error ? error.message : String(error), }); } finally { setIsConverting(false); @@ -687,7 +691,7 @@ export const FileExplorer: React.FC = ({ onValueChange={(value) => setConversionMethod(value as ConversionMethod) } - className="mt-2 grid grid-cols-3 gap-2" + className="mt-2 grid grid-cols-4 gap-2" >
@@ -778,6 +782,33 @@ export const FileExplorer: React.FC = ({ Enterprise-grade cloud processing

+ +
+
+ + +
+

+ Connect to your own Docling server instance +

+
@@ -826,13 +857,30 @@ export const FileExplorer: React.FC = ({ )} + {conversionMethod === "custom-docling" && ( +
+
+ + setCustomDoclingUrl(e.target.value)} + className="h-8" + /> +
+
+ )} +
0 - ? "border-border bg-accent/50 p-6" - : "border-border p-8 hover:border-primary" + ? "border-border bg-accent/50 p-3" + : "border-border p-3 hover:border-primary" } `} onDragOver={(e) => { @@ -883,33 +931,30 @@ export const FileExplorer: React.FC = ({ )}
- -
-

- Drag and drop your documents here or -

- -

+ +

+
+ Drag and drop your documents here or + +
+

Supported formats: PDF, DOCX, DOC, TXT, HTML, PPTX, MD

-

- Processing may take up to 2 minutes -

@@ -978,7 +1023,9 @@ export const FileExplorer: React.FC = ({ disabled={ isConverting || (conversionMethod === "azure" && - (!azureEndpoint || !azureKey)) + (!azureEndpoint || !azureKey)) || + (conversionMethod === "custom-docling" && + !customDoclingUrl) } className="min-w-[100px]" >