Files
chat_backend/llm_be/chat_backend/serializers.py
T
westfarnandCursor c21f052d5b
CI / test (pull_request) Successful in 9s
Unit Tests / test (pull_request) Successful in 10s
Read attached chat images with a vision model.
Image uploads were treated as unknown documents and never shown to the model. PNG, JPEG, WebP, and GIF now go to Ollama vision, and history returns the image bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-20 08:11:42 -05:00

405 lines
12 KiB
Python

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework import serializers
from django.db.models import Count, Q, Sum
from .models import (
CustomUser,
Announcement,
Company,
Conversation,
Prompt,
PromptFeedback,
PromptMetric,
Feedback,
FEEDBACK_CATEGORIES,
DocumentWorkspace,
Document,
DriveConnection,
)
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super(MyTokenObtainPairSerializer, cls).get_token(user)
# add custom claim
token["company"] = "something here"
return token
def validate(self, attrs):
data = super().validate(attrs)
if getattr(self.user, "deleted", False):
from rest_framework_simplejwt.exceptions import AuthenticationFailed
raise AuthenticationFailed(
"No active account found with the given credentials",
code="no_active_account",
)
return data
class CompanySerializer(serializers.ModelSerializer):
class Meta:
model = Company
fields = "__all__"
class AnnouncmentSerializer(serializers.ModelSerializer):
class Meta:
model = Announcement
fields = "__all__"
class FeedbackSerializer(serializers.ModelSerializer):
class Meta:
model = Feedback
fields = "__all__"
class CustomUserSerializer(serializers.ModelSerializer):
email = serializers.EmailField(required=True)
username = serializers.CharField()
password = serializers.CharField(min_length=8, write_only=True)
company = CompanySerializer()
has_usable_password = serializers.BooleanField()
subscription = serializers.SerializerMethodField()
class Meta:
model = CustomUser
fields = "__all__"
extra_kwargs = {"password": {"write_only": True}}
def get_subscription(self, obj):
from monetization.services.plans import needs_checkout, plan_to_dict
from monetization.models import UserSubscription
try:
sub = obj.subscription
except UserSubscription.DoesNotExist:
return {
"plan": None,
"status": UserSubscription.Status.NONE,
"source": UserSubscription.Source.NONE,
"needs_checkout": True,
"cancel_at_period_end": False,
"current_period_end": None,
}
return {
"plan": plan_to_dict(sub.plan) if sub.plan_id else None,
"status": sub.status,
"source": sub.source,
"needs_checkout": needs_checkout(obj),
"cancel_at_period_end": bool(sub.cancel_at_period_end),
"current_period_end": (
sub.current_period_end.isoformat() if sub.current_period_end else None
),
}
class SelfServeRegistrationSerializer(serializers.Serializer):
"""Minimal payload for public self-serve sign-up (gated by settings)."""
email = serializers.EmailField(required=True)
password = serializers.CharField(min_length=8, write_only=True)
first_name = serializers.CharField(
required=False, allow_blank=True, max_length=150, default=""
)
last_name = serializers.CharField(
required=False, allow_blank=True, max_length=150, default=""
)
company_name = serializers.CharField(
required=False, allow_blank=True, max_length=256, default=""
)
def validate_email(self, value: str) -> str:
email = value.strip().lower()
if CustomUser.objects.filter(email__iexact=email).exists():
raise serializers.ValidationError("A user with this email already exists.")
if CustomUser.objects.filter(username__iexact=email).exists():
raise serializers.ValidationError("A user with this email already exists.")
return email
def create(self, validated_data):
from monetization.services.plans import try_redeem_backer_email
email = validated_data["email"]
password = validated_data["password"]
first_name = (validated_data.get("first_name") or "").strip()
last_name = (validated_data.get("last_name") or "").strip()
company_name = (validated_data.get("company_name") or "").strip()
if not company_name:
company_name = f"{email}'s workspace"
company = Company.objects.create(
name=company_name,
state="NA",
zipcode="00000",
address="N/A",
)
user = CustomUser.objects.create_user(
username=email,
email=email,
password=password,
first_name=first_name,
last_name=last_name,
company=company,
is_company_manager=True,
)
try_redeem_backer_email(user)
return user
def _conversation_token_totals(conversation_id: int):
"""
Sum PromptMetric tokens for a conversation.
Returns nulls when the provider never reported usage (never fabricate 0).
"""
agg = PromptMetric.objects.filter(conversation_id=conversation_id).aggregate(
tin=Sum("tokens_in"),
tout=Sum("tokens_out"),
with_in=Count("id", filter=Q(tokens_in__isnull=False)),
with_out=Count("id", filter=Q(tokens_out__isnull=False)),
)
return (
agg["tin"] if agg["with_in"] else None,
agg["tout"] if agg["with_out"] else None,
)
def _prompt_token_pair(prompt_id: int):
metric = (
PromptMetric.objects.filter(prompt_id=prompt_id)
.order_by("-created")
.only("tokens_in", "tokens_out")
.first()
)
if metric is None:
return None, None
return metric.tokens_in, metric.tokens_out
class ConversationSerializer(serializers.ModelSerializer):
tokens_in = serializers.SerializerMethodField()
tokens_out = serializers.SerializerMethodField()
class Meta:
model = Conversation
fields = (
"title",
"created",
"last_modified",
"id",
"tokens_in",
"tokens_out",
)
def _token_pair(self, obj):
cache = self.context.setdefault("_conversation_token_cache", {})
if obj.id not in cache:
cache[obj.id] = _conversation_token_totals(obj.id)
return cache[obj.id]
def get_tokens_in(self, obj):
tin, _ = self._token_pair(obj)
return tin
def get_tokens_out(self, obj):
_, tout = self._token_pair(obj)
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()
image = serializers.SerializerMethodField()
class Meta:
model = Prompt
fields = (
"message",
"user_created",
"created",
"id",
"tokens_in",
"tokens_out",
"citations",
"feedback",
"file_type",
"image",
)
read_only_fields = ("citations", "feedback", "file_type", "image")
def _token_pair(self, obj):
cache = self.context.setdefault("_prompt_token_cache", {})
if obj.id not in cache:
cache[obj.id] = _prompt_token_pair(obj.id)
return cache[obj.id]
def get_tokens_in(self, obj):
tin, _ = self._token_pair(obj)
return tin
def get_tokens_out(self, obj):
_, 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 get_image(self, obj):
from chat_backend.services.vision import image_base64
return image_base64(obj)
def validate_message(self, value: str) -> str:
if value is None or not str(value).strip():
raise serializers.ValidationError("Message text cannot be empty.")
return str(value).strip()
class BasicUserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = (
"email",
"first_name",
"last_name",
"is_active",
"has_usable_password",
"is_company_manager",
"has_signed_tos",
)
# document serializers
class DocumentWorkspaceSerializer(serializers.ModelSerializer):
class Meta:
model = DocumentWorkspace
fields = ["id", "name", "created"]
read_only_fields = ["id", "created"]
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = [
"id",
"workspace",
"file",
"uploaded_at",
"processed",
"created",
"active",
]
read_only_fields = ["id", "uploaded_at", "processed", "created"]
# drive connection serializers (#47-#52)
class DriveConnectionSerializer(serializers.ModelSerializer):
"""Never exposes access_token/refresh_token to the client."""
class Meta:
model = DriveConnection
fields = [
"id",
"provider",
"kind",
"external_account_email",
"selected_resource_ids",
"selected_resource_labels",
"last_sync_at",
"last_sync_status",
"last_sync_error",
"sync_total",
"sync_processed",
"sync_added",
"sync_updated",
"sync_failed",
"is_active",
"created",
]
read_only_fields = fields
class DriveConnectionResourcesSerializer(serializers.Serializer):
resource_ids = serializers.ListField(
child=serializers.CharField(max_length=512), allow_empty=True
)
resource_labels = serializers.ListField(
child=serializers.CharField(max_length=512, allow_blank=True),
allow_empty=True,
required=False,
default=list,
)