151 lines
4.9 KiB
Python
151 lines
4.9 KiB
Python
"""Generic HTTP API processor wrapper for custom document processing services."""
|
|
|
|
import logging
|
|
from collections.abc import Awaitable, Callable
|
|
from typing import Any, Optional
|
|
|
|
import httpx
|
|
|
|
from .base import DocumentProcessor, ProcessingResult, ProcessorError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class CustomHTTPProcessor(DocumentProcessor):
|
|
"""Generic HTTP API processor wrapper.
|
|
|
|
Allows integration with any custom document processing API that follows
|
|
a simple request/response pattern. This makes it easy to integrate your
|
|
own text extraction services without writing a full processor.
|
|
|
|
Expected API Contract:
|
|
- POST request with file as multipart/form-data
|
|
- Response: {"text": "extracted text", "metadata": {...}}
|
|
|
|
Example:
|
|
processor = CustomHTTPProcessor(
|
|
name="my_ocr",
|
|
api_url="https://my-ocr-service.com/process",
|
|
api_key="secret",
|
|
supported_types={"application/pdf", "image/jpeg"},
|
|
)
|
|
result = await processor.process(pdf_bytes, "application/pdf")
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
api_url: str,
|
|
api_key: Optional[str] = None,
|
|
timeout: int = 60,
|
|
supported_types: Optional[set[str]] = None,
|
|
name: str = "custom",
|
|
):
|
|
"""Initialize custom HTTP processor.
|
|
|
|
Args:
|
|
api_url: Your API endpoint (should accept POST with multipart/form-data)
|
|
api_key: Optional API key for authentication (sent as Bearer token)
|
|
timeout: Request timeout in seconds (default: 60)
|
|
supported_types: MIME types your API supports
|
|
name: Unique name for this processor (default: "custom")
|
|
"""
|
|
self.api_url = api_url
|
|
self.api_key = api_key
|
|
self.timeout = timeout
|
|
self._name = name
|
|
self._supported_types = supported_types or set()
|
|
|
|
logger.info(f"Initialized CustomHTTPProcessor: {name} -> {api_url}")
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return self._name
|
|
|
|
@property
|
|
def supported_mime_types(self) -> set[str]:
|
|
return self._supported_types
|
|
|
|
async def process(
|
|
self,
|
|
content: bytes,
|
|
content_type: str,
|
|
filename: Optional[str] = None,
|
|
options: Optional[dict[str, Any]] = None,
|
|
progress_callback: Optional[
|
|
Callable[[float, Optional[float], Optional[str]], Awaitable[None]]
|
|
] = None,
|
|
) -> ProcessingResult:
|
|
"""Process via custom HTTP API.
|
|
|
|
Args:
|
|
content: Document bytes
|
|
content_type: MIME type
|
|
filename: Optional filename
|
|
options: Custom options (passed as form data to API)
|
|
|
|
Returns:
|
|
ProcessingResult with extracted text and metadata
|
|
|
|
Raises:
|
|
ProcessorError: If API call fails
|
|
"""
|
|
options = options or {}
|
|
|
|
# Prepare request
|
|
files = {"file": (filename or "document", content, content_type)}
|
|
headers = {}
|
|
|
|
if self.api_key:
|
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
response = await client.post(
|
|
self.api_url,
|
|
files=files,
|
|
headers=headers,
|
|
data=options, # Pass options as form data
|
|
)
|
|
response.raise_for_status()
|
|
|
|
# Parse response
|
|
result = response.json()
|
|
text = result.get("text", "")
|
|
metadata = result.get("metadata", {})
|
|
|
|
logger.debug(
|
|
f"Custom processor '{self.name}' extracted {len(text)} characters"
|
|
)
|
|
|
|
return ProcessingResult(
|
|
text=text,
|
|
metadata=metadata,
|
|
processor=self.name,
|
|
success=True,
|
|
)
|
|
|
|
except httpx.HTTPError as e:
|
|
logger.error(f"Custom processor '{self.name}' HTTP error: {e}")
|
|
raise ProcessorError(f"API call failed: {str(e)}") from e
|
|
except Exception as e:
|
|
logger.error(f"Custom processor '{self.name}' failed: {e}")
|
|
raise ProcessorError(f"Processing failed: {str(e)}") from e
|
|
|
|
async def health_check(self) -> bool:
|
|
"""Check if custom API is available.
|
|
|
|
Returns:
|
|
True if API responds with status < 500
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5) as client:
|
|
# Try GET request to check availability
|
|
response = await client.get(
|
|
self.api_url,
|
|
headers={"User-Agent": "nextcloud-mcp-server"},
|
|
)
|
|
return response.status_code < 500
|
|
except Exception as e:
|
|
logger.warning(f"Custom processor '{self.name}' health check failed: {e}")
|
|
return False
|