Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ea0bc7d
feat: add selective log deletion functionality to improve log management
Aug 28, 2025
3237ed1
refactor: move conversation deletion to service layer with Celery tasks
Aug 28, 2025
0d1ba93
Fix conversation clearing and prevent 404 errors
Sep 2, 2025
642cd31
Merge upstream main and resolve conflicts
Sep 2, 2025
4e0979a
Optimize PR based on feedback - improve code quality and accessibility
Sep 4, 2025
8518150
Fix linter formatting issues
Sep 4, 2025
7870f70
Fix overly aggressive localStorage clearing - address PR feedback
Sep 4, 2025
5a7be42
Fix import error for CONVERSATION_ID_INFO constant
Sep 4, 2025
e774b84
Merge branch 'main' into feat/selective-log-deletion
crazywoola Sep 8, 2025
1f305b2
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 12, 2025
f3d11cf
Simplify conversation deletion parameter parsing and resolve merge co…
connermo Sep 13, 2025
b014204
Fix merge conflict in imports: add missing Account import
connermo Sep 13, 2025
d6f2b70
Merge branch 'main' into feat/selective-log-deletion
crazywoola Sep 16, 2025
d2b90cc
Merge branch 'main' into feat/selective-log-deletion
connermo Sep 16, 2025
19b38d8
Merge branch 'main' into feat/selective-log-deletion
crazywoola Oct 15, 2025
a2eba9a
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 15, 2025
a259b4c
Merge branch 'main' into feat/selective-log-deletion
asukaminato0721 Oct 19, 2025
034ce08
[autofix.ci] apply automated fixes
autofix-ci[bot] Oct 19, 2025
41844ec
refactor: consolidate conversation deletion logic and fix critical is…
connermo Nov 2, 2025
c7d6d92
Merge upstream/main into feat/selective-log-deletion
connermo Nov 2, 2025
31f6a06
fix: remove unused handleRowClick function
connermo Nov 2, 2025
ec9dc0a
fix: improve log clearing and prevent 404 errors
connermo Nov 3, 2025
8da334c
feat: prevent duplicate log clearing tasks with Redis locks and optim…
connermo Nov 3, 2025
5817c66
fix: enable auto-refresh for log list on focus and reconnect
connermo Nov 3, 2025
6349de7
fix: implement optimistic updates for log deletion
connermo Nov 3, 2025
1c7a4b6
fix: refresh log list when navigating from other pages
connermo Nov 3, 2025
f223eaa
fix: refresh conversation list when navigating to configuration page
connermo Nov 3, 2025
ce1f293
Merge branch 'main' into feat/selective-log-deletion
connermo Nov 3, 2025
f9f089c
fix: ensure conversation list refreshes after deletion in same tab
connermo Nov 3, 2025
b56b6d1
feat: implement universal conversation list sync mechanism
connermo Nov 3, 2025
8ae7537
Merge upstream/main into feat/selective-log-deletion
connermo Nov 4, 2025
43d6fb3
Merge upstream/main into feat/selective-log-deletion
connermo Nov 24, 2025
1a19809
Merge branch 'main' into feat/selective-log-deletion
connermo Nov 24, 2025
0983027
Merge branch 'main' into feat/selective-log-deletion
crazywoola Nov 26, 2025
5bbd7b0
Merge branch 'main' into feat/selective-log-deletion
connermo Nov 30, 2025
30f4dee
Merge branch 'main' into feat/selective-log-deletion
connermo Dec 3, 2025
427fe15
fix: migrate clear conversations methods from reqparse to Pydantic mo…
connermo Dec 3, 2025
0d80ae2
fix: resolve web style check errors and pagination bug
connermo Dec 3, 2025
8aaccd9
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 3, 2025
59aa0e6
fix: change confirmLoading to isLoading in Confirm component
connermo Dec 3, 2025
6b96b4f
Merge branch 'main' into feat/selective-log-deletion
connermo Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 284 additions & 1 deletion api/controllers/console/app/conversation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging

Check failure on line 1 in api/controllers/console/app/conversation.py

View workflow job for this annotation

GitHub Actions / Style Check / Python Style

Import "logging" is not accessed (reportUnusedImport)
from datetime import datetime

import pytz # pip install pytz
Expand All @@ -6,6 +7,7 @@
from flask_restx import Resource, marshal_with, reqparse
from flask_restx.inputs import int_range
from sqlalchemy import func, or_
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy.orm import joinedload
from werkzeug.exceptions import Forbidden, NotFound

