mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-12-09 08:15:27 +01:00
Merge branch 'dev' into copilot/add-webhook-system-ai-events
Resolved merge conflicts in: - src/documents/ai_deletion_manager.py: Kept webhook integration alongside dev changes - src/documents/ai_scanner.py: Kept webhook integration and applied_fields tracking - src/documents/models.py: Integrated AISuggestionFeedback model with webhook imports All conflicts resolved maintaining both webhook functionality and new AI suggestions features from dev branch. Co-authored-by: dawnsystem <42047891+dawnsystem@users.noreply.github.com>
This commit is contained in:
parent
ebc906b713
commit
5ae18e03b5
24 changed files with 5421 additions and 299 deletions
|
|
@ -14,15 +14,11 @@ According to agents.md requirements:
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from documents.models import Document, DeletionRequest
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
logger = logging.getLogger("paperless.ai_deletion")
|
||||
|
||||
|
|
@ -30,35 +26,35 @@ logger = logging.getLogger("paperless.ai_deletion")
|
|||
class AIDeletionManager:
|
||||
"""
|
||||
Manager for AI-initiated deletion requests.
|
||||
|
||||
|
||||
Ensures all deletions go through proper user approval workflow.
|
||||
"""
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_deletion_request(
|
||||
documents: List,
|
||||
documents: list,
|
||||
reason: str,
|
||||
user: User,
|
||||
impact_analysis: Optional[Dict[str, Any]] = None,
|
||||
impact_analysis: dict[str, Any] | None = None,
|
||||
):
|
||||
"""
|
||||
Create a new deletion request that requires user approval.
|
||||
|
||||
|
||||
Args:
|
||||
documents: List of documents to be deleted
|
||||
reason: Detailed explanation from AI
|
||||
user: User who must approve
|
||||
impact_analysis: Optional detailed impact analysis
|
||||
|
||||
|
||||
Returns:
|
||||
Created DeletionRequest instance
|
||||
"""
|
||||
from documents.models import DeletionRequest
|
||||
|
||||
|
||||
# Analyze impact if not provided
|
||||
if impact_analysis is None:
|
||||
impact_analysis = AIDeletionManager._analyze_impact(documents)
|
||||
|
||||
|
||||
# Create request
|
||||
request = DeletionRequest.objects.create(
|
||||
requested_by_ai=True,
|
||||
|
|
@ -67,15 +63,15 @@ class AIDeletionManager:
|
|||
status=DeletionRequest.STATUS_PENDING,
|
||||
impact_summary=impact_analysis,
|
||||
)
|
||||
|
||||
|
||||
# Add documents
|
||||
request.documents.set(documents)
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Created deletion request {request.id} for {len(documents)} documents "
|
||||
f"requiring approval from user {user.username}"
|
||||
f"requiring approval from user {user.username}",
|
||||
)
|
||||
|
||||
|
||||
# Send webhook notification about deletion request
|
||||
try:
|
||||
from documents.webhooks import send_deletion_request_webhook
|
||||
|
|
@ -85,16 +81,16 @@ class AIDeletionManager:
|
|||
f"Failed to send deletion request webhook: {webhook_error}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
|
||||
# TODO: Send in-app notification to user about pending deletion request
|
||||
|
||||
|
||||
return request
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _analyze_impact(documents: List) -> Dict[str, Any]:
|
||||
def _analyze_impact(documents: list) -> dict[str, Any]:
|
||||
"""
|
||||
Analyze the impact of deleting the given documents.
|
||||
|
||||
|
||||
Returns comprehensive information about what will be affected.
|
||||
"""
|
||||
impact = {
|
||||
|
|
@ -109,7 +105,7 @@ class AIDeletionManager:
|
|||
"latest": None,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
for doc in documents:
|
||||
# Document details
|
||||
doc_info = {
|
||||
|
|
@ -121,77 +117,85 @@ class AIDeletionManager:
|
|||
"tags": [tag.name for tag in doc.tags.all()],
|
||||
}
|
||||
impact["documents"].append(doc_info)
|
||||
|
||||
|
||||
# Track size (if available)
|
||||
# Note: This would need actual file size tracking
|
||||
|
||||
|
||||
# Track affected metadata
|
||||
if doc.correspondent:
|
||||
impact["affected_correspondents"].add(doc.correspondent.name)
|
||||
|
||||
|
||||
if doc.document_type:
|
||||
impact["affected_types"].add(doc.document_type.name)
|
||||
|
||||
|
||||
for tag in doc.tags.all():
|
||||
impact["affected_tags"].add(tag.name)
|
||||
|
||||
|
||||
# Track date range
|
||||
if doc.created:
|
||||
if impact["date_range"]["earliest"] is None or doc.created < impact["date_range"]["earliest"]:
|
||||
if (
|
||||
impact["date_range"]["earliest"] is None
|
||||
or doc.created < impact["date_range"]["earliest"]
|
||||
):
|
||||
impact["date_range"]["earliest"] = doc.created
|
||||
|
||||
if impact["date_range"]["latest"] is None or doc.created > impact["date_range"]["latest"]:
|
||||
|
||||
if (
|
||||
impact["date_range"]["latest"] is None
|
||||
or doc.created > impact["date_range"]["latest"]
|
||||
):
|
||||
impact["date_range"]["latest"] = doc.created
|
||||
|
||||
|
||||
# Convert sets to lists for JSON serialization
|
||||
impact["affected_tags"] = list(impact["affected_tags"])
|
||||
impact["affected_correspondents"] = list(impact["affected_correspondents"])
|
||||
impact["affected_types"] = list(impact["affected_types"])
|
||||
|
||||
|
||||
# Convert dates to ISO format
|
||||
if impact["date_range"]["earliest"]:
|
||||
impact["date_range"]["earliest"] = impact["date_range"]["earliest"].isoformat()
|
||||
impact["date_range"]["earliest"] = impact["date_range"][
|
||||
"earliest"
|
||||
].isoformat()
|
||||
if impact["date_range"]["latest"]:
|
||||
impact["date_range"]["latest"] = impact["date_range"]["latest"].isoformat()
|
||||
|
||||
|
||||
return impact
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_pending_requests(user: User) -> List:
|
||||
def get_pending_requests(user: User) -> list:
|
||||
"""
|
||||
Get all pending deletion requests for a user.
|
||||
|
||||
|
||||
Args:
|
||||
user: User to get requests for
|
||||
|
||||
|
||||
Returns:
|
||||
List of pending DeletionRequest instances
|
||||
"""
|
||||
from documents.models import DeletionRequest
|
||||
|
||||
|
||||
return list(
|
||||
DeletionRequest.objects.filter(
|
||||
user=user,
|
||||
status=DeletionRequest.STATUS_PENDING,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def format_deletion_request_for_user(request) -> str:
|
||||
"""
|
||||
Format a deletion request into a human-readable message.
|
||||
|
||||
|
||||
This provides comprehensive information to the user about what
|
||||
will be deleted, as required by agents.md.
|
||||
|
||||
|
||||
Args:
|
||||
request: DeletionRequest to format
|
||||
|
||||
|
||||
Returns:
|
||||
Formatted message string
|
||||
"""
|
||||
impact = request.impact_summary
|
||||
|
||||
|
||||
message = f"""
|
||||
===========================================
|
||||
AI DELETION REQUEST #{request.id}
|
||||
|
|
@ -201,27 +205,27 @@ REASON:
|
|||
{request.ai_reason}
|
||||
|
||||
IMPACT SUMMARY:
|
||||
- Number of documents: {impact.get('document_count', 0)}
|
||||
- Affected tags: {', '.join(impact.get('affected_tags', [])) or 'None'}
|
||||
- Affected correspondents: {', '.join(impact.get('affected_correspondents', [])) or 'None'}
|
||||
- Affected document types: {', '.join(impact.get('affected_types', [])) or 'None'}
|
||||
- Number of documents: {impact.get("document_count", 0)}
|
||||
- Affected tags: {", ".join(impact.get("affected_tags", [])) or "None"}
|
||||
- Affected correspondents: {", ".join(impact.get("affected_correspondents", [])) or "None"}
|
||||
- Affected document types: {", ".join(impact.get("affected_types", [])) or "None"}
|
||||
|
||||
DATE RANGE:
|
||||
- Earliest: {impact.get('date_range', {}).get('earliest', 'Unknown')}
|
||||
- Latest: {impact.get('date_range', {}).get('latest', 'Unknown')}
|
||||
- Earliest: {impact.get("date_range", {}).get("earliest", "Unknown")}
|
||||
- Latest: {impact.get("date_range", {}).get("latest", "Unknown")}
|
||||
|
||||
DOCUMENTS TO BE DELETED:
|
||||
"""
|
||||
|
||||
for i, doc in enumerate(impact.get('documents', []), 1):
|
||||
|
||||
for i, doc in enumerate(impact.get("documents", []), 1):
|
||||
message += f"""
|
||||
{i}. ID: {doc['id']} - {doc['title']}
|
||||
Created: {doc['created']}
|
||||
Correspondent: {doc['correspondent'] or 'None'}
|
||||
Type: {doc['document_type'] or 'None'}
|
||||
Tags: {', '.join(doc['tags']) or 'None'}
|
||||
{i}. ID: {doc["id"]} - {doc["title"]}
|
||||
Created: {doc["created"]}
|
||||
Correspondent: {doc["correspondent"] or "None"}
|
||||
Type: {doc["document_type"] or "None"}
|
||||
Tags: {", ".join(doc["tags"]) or "None"}
|
||||
"""
|
||||
|
||||
|
||||
message += """
|
||||
===========================================
|
||||
|
||||
|
|
@ -232,21 +236,21 @@ No files will be deleted until you confirm this action.
|
|||
Please review the above information carefully before
|
||||
approving or rejecting this request.
|
||||
"""
|
||||
|
||||
|
||||
return message
|
||||
|
||||
|
||||
@staticmethod
|
||||
def can_ai_delete_automatically() -> bool:
|
||||
"""
|
||||
Check if AI is allowed to delete automatically.
|
||||
|
||||
|
||||
According to agents.md, AI should NEVER delete without user approval.
|
||||
This method always returns False as a safety measure.
|
||||
|
||||
|
||||
Returns:
|
||||
Always False - AI cannot auto-delete
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
__all__ = ['AIDeletionManager']
|
||||
__all__ = ["AIDeletionManager"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue