add message history
This commit is contained in:
@@ -8,7 +8,7 @@ from django.contrib.auth.decorators import login_required
|
|||||||
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from .models import Conversation, Message
|
from .models import Conversation, Message
|
||||||
from .schemas import ConversationOut, MessageOut
|
from .schemas import ConversationOut, MessageOut, PromptHistoryItemOut
|
||||||
from account.models import RoleChoices
|
from account.models import RoleChoices
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -55,6 +55,58 @@ def list_messages(request, conversation_id: UUID):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/history/{task_id}", response=List[PromptHistoryItemOut])
|
||||||
|
@login_required
|
||||||
|
def list_prompt_history(request, task_id: int):
|
||||||
|
"""
|
||||||
|
获取当前用户在某任务下的历史对话轮次。
|
||||||
|
|
||||||
|
只返回用户提示词和后一条 assistant 消息中的页面代码,用于前端渲染缩略图。
|
||||||
|
"""
|
||||||
|
conversations = Conversation.objects.filter(
|
||||||
|
user=request.user,
|
||||||
|
task_id=task_id,
|
||||||
|
).prefetch_related("messages")
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for conv in conversations:
|
||||||
|
messages = list(conv.messages.all().order_by("created", "id"))
|
||||||
|
for idx, user_msg in enumerate(messages):
|
||||||
|
if user_msg.role != "user":
|
||||||
|
continue
|
||||||
|
|
||||||
|
assistant_msg = None
|
||||||
|
for reply in messages[idx + 1:]:
|
||||||
|
if reply.role == "user":
|
||||||
|
break
|
||||||
|
if reply.role == "assistant":
|
||||||
|
assistant_msg = reply
|
||||||
|
break
|
||||||
|
|
||||||
|
if not assistant_msg:
|
||||||
|
continue
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
(
|
||||||
|
user_msg.created,
|
||||||
|
{
|
||||||
|
"user_message_id": user_msg.id,
|
||||||
|
"assistant_message_id": assistant_msg.id,
|
||||||
|
"submission_id": assistant_msg.submission_id,
|
||||||
|
"source": user_msg.source,
|
||||||
|
"prompt": user_msg.content,
|
||||||
|
"prompt_level": user_msg.prompt_level,
|
||||||
|
"code_html": assistant_msg.code_html,
|
||||||
|
"code_css": assistant_msg.code_css,
|
||||||
|
"code_js": assistant_msg.code_js,
|
||||||
|
"created": user_msg.created.isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return [item for _, item in sorted(items, key=lambda row: row[0], reverse=True)]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/conversations/{conversation_id}/classify")
|
@router.post("/conversations/{conversation_id}/classify")
|
||||||
@login_required
|
@login_required
|
||||||
def classify_conversation(request, conversation_id: UUID, force: bool = False):
|
def classify_conversation(request, conversation_id: UUID, force: bool = False):
|
||||||
|
|||||||
@@ -15,6 +15,19 @@ class MessageOut(Schema):
|
|||||||
created: str
|
created: str
|
||||||
|
|
||||||
|
|
||||||
|
class PromptHistoryItemOut(Schema):
|
||||||
|
user_message_id: int
|
||||||
|
assistant_message_id: int
|
||||||
|
submission_id: Optional[UUID] = None
|
||||||
|
source: str
|
||||||
|
prompt: str
|
||||||
|
prompt_level: Optional[int] = None
|
||||||
|
code_html: Optional[str] = None
|
||||||
|
code_css: Optional[str] = None
|
||||||
|
code_js: Optional[str] = None
|
||||||
|
created: str
|
||||||
|
|
||||||
|
|
||||||
class ConversationOut(Schema):
|
class ConversationOut(Schema):
|
||||||
id: UUID
|
id: UUID
|
||||||
user_id: int
|
user_id: int
|
||||||
|
|||||||
@@ -175,3 +175,96 @@ class DeleteMessagePairTest(TestCase):
|
|||||||
self.client.force_login(other)
|
self.client.force_login(other)
|
||||||
resp = self.client.delete(f"/api/prompt/messages/{self.asst_msg.id}/pair")
|
resp = self.client.delete(f"/api/prompt/messages/{self.asst_msg.id}/pair")
|
||||||
self.assertEqual(resp.status_code, 403)
|
self.assertEqual(resp.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
class PromptHistoryTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = _make_user("history-user")
|
||||||
|
self.other = _make_user("history-other")
|
||||||
|
self.task = _make_task()
|
||||||
|
self.other_task = Task.objects.create(
|
||||||
|
title="Other Task", task_type="challenge", display=2, content=""
|
||||||
|
)
|
||||||
|
|
||||||
|
def _pair(
|
||||||
|
self,
|
||||||
|
user,
|
||||||
|
task,
|
||||||
|
prompt,
|
||||||
|
source="conversation",
|
||||||
|
html="<main>page</main>",
|
||||||
|
css="main { color: red; }",
|
||||||
|
js="",
|
||||||
|
):
|
||||||
|
conv = Conversation.objects.create(user=user, task=task)
|
||||||
|
user_msg = Message.objects.create(
|
||||||
|
conversation=conv,
|
||||||
|
role="user",
|
||||||
|
source=source,
|
||||||
|
content=prompt,
|
||||||
|
)
|
||||||
|
asst_msg = Message.objects.create(
|
||||||
|
conversation=conv,
|
||||||
|
role="assistant",
|
||||||
|
source=source,
|
||||||
|
content="" if source == "manual" else "answer",
|
||||||
|
code_html=html,
|
||||||
|
code_css=css,
|
||||||
|
code_js=js,
|
||||||
|
)
|
||||||
|
return user_msg, asst_msg
|
||||||
|
|
||||||
|
def test_history_returns_ai_and_manual_prompt_rounds_with_page_code(self):
|
||||||
|
ai_user, ai_asst = self._pair(self.user, self.task, "做一个登录页")
|
||||||
|
manual_user, manual_asst = self._pair(
|
||||||
|
self.user,
|
||||||
|
self.task,
|
||||||
|
"我让外部 AI 做一个卡片",
|
||||||
|
source="manual",
|
||||||
|
html="<section>card</section>",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/prompt/history/{self.task.id}")
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
ids = {item["user_message_id"] for item in data}
|
||||||
|
self.assertEqual(ids, {ai_user.id, manual_user.id})
|
||||||
|
by_source = {item["source"]: item for item in data}
|
||||||
|
self.assertEqual(by_source["conversation"]["assistant_message_id"], ai_asst.id)
|
||||||
|
self.assertEqual(by_source["conversation"]["prompt"], "做一个登录页")
|
||||||
|
self.assertEqual(by_source["manual"]["assistant_message_id"], manual_asst.id)
|
||||||
|
self.assertEqual(by_source["manual"]["code_html"], "<section>card</section>")
|
||||||
|
self.assertNotIn("content", by_source["conversation"])
|
||||||
|
|
||||||
|
def test_history_is_scoped_to_current_user_and_task(self):
|
||||||
|
own_user, _ = self._pair(self.user, self.task, "自己的提示词")
|
||||||
|
self._pair(self.other, self.task, "别人的提示词")
|
||||||
|
self._pair(self.user, self.other_task, "其他任务提示词")
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/prompt/history/{self.task.id}")
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0]["user_message_id"], own_user.id)
|
||||||
|
self.assertEqual(data[0]["prompt"], "自己的提示词")
|
||||||
|
|
||||||
|
def test_history_keeps_rounds_without_page_code(self):
|
||||||
|
conv = Conversation.objects.create(user=self.user, task=self.task)
|
||||||
|
user_msg = Message.objects.create(conversation=conv, role="user", content="只聊天")
|
||||||
|
asst_msg = Message.objects.create(conversation=conv, role="assistant", content="没有代码")
|
||||||
|
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
resp = self.client.get(f"/api/prompt/history/{self.task.id}")
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
data = resp.json()
|
||||||
|
self.assertEqual(len(data), 1)
|
||||||
|
self.assertEqual(data[0]["user_message_id"], user_msg.id)
|
||||||
|
self.assertEqual(data[0]["assistant_message_id"], asst_msg.id)
|
||||||
|
self.assertIsNone(data[0]["code_html"])
|
||||||
|
self.assertIsNone(data[0]["code_css"])
|
||||||
|
self.assertIsNone(data[0]["code_js"])
|
||||||
|
|||||||
Reference in New Issue
Block a user