Skip to content
Open
Show file tree
Hide file tree
Changes from 21 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
66 changes: 66 additions & 0 deletions api/controllers/console/app/conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ def get(self, app_model):

return conversations

@api.doc("clear_completion_conversations")
@api.doc(description="Clear completion conversations and related data")
@api.doc(params={"app_id": "Application ID"})
@api.expect(
api.parser().add_argument(
"conversation_ids",
type=list,
location="json",
required=False,
help="Optional list of conversation IDs to clear. If not provided, all conversations will be cleared.",
)
)
@api.response(202, "Clearing task queued successfully")
@api.response(403, "Insufficient permissions")
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=AppMode.COMPLETION)
@edit_permission_required
def delete(self, app_model):
current_user, _ = current_account_with_tenant()
parser = reqparse.RequestParser()
parser.add_argument("conversation_ids", type=list, location="json", required=False)
args = parser.parse_args()

conversation_ids = [str(id) for id in args["conversation_ids"]] if args.get("conversation_ids") else None

result = ConversationService.clear_conversations(
app_model=app_model, user=current_user, conversation_ids=conversation_ids
)

return result, 202


@console_ns.route("/apps/<uuid:app_id>/completion-conversations/<uuid:conversation_id>")
class CompletionConversationDetailApi(Resource):
Expand Down Expand Up @@ -337,6 +370,39 @@ def get(self, app_model):

return conversations

@api.doc("clear_chat_conversations")
@api.doc(description="Clear chat conversations and related data")
@api.doc(params={"app_id": "Application ID"})
@api.expect(
api.parser().add_argument(
"conversation_ids",
type=list,
location="json",
required=False,
help="Optional list of conversation IDs to clear. If not provided, all conversations will be cleared.",
)
)
@api.response(202, "Clearing task queued successfully")
@api.response(403, "Insufficient permissions")
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT])
@edit_permission_required
def delete(self, app_model):
current_user, _ = current_account_with_tenant()
parser = reqparse.RequestParser()
parser.add_argument("conversation_ids", type=list, location="json", required=False)
args = parser.parse_args()

conversation_ids = [str(id) for id in args["conversation_ids"]] if args.get("conversation_ids") else None

result = ConversationService.clear_conversations(
app_model=app_model, user=current_user, conversation_ids=conversation_ids
)

return result, 202


@console_ns.route("/apps/<uuid:app_id>/chat-conversations/<uuid:conversation_id>")
class ChatConversationDetailApi(Resource):
Expand Down
88 changes: 86 additions & 2 deletions api/services/conversation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
LastConversationNotExistsError,
)
from services.errors.message import MessageNotExistsError
from tasks.clear_conversation_task import clear_conversations_task
from tasks.delete_conversation_task import delete_conversation_related_data

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -128,7 +129,18 @@ def rename(
else:
conversation.name = name
conversation.updated_at = naive_utc_now()
db.session.commit()
try:
db.session.commit()
except Exception as e:
# Handle case where conversation was deleted after we retrieved it
from sqlalchemy.orm.exc import StaleDataError

db.session.rollback()
if isinstance(e, StaleDataError):
# Conversation was likely deleted, raise ConversationNotExistsError
raise ConversationNotExistsError()
else:
raise

return conversation

Expand All @@ -152,7 +164,18 @@ def auto_generate_name(cls, app_model: App, conversation: Conversation):
)
conversation.name = name

db.session.commit()
try:
db.session.commit()
except Exception as e:
# Handle case where conversation was deleted after we retrieved it
from sqlalchemy.orm.exc import StaleDataError

db.session.rollback()
if isinstance(e, StaleDataError):
# Conversation was likely deleted, raise ConversationNotExistsError
raise ConversationNotExistsError()
else:
raise

return conversation

Expand Down Expand Up @@ -194,6 +217,67 @@ def delete(cls, app_model: App, conversation_id: str, user: Union[Account, EndUs
db.session.rollback()
raise e

@classmethod
def clear_conversations(
cls, app_model: App, user: Union[Account, EndUser] | None, conversation_ids: list[str] | None = None
) -> dict[str, Any]:
"""
Clear conversations and related data, optionally for specific conversation IDs.
Uses Celery task for handling large datasets.

Args:
app_model: The app model
user: The user (Account or EndUser)
conversation_ids: Optional list of specific conversation IDs to clear

Returns:
dict with task info and estimated counts
"""
# Validate conversation ownership if specific IDs provided
if conversation_ids:
for conversation_id in conversation_ids:
cls.get_conversation(app_model, conversation_id, user)

# Get conversation mode for task
if app_model.mode == "completion":
mode = "completion"
else:
mode = "chat" # covers chat, agent-chat, advanced-chat

# Queue the Celery task
task = clear_conversations_task.delay(
app_id=app_model.id,
conversation_mode=mode,
conversation_ids=conversation_ids,
user_id=user.id if user else None,
user_type="account" if isinstance(user, Account) else "end_user" if isinstance(user, EndUser) else None,
)

# Get estimated counts for response
if conversation_ids:
conversation_count = len(conversation_ids)
else:
# Estimate total conversations for this app
conversation_count = (
db.session.query(Conversation)
.filter(
Conversation.app_id == app_model.id,
Conversation.mode == mode,
Conversation.from_source == ("api" if isinstance(user, EndUser) else "console"),
Conversation.from_end_user_id == (user.id if isinstance(user, EndUser) else None),
Conversation.from_account_id == (user.id if isinstance(user, Account) else None),
Conversation.is_deleted == False,
)
.count()
)

return {
"task_id": task.id,
"status": "queued",
"estimated_conversations": conversation_count,
"mode": "selective" if conversation_ids else "all",
}

@classmethod
def get_conversational_variable(
cls,
Expand Down
39 changes: 39 additions & 0 deletions api/tasks/clean_uploaded_files_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import logging

from celery import shared_task

from extensions.ext_database import db
from extensions.ext_storage import storage
from models.model import UploadFile

logger = logging.getLogger(__name__)


@shared_task(queue="dataset")
def clean_uploaded_files_task(upload_file_ids: list[str]):
"""
Asynchronously clean uploaded files from storage.
This task is called after database records have been successfully deleted.

Args:
upload_file_ids: List of upload file IDs to delete from storage
"""
if not upload_file_ids:
return

success_count = 0
failed_count = 0

for upload_file_id in upload_file_ids:
try:
upload_file = db.session.query(UploadFile).where(UploadFile.id == upload_file_id).first()

if upload_file and upload_file.key:
storage.delete(upload_file.key)
success_count += 1

except Exception:
logger.exception("Failed to delete upload file %s", upload_file_id)
failed_count += 1

return {"total": len(upload_file_ids), "success": success_count, "failed": failed_count}
Loading