File size: 4,735 Bytes
310260a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""
Conversation Cleanup Job

Implements 90-day retention policy for conversations and messages.
Permanently deletes soft-deleted conversations older than 90 days.
"""

from sqlmodel import Session, select
from datetime import datetime, timedelta
from ..database import engine
from ..models.conversation import Conversation
from ..models.message import Message
from ..utils.logging import StructuredLogger

logger = StructuredLogger("cleanup")


def cleanup_old_conversations(dry_run: bool = False) -> dict:
    """
    Delete conversations and messages older than 90 days.

    This function implements the 90-day retention policy by:
    1. Finding conversations soft-deleted more than 90 days ago
    2. Deleting associated messages
    3. Permanently deleting the conversations

    Args:
        dry_run: If True, only count records without deleting

    Returns:
        Dict with cleanup statistics
    """
    cutoff_date = datetime.utcnow() - timedelta(days=90)

    with Session(engine) as session:
        # Find conversations to delete
        statement = select(Conversation).where(
            Conversation.deleted_at.is_not(None),
            Conversation.deleted_at < cutoff_date
        )
        conversations_to_delete = session.exec(statement).all()

        conversation_count = len(conversations_to_delete)
        message_count = 0

        if not dry_run:
            for conversation in conversations_to_delete:
                # Delete associated messages
                message_statement = select(Message).where(
                    Message.conversation_id == conversation.id
                )
                messages = session.exec(message_statement).all()
                message_count += len(messages)

                for message in messages:
                    session.delete(message)

                # Delete conversation
                session.delete(conversation)

            session.commit()

            logger.info(
                "cleanup_completed",
                conversations_deleted=conversation_count,
                messages_deleted=message_count,
                cutoff_date=cutoff_date.isoformat()
            )
        else:
            # Count messages without deleting
            for conversation in conversations_to_delete:
                message_statement = select(Message).where(
                    Message.conversation_id == conversation.id
                )
                messages = session.exec(message_statement).all()
                message_count += len(messages)

            logger.info(
                "cleanup_dry_run",
                conversations_to_delete=conversation_count,
                messages_to_delete=message_count,
                cutoff_date=cutoff_date.isoformat()
            )

    return {
        "conversations_deleted": conversation_count,
        "messages_deleted": message_count,
        "cutoff_date": cutoff_date.isoformat(),
        "dry_run": dry_run
    }


def cleanup_orphaned_messages() -> dict:
    """
    Delete messages that belong to deleted conversations.

    This is a safety cleanup for any orphaned messages.

    Returns:
        Dict with cleanup statistics
    """
    with Session(engine) as session:
        # Find messages with no parent conversation
        statement = select(Message).where(
            ~Message.conversation_id.in_(
                select(Conversation.id)
            )
        )
        orphaned_messages = session.exec(statement).all()

        count = len(orphaned_messages)

        for message in orphaned_messages:
            session.delete(message)

        session.commit()

        logger.info(
            "orphaned_messages_cleanup",
            messages_deleted=count
        )

    return {
        "orphaned_messages_deleted": count
    }


if __name__ == "__main__":
    """
    Run cleanup job from command line.

    Usage:
        python -m backend.src.jobs.cleanup_conversations
        python -m backend.src.jobs.cleanup_conversations --dry-run
    """
    import sys

    dry_run = "--dry-run" in sys.argv

    print("Starting conversation cleanup job...")
    print(f"Dry run: {dry_run}")
    print(f"Cutoff date: {(datetime.utcnow() - timedelta(days=90)).isoformat()}")
    print()

    # Run main cleanup
    result = cleanup_old_conversations(dry_run=dry_run)
    print(f"Conversations deleted: {result['conversations_deleted']}")
    print(f"Messages deleted: {result['messages_deleted']}")
    print()

    # Run orphaned messages cleanup
    if not dry_run:
        orphaned_result = cleanup_orphaned_messages()
        print(f"Orphaned messages deleted: {orphaned_result['orphaned_messages_deleted']}")
        print()

    print("Cleanup job completed!")