Expand All @@ -24,7 +26,10 @@
from libs.helper import DatetimeString
from libs.login import login_required
from models import Account, Conversation, EndUser, Message, MessageAnnotation
from models.model import AppMode
from models.model import AppMode, MessageAgentThought, MessageChain, MessageFeedback, MessageFile, UploadFile
from models.tools import ToolConversationVariables, ToolFile
from models.web import PinnedConversation
from models.workflow import ConversationVariable
from services.conversation_service import ConversationService
from services.errors.conversation import ConversationNotExistsError

Expand Down Expand Up @@ -123,6 +128,144 @@

return conversations

@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=AppMode.COMPLETION)
def delete(self, app_model):
"""Clear completion conversations and related data including files"""
if not current_user.is_editor:
logger.warning(

Check failure on line 138 in api/controllers/console/app/conversation.py

View workflow job for this annotation

GitHub Actions / Style Check / Python Style

"logger" is not defined (reportUndefinedVariable)
"Unauthorized deletion attempt: user %s tried to delete completion conversations for app %s",
current_user.id,
app_model.id,
)
raise Forbidden()

parser = reqparse.RequestParser()
parser.add_argument("conversation_ids", type=list, location="json")
args = parser.parse_args()

# If specific conversation IDs provided, delete only those; otherwise delete all
if args["conversation_ids"]:
conversation_ids = [str(id) for id in args["conversation_ids"]]
conversations = (
db.session.query(Conversation)
.filter(
Conversation.app_id == app_model.id,
Conversation.mode == "completion",
Conversation.id.in_(conversation_ids),
)
.all()
)
else:
# Get all conversations for this app
conversations = (
db.session.query(Conversation)
.filter(Conversation.app_id == app_model.id, Conversation.mode == "completion")
.all()
)

# Collect all message IDs and upload file IDs first
all_message_ids = []
upload_file_ids = []

for conversation in conversations:
message_ids = db.session.query(Message.id).where(Message.conversation_id == conversation.id).all()
all_message_ids.extend([msg_id[0] for msg_id in message_ids])

# Collect upload file IDs for async deletion
if all_message_ids:
message_files = db.session.query(MessageFile).where(MessageFile.message_id.in_(all_message_ids)).all()
upload_file_ids = [mf.upload_file_id for mf in message_files if mf.upload_file_id]

# Delete all database records first (in transaction)
try:
# Delete message-related database records
if all_message_ids:
try:
db.session.query(MessageFeedback).where(MessageFeedback.message_id.in_(all_message_ids)).delete(
synchronize_session=False
)
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(MessageFile).where(MessageFile.message_id.in_(all_message_ids)).delete(
synchronize_session=False
)
except Exception:
pass
try:
db.session.query(MessageChain).where(MessageChain.message_id.in_(all_message_ids)).delete(
synchronize_session=False
)
except Exception:
pass
try:
db.session.query(MessageAgentThought).where(MessageAgentThought.message_id.in_(all_message_ids)).delete(synchronize_session=False)
except Exception:
pass

