## Summary - Closes [#67](#67) — new `PromptFeedback` model (unique on `(prompt, user)`) with `rating` (`up`|`down`), optional `reason` / `comment`, and timestamps via `TimeInfoBase`. - `POST /api/prompt_feedback` upserts `{ prompt_id, rating, reason?, comment? }`; `DELETE /api/prompt_feedback?prompt_id=` clears the caller's vote. - `GET conversation_details` now nests the caller's `feedback: { rating, reason, comment }` (or `null`) on each prompt so [chat_web_app#101](ai_ml_operations/chat_web_app#101) can rehydrate thumbs UI. - Auth required; users can only rate assistant prompts in their own non-deleted conversations. Distinct from app-wide `POST /feedbacks/`. - Joinable to `PromptMetric` via `prompt_id` for per-model accuracy slices. ## Test plan - [ ] `uv run python manage.py test chat_backend.tests.test_views_prompt_feedback` - [ ] Upsert thumbs up, then down with reason/comment — one row updated - [ ] DELETE clears vote; second DELETE → 404 - [ ] Rating another user's prompt → 404; rating a user message → 400 - [ ] Reload conversation details → assistant prompts show caller feedback only - [ ] Companion FE [chat_web_app#101](ai_ml_operations/chat_web_app#101) thumbs + reason popover against this APIReviewed-on: #70
This commit was merged in pull request #70.
This commit is contained in:
1 parent
bf3fffa343
commit
2aeb95136a
7 files changed
+479
-5
No files matched your search
@@ -8,6 +8,7 @@ from .models import (
|
||||
Company,
|
||||
Conversation,
|
||||
Prompt,
|
||||
PromptFeedback,
|
||||
PromptMetric,
|
||||
Feedback,
|
||||
FEEDBACK_CATEGORIES,
|
||||
@@ -210,9 +211,52 @@ class ConversationSerializer(serializers.ModelSerializer):
|
||||
return tout
|
||||
|
||||
|
||||
class PromptFeedbackSerializer(serializers.ModelSerializer):
|
||||
prompt_id = serializers.IntegerField(source="prompt.id", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PromptFeedback
|
||||
fields = (
|
||||
"id",
|
||||
"prompt_id",
|
||||
"rating",
|
||||
"reason",
|
||||
"comment",
|
||||
"created",
|
||||
"last_modified",
|
||||
)
|
||||
read_only_fields = ("id", "prompt_id", "created", "last_modified")
|
||||
|
||||
|
||||
class PromptFeedbackUpsertSerializer(serializers.Serializer):
|
||||
prompt_id = serializers.IntegerField()
|
||||
rating = serializers.ChoiceField(choices=PromptFeedback.Rating.choices)
|
||||
reason = serializers.ChoiceField(
|
||||
choices=PromptFeedback.Reason.choices,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
)
|
||||
comment = serializers.CharField(
|
||||
required=False, allow_null=True, allow_blank=True, max_length=1024
|
||||
)
|
||||
|
||||
def validate_reason(self, value):
|
||||
if value == "":
|
||||
return None
|
||||
return value
|
||||
|
||||
def validate_comment(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
stripped = str(value).strip()
|
||||
return stripped or None
|
||||
|
||||
|
||||
class PromptSerializer(serializers.ModelSerializer):
|
||||
tokens_in = serializers.SerializerMethodField()
|
||||
tokens_out = serializers.SerializerMethodField()
|
||||
feedback = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Prompt
|
||||
@@ -224,8 +268,9 @@ class PromptSerializer(serializers.ModelSerializer):
|
||||
"tokens_in",
|
||||
"tokens_out",
|
||||
"citations",
|
||||
"feedback",
|
||||
)
|
||||
read_only_fields = ("citations",)
|
||||
read_only_fields = ("citations", "feedback")
|
||||
|
||||
def _token_pair(self, obj):
|
||||
cache = self.context.setdefault("_prompt_token_cache", {})
|
||||
@@ -241,6 +286,34 @@ class PromptSerializer(serializers.ModelSerializer):
|
||||
_, tout = self._token_pair(obj)
|
||||
return tout
|
||||
|
||||
def get_feedback(self, obj):
|
||||
"""Current caller's rating for this prompt, if any."""
|
||||
request = self.context.get("request")
|
||||
if request is None or not getattr(request, "user", None):
|
||||
return None
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return None
|
||||
|
||||
by_prompt = self.context.get("_prompt_feedback_by_id")
|
||||
if by_prompt is None:
|
||||
prompt_ids = self.context.get("_prompt_ids_for_feedback")
|
||||
qs = PromptFeedback.objects.filter(user=user).only(
|
||||
"prompt_id", "rating", "reason", "comment"
|
||||
)
|
||||
if prompt_ids is not None:
|
||||
qs = qs.filter(prompt_id__in=prompt_ids)
|
||||
by_prompt = {
|
||||
row.prompt_id: {
|
||||
"rating": row.rating,
|
||||
"reason": row.reason,
|
||||
"comment": row.comment,
|
||||
}
|
||||
for row in qs
|
||||
}
|
||||
self.context["_prompt_feedback_by_id"] = by_prompt
|
||||
return by_prompt.get(obj.id)
|
||||
|
||||
def validate_message(self, value: str) -> str:
|
||||
if value is None or not str(value).strip():
|
||||
raise serializers.ValidationError("Message text cannot be empty.")
|
||||
|
||||
Reference in new issue
Block a user