fix
This commit is contained in:
@@ -8,6 +8,7 @@ from django.contrib.auth.decorators import login_required
|
|||||||
|
|
||||||
from django.db.models import Count, Prefetch
|
from django.db.models import Count, Prefetch
|
||||||
from .models import Conversation, Message
|
from .models import Conversation, Message
|
||||||
|
from .utils import get_preceding_user_message
|
||||||
from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut
|
from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut
|
||||||
from account.models import RoleChoices
|
from account.models import RoleChoices
|
||||||
|
|
||||||
@@ -166,16 +167,15 @@ def delete_message_pair(request, message_id: int):
|
|||||||
if asst_msg.conversation.user != request.user and request.user.role != RoleChoices.SUPER:
|
if asst_msg.conversation.user != request.user and request.user.role != RoleChoices.SUPER:
|
||||||
raise HttpError(403, "只能删除自己的消息")
|
raise HttpError(403, "只能删除自己的消息")
|
||||||
|
|
||||||
|
if asst_msg.submission_id and request.user.role != RoleChoices.SUPER:
|
||||||
|
from submission.models import Rating, SubmissionAward
|
||||||
|
has_ratings = Rating.objects.filter(submission_id=asst_msg.submission_id).exists()
|
||||||
|
has_awards = SubmissionAward.objects.filter(submission_id=asst_msg.submission_id).exists()
|
||||||
|
if has_ratings or has_awards:
|
||||||
|
raise HttpError(400, "该消息关联的提交已被评分或获奖,无法删除")
|
||||||
|
|
||||||
# Find the preceding user message
|
# Find the preceding user message
|
||||||
user_msg = (
|
user_msg = get_preceding_user_message(asst_msg)
|
||||||
Message.objects.filter(
|
|
||||||
conversation=asst_msg.conversation,
|
|
||||||
created__lt=asst_msg.created,
|
|
||||||
role="user",
|
|
||||||
)
|
|
||||||
.order_by("-created")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete messages first, then submission
|
# Delete messages first, then submission
|
||||||
submission_id = asst_msg.submission_id # capture before deletion nulls it
|
submission_id = asst_msg.submission_id # capture before deletion nulls it
|
||||||
@@ -187,10 +187,7 @@ def delete_message_pair(request, message_id: int):
|
|||||||
submission_deleted = False
|
submission_deleted = False
|
||||||
if submission_id:
|
if submission_id:
|
||||||
from submission.models import Submission as SubmissionModel
|
from submission.models import Submission as SubmissionModel
|
||||||
try:
|
SubmissionModel.objects.filter(id=submission_id).delete()
|
||||||
SubmissionModel.objects.filter(id=submission_id).delete()
|
submission_deleted = True
|
||||||
submission_deleted = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {"deleted": True, "submission_deleted": submission_deleted}
|
return {"deleted": True, "submission_deleted": submission_deleted}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
from channels.db import database_sync_to_async
|
from channels.db import database_sync_to_async
|
||||||
from django.db.models import Count
|
from .models import Message
|
||||||
from .models import Conversation, Message
|
from .utils import get_or_create_active_conversation
|
||||||
from .llm import stream_chat, extract_code, stream_guidance, parse_guidance_response
|
from .llm import stream_chat, extract_code, stream_guidance, parse_guidance_response
|
||||||
|
|
||||||
|
|
||||||
@@ -79,15 +79,7 @@ class PromptConsumer(AsyncWebsocketConsumer):
|
|||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def get_or_create_conversation(self):
|
def get_or_create_conversation(self):
|
||||||
conv = (
|
return get_or_create_active_conversation(self.user, self.task_id)
|
||||||
Conversation.objects.filter(user=self.user, task_id=self.task_id)
|
|
||||||
.annotate(msg_count=Count("messages"))
|
|
||||||
.order_by("-msg_count", "-created")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not conv:
|
|
||||||
conv = Conversation.objects.create(user=self.user, task_id=self.task_id)
|
|
||||||
return conv
|
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def delete_message(self, message):
|
def delete_message(self, message):
|
||||||
@@ -195,15 +187,7 @@ class GuidanceConsumer(AsyncWebsocketConsumer):
|
|||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def get_or_create_conversation(self):
|
def get_or_create_conversation(self):
|
||||||
conv = (
|
return get_or_create_active_conversation(self.user, self.task_id)
|
||||||
Conversation.objects.filter(user=self.user, task_id=self.task_id)
|
|
||||||
.annotate(msg_count=Count("messages"))
|
|
||||||
.order_by("-msg_count", "-created")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not conv:
|
|
||||||
conv = Conversation.objects.create(user=self.user, task_id=self.task_id)
|
|
||||||
return conv
|
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def delete_message(self, message):
|
def delete_message(self, message):
|
||||||
|
|||||||
32
prompt/utils.py
Normal file
32
prompt/utils.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from django.db.models import Count, Q
|
||||||
|
from .models import Conversation
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_conversation(user, task_id):
|
||||||
|
"""Return the conversation with the most messages for this user+task, or None."""
|
||||||
|
return (
|
||||||
|
Conversation.objects.filter(user=user, task_id=task_id)
|
||||||
|
.annotate(msg_count=Count("messages"))
|
||||||
|
.order_by("-msg_count", "-created")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_active_conversation(user, task_id):
|
||||||
|
conv = get_active_conversation(user, task_id)
|
||||||
|
if not conv:
|
||||||
|
conv = Conversation.objects.create(user=user, task_id=task_id)
|
||||||
|
return conv
|
||||||
|
|
||||||
|
|
||||||
|
def get_preceding_user_message(asst_msg):
|
||||||
|
"""Return the user message immediately preceding an assistant message in its conversation."""
|
||||||
|
return (
|
||||||
|
asst_msg.conversation.messages.filter(role="user")
|
||||||
|
.filter(
|
||||||
|
Q(created__lt=asst_msg.created)
|
||||||
|
| Q(created=asst_msg.created, id__lt=asst_msg.id)
|
||||||
|
)
|
||||||
|
.order_by("-created", "-id")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import csv
|
import csv
|
||||||
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from typing import List, Literal, Optional
|
from typing import List, Literal, Optional
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@@ -23,7 +24,8 @@ from django.db.models import (
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from account.decorators import admin_required, super_required
|
from account.decorators import admin_required, super_required
|
||||||
from prompt.models import Conversation, Message
|
from prompt.models import Conversation, Message
|
||||||
from .classifier import classify_conversation_messages
|
from prompt.utils import get_active_conversation, get_preceding_user_message
|
||||||
|
from .classifier import classify_message
|
||||||
|
|
||||||
|
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
@@ -58,6 +60,7 @@ from task.models import Task
|
|||||||
from account.models import RoleChoices, User
|
from account.models import RoleChoices, User
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _validate_item_ordering(value: str):
|
def _validate_item_ordering(value: str):
|
||||||
@@ -131,20 +134,19 @@ def create_submission(request, payload: SubmissionIn):
|
|||||||
task = get_object_or_404(Task, id=payload.task_id)
|
task = get_object_or_404(Task, id=payload.task_id)
|
||||||
|
|
||||||
manual_asst_msg = None
|
manual_asst_msg = None
|
||||||
|
linked_msg = None
|
||||||
|
new_user_msg_id = None
|
||||||
|
|
||||||
if payload.prompt:
|
if payload.prompt:
|
||||||
conversation = (
|
conversation = get_active_conversation(request.user, task.id)
|
||||||
Conversation.objects.filter(user=request.user, task=task)
|
|
||||||
.annotate(msg_count=Count("messages"))
|
|
||||||
.order_by("-msg_count", "-created")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not conversation:
|
if not conversation:
|
||||||
conversation = Conversation.objects.create(
|
conversation = Conversation.objects.create(
|
||||||
user=request.user, task=task, is_active=False
|
user=request.user, task=task, is_active=False
|
||||||
)
|
)
|
||||||
Message.objects.create(
|
user_msg = Message.objects.create(
|
||||||
conversation=conversation, role="user", content=payload.prompt, source="manual"
|
conversation=conversation, role="user", content=payload.prompt, source="manual"
|
||||||
)
|
)
|
||||||
|
new_user_msg_id = user_msg.id
|
||||||
manual_asst_msg = Message.objects.create(
|
manual_asst_msg = Message.objects.create(
|
||||||
conversation=conversation,
|
conversation=conversation,
|
||||||
role="assistant",
|
role="assistant",
|
||||||
@@ -154,48 +156,60 @@ def create_submission(request, payload: SubmissionIn):
|
|||||||
code_js=payload.js,
|
code_js=payload.js,
|
||||||
source="manual",
|
source="manual",
|
||||||
)
|
)
|
||||||
|
elif payload.message_id:
|
||||||
|
linked_msg = Message.objects.filter(
|
||||||
|
id=payload.message_id,
|
||||||
|
role="assistant",
|
||||||
|
conversation__user=request.user,
|
||||||
|
conversation__task=task,
|
||||||
|
).first()
|
||||||
|
if linked_msg is None:
|
||||||
|
logger.warning(
|
||||||
|
"create_submission: message_id %s not found for user %s task %s",
|
||||||
|
payload.message_id, request.user.id, task.id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
user_msg = get_preceding_user_message(linked_msg)
|
||||||
|
if user_msg:
|
||||||
|
new_user_msg_id = user_msg.id
|
||||||
|
|
||||||
|
# Idempotent: re-submitting the same AI message updates its existing submission
|
||||||
|
# instead of creating an orphaned duplicate.
|
||||||
|
if linked_msg and linked_msg.submission_id:
|
||||||
|
submission = linked_msg.submission
|
||||||
|
submission.html = payload.html
|
||||||
|
submission.css = payload.css
|
||||||
|
submission.js = payload.js
|
||||||
|
submission.save(update_fields=["html", "css", "js"])
|
||||||
else:
|
else:
|
||||||
conversation = (
|
submission = Submission.objects.create(
|
||||||
Conversation.objects.filter(user=request.user, task=task)
|
user=request.user,
|
||||||
.annotate(msg_count=Count("messages"))
|
task=task,
|
||||||
.order_by("-msg_count", "-created")
|
html=payload.html,
|
||||||
.first()
|
css=payload.css,
|
||||||
|
js=payload.js,
|
||||||
)
|
)
|
||||||
|
|
||||||
if conversation:
|
# Link assistant message to submission
|
||||||
threading.Thread(target=classify_conversation_messages, args=(conversation.id,), daemon=True).start()
|
if manual_asst_msg:
|
||||||
|
manual_asst_msg.submission = submission
|
||||||
|
manual_asst_msg.save(update_fields=["submission"])
|
||||||
|
elif linked_msg:
|
||||||
|
linked_msg.submission = submission
|
||||||
|
linked_msg.save(update_fields=["submission"])
|
||||||
|
|
||||||
submission = Submission.objects.create(
|
# Mark any showcased older submissions from same user+task as stale
|
||||||
user=request.user,
|
SubmissionAward.objects.filter(
|
||||||
task=task,
|
submission__user=request.user,
|
||||||
html=payload.html,
|
submission__task=task,
|
||||||
css=payload.css,
|
is_stale=False,
|
||||||
js=payload.js,
|
).exclude(submission=submission).update(is_stale=True)
|
||||||
)
|
|
||||||
|
|
||||||
# Link assistant message to submission
|
# Classify only the newly-added prompt message, and only if not already classified
|
||||||
if manual_asst_msg:
|
if new_user_msg_id is not None and not Message.objects.filter(
|
||||||
manual_asst_msg.submission = submission
|
id=new_user_msg_id, prompt_level__isnull=False
|
||||||
manual_asst_msg.save(update_fields=["submission"])
|
).exists():
|
||||||
elif payload.message_id:
|
threading.Thread(target=classify_message, args=(new_user_msg_id,), daemon=True).start()
|
||||||
try:
|
|
||||||
msg = Message.objects.get(
|
|
||||||
id=payload.message_id,
|
|
||||||
role="assistant",
|
|
||||||
conversation__user=request.user,
|
|
||||||
conversation__task=task,
|
|
||||||
)
|
|
||||||
msg.submission = submission
|
|
||||||
msg.save(update_fields=["submission"])
|
|
||||||
except Message.DoesNotExist:
|
|
||||||
pass # invalid message_id — submission already created, silently skip
|
|
||||||
|
|
||||||
# Mark any showcased older submissions from same user+task as stale
|
|
||||||
SubmissionAward.objects.filter(
|
|
||||||
submission__user=request.user,
|
|
||||||
submission__task=task,
|
|
||||||
is_stale=False,
|
|
||||||
).exclude(submission=submission).update(is_stale=True)
|
|
||||||
|
|
||||||
return {"id": str(submission.id)}
|
return {"id": str(submission.id)}
|
||||||
|
|
||||||
@@ -341,19 +355,15 @@ def delete_submission(request, submission_id: UUID):
|
|||||||
if submission.user != request.user and request.user.role != RoleChoices.SUPER:
|
if submission.user != request.user and request.user.role != RoleChoices.SUPER:
|
||||||
raise HttpError(403, "只能删除自己的提交")
|
raise HttpError(403, "只能删除自己的提交")
|
||||||
|
|
||||||
|
if request.user.role != RoleChoices.SUPER:
|
||||||
|
has_ratings = Rating.objects.filter(submission=submission).exists()
|
||||||
|
has_awards = SubmissionAward.objects.filter(submission=submission).exists()
|
||||||
|
if has_ratings or has_awards:
|
||||||
|
raise HttpError(400, "该提交已被评分或获奖,无法删除")
|
||||||
|
|
||||||
# 找到关联的助手消息,再找前一条用户消息
|
# 找到关联的助手消息,再找前一条用户消息
|
||||||
asst_msg = Message.objects.filter(submission=submission).first()
|
asst_msg = Message.objects.filter(submission=submission).first()
|
||||||
user_msg = None
|
user_msg = get_preceding_user_message(asst_msg) if asst_msg else None
|
||||||
if asst_msg:
|
|
||||||
user_msg = (
|
|
||||||
Message.objects.filter(
|
|
||||||
conversation=asst_msg.conversation,
|
|
||||||
created__lt=asst_msg.created,
|
|
||||||
role="user",
|
|
||||||
)
|
|
||||||
.order_by("-created")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
submission.delete() # CASCADE 自动删除关联的 asst_msg
|
submission.delete() # CASCADE 自动删除关联的 asst_msg
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user