# Delete messages, annotations, and conversation variables
for conversation in conversations:
db.session.query(Message).where(Message.conversation_id == conversation.id).delete()
db.session.query(MessageAnnotation).where(MessageAnnotation.conversation_id == conversation.id).delete()
try:
db.session.query(ConversationVariable).where(ConversationVariable.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(ToolConversationVariables).where(ToolConversationVariables.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(ToolFile).where(ToolFile.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(PinnedConversation).where(PinnedConversation.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version

# Delete upload file records
if upload_file_ids:
db.session.query(UploadFile).where(UploadFile.id.in_(upload_file_ids)).delete(
synchronize_session=False
)

# Delete conversations
if args["conversation_ids"]:
conversation_ids = [str(id) for id in args["conversation_ids"]]
db.session.query(Conversation).filter(
Conversation.app_id == app_model.id,
Conversation.mode == "completion",
Conversation.id.in_(conversation_ids),
).delete(synchronize_session=False)
else:
db.session.query(Conversation).filter(
Conversation.app_id == app_model.id, Conversation.mode == "completion"
).delete()

# Commit all database changes first
db.session.commit()

# Schedule async file cleanup after successful database deletion
if upload_file_ids:
from tasks.clean_uploaded_files_task import clean_uploaded_files_task

clean_uploaded_files_task.delay(upload_file_ids)

except Exception as e:
db.session.rollback()
raise e

return {
"result": "success",
"conversations_deleted": len(conversations),
"messages_deleted": len(all_message_ids),
"files_deleted": len(upload_file_ids),
}


@console_ns.route("/apps/<uuid:app_id>/completion-conversations/<uuid:conversation_id>")
class CompletionConversationDetailApi(Resource):
Expand Down Expand Up @@ -327,6 +470,146 @@

return conversations

@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
def delete(self, app_model):
"""Clear chat conversations and related data including files"""
if not current_user.is_editor:
logger.warning(

Check failure on line 480 in api/controllers/console/app/conversation.py

View workflow job for this annotation

GitHub Actions / Style Check / Python Style

"logger" is not defined (reportUndefinedVariable)
"Unauthorized deletion attempt: user %s tried to delete chat conversations for app %s",
current_user.id,
app_model.id,
)
raise Forbidden()

parser = reqparse.RequestParser()
parser.add_argument("conversation_ids", type=list, location="json")
args = parser.parse_args()

# If specific conversation IDs provided, delete only those; otherwise delete all
if args["conversation_ids"]:
conversation_ids = [str(id) for id in args["conversation_ids"]]
conversations = (
db.session.query(Conversation)
.filter(
Conversation.app_id == app_model.id,
Conversation.mode.in_(["chat", "agent-chat", "advanced-chat"]),
Conversation.id.in_(conversation_ids),
)
.all()
)
else:
# Get all conversations for this app
conversations = (
db.session.query(Conversation)
.filter(
Conversation.app_id == app_model.id, Conversation.mode.in_(["chat", "agent-chat", "advanced-chat"])
)
.all()
)

# Collect all message IDs and upload file IDs first
all_message_ids = []
upload_file_ids = []

for conversation in conversations:
message_ids = db.session.query(Message.id).where(Message.conversation_id == conversation.id).all()
all_message_ids.extend([msg_id[0] for msg_id in message_ids])

# Collect upload file IDs for async deletion
if all_message_ids:
message_files = db.session.query(MessageFile).where(MessageFile.message_id.in_(all_message_ids)).all()
upload_file_ids = [mf.upload_file_id for mf in message_files if mf.upload_file_id]

# Delete all database records first (in transaction)
try:
# Delete message-related database records
if all_message_ids:
try:
db.session.query(MessageFeedback).where(MessageFeedback.message_id.in_(all_message_ids)).delete(
synchronize_session=False
)
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(MessageFile).where(MessageFile.message_id.in_(all_message_ids)).delete(
synchronize_session=False
)
except Exception:
pass
try:
db.session.query(MessageChain).where(MessageChain.message_id.in_(all_message_ids)).delete(
synchronize_session=False
)
except Exception:
pass
try:
db.session.query(MessageAgentThought).where(MessageAgentThought.message_id.in_(all_message_ids)).delete(synchronize_session=False)
except Exception:
pass

# Delete messages, annotations, and conversation variables
for conversation in conversations:
db.session.query(Message).where(Message.conversation_id == conversation.id).delete()
db.session.query(MessageAnnotation).where(MessageAnnotation.conversation_id == conversation.id).delete()
try:
db.session.query(ConversationVariable).where(ConversationVariable.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(ToolConversationVariables).where(ToolConversationVariables.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(ToolFile).where(ToolFile.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version
try:
db.session.query(PinnedConversation).where(PinnedConversation.conversation_id == conversation.id).delete()
except (OperationalError, ProgrammingError):
pass # Table might not exist in this version

# Delete upload file records
if upload_file_ids:
db.session.query(UploadFile).where(UploadFile.id.in_(upload_file_ids)).delete(
synchronize_session=False
)

# Delete conversations
if args["conversation_ids"]:
conversation_ids = [str(id) for id in args["conversation_ids"]]
db.session.query(Conversation).filter(
Conversation.app_id == app_model.id,
Conversation.mode.in_(["chat", "agent-chat", "advanced-chat"]),
Conversation.id.in_(conversation_ids),
).delete(synchronize_session=False)
else:
db.session.query(Conversation).filter(
Conversation.app_id == app_model.id, Conversation.mode.in_(["chat", "agent-chat", "advanced-chat"])
).delete()

# Commit all database changes first
db.session.commit()

# Schedule async file cleanup after successful database deletion
if upload_file_ids:
from tasks.clean_uploaded_files_task import clean_uploaded_files_task

clean_uploaded_files_task.delay(upload_file_ids)

except Exception as e:
db.session.rollback()
raise e

return {
"result": "success",
"conversations_deleted": len(conversations),
"messages_deleted": len(all_message_ids),
"files_deleted": len(upload_file_ids),
}


@console_ns.route("/apps/<uuid:app_id>/chat-conversations/<uuid:conversation_id>")
class ChatConversationDetailApi(Resource):
Expand Down
Loading
Loading