feat: add Paperless-ngx document storage integration
- Add /documents router with webhook, upload, search, health endpoints - Create DocumentSyncService for indexing documents to vectors/graph - Add PaperlessClient for REST API integration - Configure dependency injection for Paperless client - Add document models for webhook payloads and responses - Event-driven architecture via Paperless workflow webhooks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
Document storage models for Library Desk.
|
||||
|
||||
Models for Paperless-ngx document management, virus scanning,
|
||||
and document sync operations.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class DocumentType(str, Enum):
|
||||
"""Types of documents supported in the document store."""
|
||||
PDF = "pdf"
|
||||
IMAGE = "image"
|
||||
VIDEO = "video"
|
||||
TEXT = "text"
|
||||
ARCHIVE = "archive"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class SyncStatus(str, Enum):
|
||||
"""Status of document sync with Library Desk."""
|
||||
PENDING = "pending"
|
||||
INDEXED = "indexed"
|
||||
FAILED = "failed"
|
||||
SKIPPED = "skipped"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Document Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DocumentMetadata(BaseModel):
|
||||
"""Metadata for a document in Paperless-ngx."""
|
||||
paperless_id: int = Field(..., description="Paperless-ngx document ID")
|
||||
title: str = Field(..., description="Document title")
|
||||
filename: Optional[str] = Field(None, description="Original filename")
|
||||
content: Optional[str] = Field(None, description="Extracted text content")
|
||||
created: Optional[datetime] = Field(None, description="Document creation date")
|
||||
modified: Optional[datetime] = Field(None, description="Last modification date")
|
||||
added: Optional[datetime] = Field(None, description="Date added to Paperless")
|
||||
correspondent: Optional[str] = Field(None, description="Correspondent name")
|
||||
document_type: Optional[str] = Field(None, description="Document type name")
|
||||
tags: List[str] = Field(default_factory=list, description="Tag names")
|
||||
custom_fields: Dict[str, Any] = Field(default_factory=dict, description="Custom field values")
|
||||
|
||||
|
||||
class DocumentRecord(BaseModel):
|
||||
"""A document record with sync status."""
|
||||
metadata: DocumentMetadata = Field(..., description="Document metadata from Paperless")
|
||||
sync_status: SyncStatus = Field(default=SyncStatus.PENDING, description="Library Desk sync status")
|
||||
indexed_at: Optional[datetime] = Field(None, description="When indexed in Library Desk")
|
||||
collection: Optional[str] = Field(None, description="Collection name (e.g., 'fastapi-docs')")
|
||||
source_url: Optional[str] = Field(None, description="Original source URL if uploaded via HybridRAG")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Upload Request/Response Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DocumentUploadRequest(BaseModel):
|
||||
"""Request to upload a document to Paperless-ngx."""
|
||||
url: Optional[str] = Field(None, description="URL to download document from")
|
||||
title: Optional[str] = Field(None, description="Document title (derived from filename if not set)")
|
||||
collection: Optional[str] = Field(None, description="Collection to add document to")
|
||||
tags: List[str] = Field(default_factory=list, description="Tags to apply")
|
||||
correspondent: Optional[str] = Field(None, description="Correspondent name")
|
||||
document_type: Optional[str] = Field(None, description="Document type name")
|
||||
|
||||
|
||||
class DocumentUploadResponse(BaseModel):
|
||||
"""Response from document upload."""
|
||||
task_id: str = Field(..., description="Paperless task ID for tracking")
|
||||
filename: str = Field(..., description="Uploaded filename")
|
||||
message: str = Field(..., description="Status message")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Webhook Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class PaperlessWebhookPayload(BaseModel):
|
||||
"""Payload from Paperless-ngx webhook."""
|
||||
document_id: int = Field(..., description="Paperless document ID")
|
||||
event: str = Field(..., description="Event type (document_added, document_updated)")
|
||||
title: Optional[str] = Field(None, description="Document title")
|
||||
correspondent: Optional[int] = Field(None, description="Correspondent ID")
|
||||
document_type: Optional[int] = Field(None, description="Document type ID")
|
||||
tags: List[int] = Field(default_factory=list, description="Tag IDs")
|
||||
|
||||
|
||||
class WebhookResponse(BaseModel):
|
||||
"""Response to webhook processing."""
|
||||
document_id: int = Field(..., description="Processed document ID")
|
||||
status: str = Field(..., description="Processing status")
|
||||
indexed: bool = Field(..., description="Whether document was indexed")
|
||||
message: Optional[str] = Field(None, description="Additional details")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Sync Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class SyncRequest(BaseModel):
|
||||
"""Request to sync documents from Paperless-ngx."""
|
||||
since: Optional[datetime] = Field(None, description="Only sync documents modified after this time")
|
||||
collection: Optional[str] = Field(None, description="Only sync documents in this collection")
|
||||
limit: int = Field(default=100, ge=1, le=1000, description="Maximum documents to sync")
|
||||
force_reindex: bool = Field(default=False, description="Re-index already indexed documents")
|
||||
|
||||
|
||||
class SyncResult(BaseModel):
|
||||
"""Result of a sync operation."""
|
||||
documents_found: int = Field(..., description="Total documents matching criteria")
|
||||
documents_indexed: int = Field(..., description="Successfully indexed")
|
||||
documents_skipped: int = Field(..., description="Skipped (already indexed)")
|
||||
documents_failed: int = Field(..., description="Failed to index")
|
||||
errors: List[str] = Field(default_factory=list, description="Error messages")
|
||||
duration_seconds: float = Field(..., description="Sync duration")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Collection Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class Collection(BaseModel):
|
||||
"""A logical grouping of documents."""
|
||||
name: str = Field(..., description="Collection name (e.g., 'fastapi-docs')")
|
||||
description: Optional[str] = Field(None, description="Collection description")
|
||||
document_count: int = Field(default=0, description="Number of documents")
|
||||
source: Optional[str] = Field(None, description="Source (e.g., 'github.com/tiangolo/fastapi')")
|
||||
last_sync: Optional[datetime] = Field(None, description="Last sync timestamp")
|
||||
wiki_page: Optional[str] = Field(None, description="Wiki catalog page path")
|
||||
|
||||
|
||||
class CollectionListResponse(BaseModel):
|
||||
"""Response listing all collections."""
|
||||
collections: List[Collection] = Field(..., description="List of collections")
|
||||
total_documents: int = Field(..., description="Total documents across all collections")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Search Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DocumentSearchRequest(BaseModel):
|
||||
"""Request to search documents."""
|
||||
query: str = Field(..., min_length=1, description="Search query")
|
||||
collection: Optional[str] = Field(None, description="Limit to collection")
|
||||
document_type: Optional[DocumentType] = Field(None, description="Filter by type")
|
||||
limit: int = Field(default=10, ge=1, le=50, description="Maximum results")
|
||||
include_content: bool = Field(default=False, description="Include full text content")
|
||||
|
||||
|
||||
class DocumentSearchHit(BaseModel):
|
||||
"""A document search result."""
|
||||
paperless_id: int = Field(..., description="Paperless document ID")
|
||||
title: str = Field(..., description="Document title")
|
||||
score: float = Field(..., description="Relevance score")
|
||||
highlights: Optional[str] = Field(None, description="Highlighted matching text")
|
||||
collection: Optional[str] = Field(None, description="Collection name")
|
||||
document_type: Optional[str] = Field(None, description="Document type")
|
||||
content_preview: Optional[str] = Field(None, description="Content preview if requested")
|
||||
|
||||
|
||||
class DocumentSearchResponse(BaseModel):
|
||||
"""Response from document search."""
|
||||
query: str = Field(..., description="Original query")
|
||||
hits: List[DocumentSearchHit] = Field(..., description="Search results")
|
||||
total: int = Field(..., description="Total matching documents")
|
||||
duration_ms: int = Field(..., description="Search duration in milliseconds")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Health Check Models
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class DocumentStoreHealth(BaseModel):
|
||||
"""Health status of document storage components."""
|
||||
paperless_healthy: bool = Field(..., description="Paperless-ngx responding")
|
||||
paperless_version: Optional[str] = Field(None, description="Paperless version")
|
||||
total_documents: Optional[int] = Field(None, description="Total documents in Paperless")
|
||||
indexed_documents: Optional[int] = Field(None, description="Documents indexed in Library Desk")
|
||||
Reference in New Issue
Block a user