diff --git a/apps/rlhf/__init__.py b/apps/rlhf/__init__.py new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/apps/rlhf/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/rlhf/admin.py b/apps/rlhf/admin.py new file mode 100644 index 0000000..9bbea70 --- /dev/null +++ b/apps/rlhf/admin.py @@ -0,0 +1,72 @@ +from django.contrib import admin +from .models import ( + Conversation, Message, Feedback, FeedbackTag, DetailedFeedback, + ConversationSubmission, ConversationEvaluation, SystemConfig +) + + +@admin.register(Conversation) +class ConversationAdmin(admin.ModelAdmin): + list_display = ('id', 'user', 'is_submitted', 'created_at') + list_filter = ('is_submitted', 'created_at') + search_fields = ('id', 'user__username') + date_hierarchy = 'created_at' + + +@admin.register(Message) +class MessageAdmin(admin.ModelAdmin): + list_display = ('id', 'conversation', 'role', 'short_content', 'timestamp') + list_filter = ('role', 'timestamp') + search_fields = ('id', 'conversation__id', 'content') + date_hierarchy = 'timestamp' + + def short_content(self, obj): + return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content + short_content.short_description = '内容' + + +@admin.register(Feedback) +class FeedbackAdmin(admin.ModelAdmin): + list_display = ('id', 'message', 'conversation', 'user', 'feedback_value', 'timestamp') + list_filter = ('feedback_value', 'timestamp') + search_fields = ('id', 'message__id', 'conversation__id', 'user__username') + date_hierarchy = 'timestamp' + + +@admin.register(FeedbackTag) +class FeedbackTagAdmin(admin.ModelAdmin): + list_display = ('id', 'tag_name', 'tag_type', 'description', 'created_at') + list_filter = ('tag_type', 'created_at') + search_fields = ('id', 'tag_name', 'description') + + +@admin.register(DetailedFeedback) +class DetailedFeedbackAdmin(admin.ModelAdmin): + list_display = ('id', 'message', 'conversation', 'user', 'feedback_type', 'is_inline', 'created_at') + list_filter = ('feedback_type', 'is_inline', 'created_at') + search_fields = ('id', 'message__id', 'conversation__id', 'user__username', 'custom_content') + date_hierarchy = 'created_at' + + +@admin.register(ConversationSubmission) +class ConversationSubmissionAdmin(admin.ModelAdmin): + list_display = ('id', 'conversation', 'user', 'title', 'status', 'quality_score', 'reviewer', 'submitted_at', 'reviewed_at') + list_filter = ('status', 'quality_score', 'submitted_at', 'reviewed_at') + search_fields = ('id', 'conversation__id', 'user__username', 'title', 'description', 'reviewer__username') + date_hierarchy = 'submitted_at' + + +@admin.register(ConversationEvaluation) +class ConversationEvaluationAdmin(admin.ModelAdmin): + list_display = ('id', 'conversation', 'user', 'has_logical_issues', 'needs_satisfied', 'created_at') + list_filter = ('has_logical_issues', 'needs_satisfied', 'created_at') + search_fields = ('id', 'conversation__id', 'user__username', 'overall_feeling') + date_hierarchy = 'created_at' + + +@admin.register(SystemConfig) +class SystemConfigAdmin(admin.ModelAdmin): + list_display = ('id', 'config_key', 'config_value', 'config_type', 'description', 'updated_at') + list_filter = ('config_type', 'updated_at') + search_fields = ('id', 'config_key', 'config_value', 'description') + date_hierarchy = 'updated_at' \ No newline at end of file diff --git a/apps/rlhf/apps.py b/apps/rlhf/apps.py new file mode 100644 index 0000000..6fd3057 --- /dev/null +++ b/apps/rlhf/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RlhfConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.rlhf' \ No newline at end of file diff --git a/apps/rlhf/management/__init__.py b/apps/rlhf/management/__init__.py new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/apps/rlhf/management/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/rlhf/management/commands/__init__.py b/apps/rlhf/management/commands/__init__.py new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/apps/rlhf/management/commands/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/rlhf/management/commands/init_feedback_tags.py b/apps/rlhf/management/commands/init_feedback_tags.py new file mode 100644 index 0000000..1b443a7 --- /dev/null +++ b/apps/rlhf/management/commands/init_feedback_tags.py @@ -0,0 +1,55 @@ +import uuid +from django.core.management.base import BaseCommand +from rlhf.models import FeedbackTag +from django.utils import timezone + + +class Command(BaseCommand): + help = '初始化反馈标签' + + def handle(self, *args, **options): + # 正面标签 + positive_tags = [ + ('有帮助', '回答对问题有实际帮助'), + ('准确', '信息准确可靠'), + ('清晰', '表达清楚易懂'), + ('完整', '回答全面完整'), + ('友好', '语调友善亲切'), + ('创新', '提供了新颖的观点') + ] + + # 负面标签 + negative_tags = [ + ('不准确', '包含错误信息'), + ('不相关', '回答偏离主题'), + ('不完整', '回答过于简略'), + ('不清晰', '表达模糊难懂'), + ('不友好', '语调生硬冷淡'), + ('重复', '内容重复冗余') + ] + + # 插入正面标签 + for tag_name, description in positive_tags: + FeedbackTag.objects.get_or_create( + tag_name=tag_name, + defaults={ + 'id': str(uuid.uuid4()), + 'tag_type': 'positive', + 'description': description, + 'created_at': timezone.now() + } + ) + + # 插入负面标签 + for tag_name, description in negative_tags: + FeedbackTag.objects.get_or_create( + tag_name=tag_name, + defaults={ + 'id': str(uuid.uuid4()), + 'tag_type': 'negative', + 'description': description, + 'created_at': timezone.now() + } + ) + + self.stdout.write(self.style.SUCCESS('✅ 反馈标签已添加')) \ No newline at end of file diff --git a/apps/rlhf/migrations/0001_initial.py b/apps/rlhf/migrations/0001_initial.py new file mode 100644 index 0000000..320c586 --- /dev/null +++ b/apps/rlhf/migrations/0001_initial.py @@ -0,0 +1,152 @@ +# Generated by Django 5.1.5 on 2025-06-09 08:28 + +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='FeedbackTag', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('tag_name', models.CharField(max_length=50, unique=True)), + ('tag_type', models.CharField(choices=[('positive', '正面'), ('negative', '负面')], max_length=20)), + ('description', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ], + options={ + 'verbose_name': '反馈标签', + 'verbose_name_plural': '反馈标签', + }, + ), + migrations.CreateModel( + name='SystemConfig', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('config_key', models.CharField(max_length=50, unique=True)), + ('config_value', models.TextField(blank=True, null=True)), + ('config_type', models.CharField(choices=[('string', '字符串'), ('integer', '整数'), ('float', '浮点数'), ('boolean', '布尔值'), ('json', 'JSON')], default='string', max_length=20)), + ('description', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), + ], + options={ + 'verbose_name': '系统配置', + 'verbose_name_plural': '系统配置', + }, + ), + migrations.CreateModel( + name='Conversation', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('is_submitted', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': '对话', + 'verbose_name_plural': '对话', + }, + ), + migrations.CreateModel( + name='ConversationSubmission', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('title', models.CharField(blank=True, max_length=255, null=True)), + ('description', models.TextField(blank=True, null=True)), + ('status', models.CharField(choices=[('submitted', '已提交'), ('reviewed', '已审核'), ('accepted', '已接受'), ('rejected', '已拒绝')], default='submitted', max_length=20)), + ('quality_score', models.IntegerField(blank=True, null=True)), + ('reviewer_notes', models.TextField(blank=True, null=True)), + ('submitted_at', models.DateTimeField(default=django.utils.timezone.now)), + ('reviewed_at', models.DateTimeField(blank=True, null=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='rlhf.conversation')), + ('reviewer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_submissions', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': '对话提交', + 'verbose_name_plural': '对话提交', + }, + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('role', models.CharField(choices=[('user', '用户'), ('assistant', '助手'), ('system', '系统')], max_length=20)), + ('content', models.TextField()), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='rlhf.conversation')), + ], + options={ + 'verbose_name': '消息', + 'verbose_name_plural': '消息', + 'ordering': ['timestamp'], + }, + ), + migrations.CreateModel( + name='Feedback', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('feedback_value', models.IntegerField()), + ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='rlhf.conversation')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to=settings.AUTH_USER_MODEL)), + ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedback', to='rlhf.message')), + ], + options={ + 'verbose_name': '反馈', + 'verbose_name_plural': '反馈', + }, + ), + migrations.CreateModel( + name='DetailedFeedback', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('feedback_type', models.CharField(choices=[('positive', '正面'), ('negative', '负面'), ('neutral', '中性')], max_length=20)), + ('feedback_tags', models.TextField(blank=True, null=True)), + ('custom_tags', models.TextField(blank=True, null=True)), + ('custom_content', models.TextField(blank=True, null=True)), + ('is_inline', models.BooleanField(default=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='detailed_feedback', to='rlhf.conversation')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='detailed_feedback', to=settings.AUTH_USER_MODEL)), + ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='detailed_feedback', to='rlhf.message')), + ], + options={ + 'verbose_name': '详细反馈', + 'verbose_name_plural': '详细反馈', + }, + ), + migrations.CreateModel( + name='ConversationEvaluation', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('overall_feeling', models.TextField(blank=True, null=True)), + ('has_logical_issues', models.CharField(choices=[('yes', '是'), ('no', '否'), ('unsure', '不确定')], max_length=10)), + ('needs_satisfied', models.CharField(choices=[('yes', '是'), ('no', '否'), ('partially', '部分')], max_length=10)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), + ('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evaluations', to='rlhf.conversation')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='evaluations', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': '对话评估', + 'verbose_name_plural': '对话评估', + 'unique_together': {('conversation', 'user')}, + }, + ), + ] diff --git a/apps/rlhf/migrations/__init__.py b/apps/rlhf/migrations/__init__.py new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/apps/rlhf/migrations/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/rlhf/models.py b/apps/rlhf/models.py new file mode 100644 index 0000000..fc756c8 --- /dev/null +++ b/apps/rlhf/models.py @@ -0,0 +1,195 @@ +from django.db import models +import uuid +from django.utils import timezone +from apps.user.models import User + + +class Conversation(models.Model): + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='conversations') + is_submitted = models.BooleanField(default=False) + created_at = models.DateTimeField(default=timezone.now) + + class Meta: + verbose_name = '对话' + verbose_name_plural = '对话' + + def __str__(self): + return f"Conversation {self.id[:8]}" + + +class Message(models.Model): + ROLE_CHOICES = ( + ('user', '用户'), + ('assistant', '助手'), + ('system', '系统'), + ) + + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages') + role = models.CharField(max_length=20, choices=ROLE_CHOICES) + content = models.TextField() + timestamp = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '消息' + verbose_name_plural = '消息' + ordering = ['timestamp'] + + def __str__(self): + return f"{self.role}: {self.content[:50]}..." + + +class Feedback(models.Model): + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='feedback') + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='feedback') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='feedback') + feedback_value = models.IntegerField() + timestamp = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '反馈' + verbose_name_plural = '反馈' + + def __str__(self): + return f"Feedback on {self.message.id[:8]}" + + +class FeedbackTag(models.Model): + TAG_TYPE_CHOICES = ( + ('positive', '正面'), + ('negative', '负面'), + ) + + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + tag_name = models.CharField(max_length=50, unique=True) + tag_type = models.CharField(max_length=20, choices=TAG_TYPE_CHOICES) + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '反馈标签' + verbose_name_plural = '反馈标签' + + def __str__(self): + return f"{self.tag_name} ({self.tag_type})" + + +class DetailedFeedback(models.Model): + FEEDBACK_TYPE_CHOICES = ( + ('positive', '正面'), + ('negative', '负面'), + ('neutral', '中性'), + ) + + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name='detailed_feedback') + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='detailed_feedback') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='detailed_feedback') + feedback_type = models.CharField(max_length=20, choices=FEEDBACK_TYPE_CHOICES) + feedback_tags = models.TextField(blank=True, null=True) # JSON格式存储多个标签 + custom_tags = models.TextField(blank=True, null=True) + custom_content = models.TextField(blank=True, null=True) + is_inline = models.BooleanField(default=True) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '详细反馈' + verbose_name_plural = '详细反馈' + + def __str__(self): + return f"{self.feedback_type} feedback on {self.message.id[:8]}" + + +class ConversationSubmission(models.Model): + STATUS_CHOICES = ( + ('submitted', '已提交'), + ('reviewed', '已审核'), + ('accepted', '已接受'), + ('rejected', '已拒绝'), + ) + + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='submissions') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='submissions') + title = models.CharField(max_length=255, blank=True, null=True) + description = models.TextField(blank=True, null=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted') + quality_score = models.IntegerField(null=True, blank=True) + reviewer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='reviewed_submissions') + reviewer_notes = models.TextField(blank=True, null=True) + submitted_at = models.DateTimeField(default=timezone.now) + reviewed_at = models.DateTimeField(null=True, blank=True) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '对话提交' + verbose_name_plural = '对话提交' + + def __str__(self): + return f"Submission for {self.conversation.id[:8]}" + + +class ConversationEvaluation(models.Model): + LOGICAL_CHOICES = ( + ('yes', '是'), + ('no', '否'), + ('unsure', '不确定'), + ) + + NEEDS_CHOICES = ( + ('yes', '是'), + ('no', '否'), + ('partially', '部分'), + ) + + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='evaluations') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='evaluations') + overall_feeling = models.TextField(blank=True, null=True) + has_logical_issues = models.CharField(max_length=10, choices=LOGICAL_CHOICES) + needs_satisfied = models.CharField(max_length=10, choices=NEEDS_CHOICES) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '对话评估' + verbose_name_plural = '对话评估' + unique_together = ('conversation', 'user') + + def __str__(self): + return f"Evaluation for {self.conversation.id[:8]}" + + +class SystemConfig(models.Model): + CONFIG_TYPE_CHOICES = ( + ('string', '字符串'), + ('integer', '整数'), + ('float', '浮点数'), + ('boolean', '布尔值'), + ('json', 'JSON'), + ) + + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + config_key = models.CharField(max_length=50, unique=True) + config_value = models.TextField(blank=True, null=True) + config_type = models.CharField(max_length=20, choices=CONFIG_TYPE_CHOICES, default='string') + description = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(default=timezone.now) + + class Meta: + verbose_name = '系统配置' + verbose_name_plural = '系统配置' + + def __str__(self): + return self.config_key \ No newline at end of file diff --git a/apps/rlhf/serializers.py b/apps/rlhf/serializers.py new file mode 100644 index 0000000..7ed471a --- /dev/null +++ b/apps/rlhf/serializers.py @@ -0,0 +1,84 @@ +from rest_framework import serializers +from .models import ( + Conversation, Message, Feedback, FeedbackTag, DetailedFeedback, + ConversationSubmission, ConversationEvaluation, SystemConfig +) +from apps.user.serializers import UserSerializer + + +class ConversationSerializer(serializers.ModelSerializer): + class Meta: + model = Conversation + fields = ['id', 'user', 'is_submitted', 'created_at'] + read_only_fields = ['id', 'created_at', 'user'] + + +class MessageSerializer(serializers.ModelSerializer): + class Meta: + model = Message + fields = ['id', 'conversation', 'role', 'content', 'timestamp'] + read_only_fields = ['id', 'timestamp'] + + +class FeedbackSerializer(serializers.ModelSerializer): + class Meta: + model = Feedback + fields = ['id', 'message', 'conversation', 'user', 'feedback_value', 'timestamp'] + read_only_fields = ['id', 'timestamp'] + + +class FeedbackTagSerializer(serializers.ModelSerializer): + class Meta: + model = FeedbackTag + fields = ['id', 'tag_name', 'tag_type', 'description', 'created_at'] + read_only_fields = ['id', 'created_at'] + + +class DetailedFeedbackSerializer(serializers.ModelSerializer): + class Meta: + model = DetailedFeedback + fields = ['id', 'message', 'conversation', 'user', 'feedback_type', 'feedback_tags', 'custom_tags', 'custom_content', 'is_inline', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at'] + + +class ConversationSubmissionSerializer(serializers.ModelSerializer): + user_details = UserSerializer(source='user', read_only=True) + reviewer_details = UserSerializer(source='reviewer', read_only=True) + + class Meta: + model = ConversationSubmission + fields = ['id', 'conversation', 'user', 'user_details', 'title', 'description', 'status', 'quality_score', 'reviewer', 'reviewer_details', 'reviewer_notes', 'submitted_at', 'reviewed_at', 'created_at', 'updated_at'] + read_only_fields = ['id', 'submitted_at', 'reviewed_at', 'created_at', 'updated_at'] + + +class ConversationEvaluationSerializer(serializers.ModelSerializer): + class Meta: + model = ConversationEvaluation + fields = ['id', 'conversation', 'user', 'overall_feeling', 'has_logical_issues', 'needs_satisfied', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at'] + + +class SystemConfigSerializer(serializers.ModelSerializer): + class Meta: + model = SystemConfig + fields = ['id', 'config_key', 'config_value', 'config_type', 'description', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at'] + + +class ConversationWithMessagesSerializer(serializers.ModelSerializer): + messages = MessageSerializer(many=True, read_only=True) + + class Meta: + model = Conversation + fields = ['id', 'user', 'is_submitted', 'created_at', 'messages'] + read_only_fields = ['id', 'created_at'] + + +class MessageWithFeedbackSerializer(serializers.ModelSerializer): + feedback = FeedbackSerializer(many=True, read_only=True) + detailed_feedback = DetailedFeedbackSerializer(many=True, read_only=True) + + class Meta: + model = Message + fields = ['id', 'conversation', 'role', 'content', 'timestamp', 'feedback', 'detailed_feedback'] + read_only_fields = ['id', 'timestamp'] \ No newline at end of file diff --git a/apps/rlhf/siliconflow_client.py b/apps/rlhf/siliconflow_client.py new file mode 100644 index 0000000..3174cbc --- /dev/null +++ b/apps/rlhf/siliconflow_client.py @@ -0,0 +1,307 @@ +import requests +import json +import time +import logging + +logger = logging.getLogger(__name__) + +class SiliconFlowClient: + def __init__(self, api_key="sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf", model="Qwen/QwQ-32B"): + """ + 初始化SiliconFlow客户端 + """ + self.api_key = api_key + self.model = model + self.base_url = "https://api.siliconflow.cn/v1" + self.messages = [] + self.system_message = None + + logger.info(f"初始化SiliconFlow客户端 - 模型: {model}") + + def set_model(self, model): + """设置使用的模型""" + self.model = model + logger.info(f"SiliconFlow切换模型: {model}") + + def set_system_message(self, message): + """设置系统消息""" + self.system_message = message + self.messages = [] + if message: + self.messages.append({"role": "system", "content": message}) + logger.debug(f"SiliconFlow设置系统消息 - 长度: {len(message) if message else 0}") + + def add_message(self, role, content): + """添加消息到对话历史""" + self.messages.append({"role": role, "content": content}) + + def clear_history(self): + """清空对话历史""" + self.messages = [] + if self.system_message: + self.messages.append({"role": "system", "content": self.system_message}) + logger.debug("SiliconFlow清空对话历史") + + def chat(self, message): + """ + 非流式对话 + """ + try: + # 添加用户消息 + self.add_message("user", message) + + payload = { + "model": self.model, + "messages": self.messages, + "stream": False, + "max_tokens": 2048, + "temperature": 0.7, + "top_p": 0.7, + } + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + response = requests.post( + f"{self.base_url}/chat/completions", + json=payload, + headers=headers, + timeout=60 + ) + + if response.status_code == 200: + data = response.json() + assistant_message = data['choices'][0]['message']['content'] + self.add_message("assistant", assistant_message) + return assistant_message + else: + error_msg = f"SiliconFlow API错误: {response.status_code} - {response.text}" + logger.error(error_msg) + return f"API调用失败: {error_msg}" + + except Exception as e: + error_msg = f"SiliconFlow对话出错: {str(e)}" + logger.exception("SiliconFlow对话异常") + return error_msg + + def chat_stream(self, message): + """ + 流式对话 + """ + try: + # 添加用户消息 + self.add_message("user", message) + + payload = { + "model": self.model, + "messages": self.messages, + "stream": True, + "max_tokens": 2048, + "temperature": 0.7, + "top_p": 0.7, + } + + headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" + } + + response = requests.post( + f"{self.base_url}/chat/completions", + json=payload, + headers=headers, + stream=True, + timeout=120 + ) + + if response.status_code != 200: + error_msg = f"SiliconFlow API错误: {response.status_code} - {response.text}" + logger.error(error_msg) + yield f"API调用失败: {error_msg}" + return + + assistant_message = "" + for line in response.iter_lines(): + if line: + line = line.decode('utf-8') + if line.startswith('data: '): + data_str = line[6:] # 移除 'data: ' 前缀 + + if data_str.strip() == '[DONE]': + break + + try: + data = json.loads(data_str) + if 'choices' in data and len(data['choices']) > 0: + delta = data['choices'][0].get('delta', {}) + content = delta.get('content', '') + + if content: + assistant_message += content + yield content + + except json.JSONDecodeError: + continue + + # 添加完整的助手回复到历史 + if assistant_message: + self.add_message("assistant", assistant_message) + + except requests.exceptions.Timeout: + error_msg = "SiliconFlow请求超时" + logger.error(error_msg) + yield error_msg + except Exception as e: + error_msg = f"SiliconFlow流式对话出错: {str(e)}" + logger.exception("SiliconFlow流式对话异常") + yield error_msg + + @classmethod + def get_available_models(cls, api_key="sk-xqbujijjqqmlmlvkhvxeogqjtzslnhdtqxqgiyuhwpoqcjvf"): + """ + 获取可用的模型列表 + """ + import os + + # 尝试多种网络配置 + proxy_configs = [ + # 不使用代理 + {'proxies': None, 'verify': True}, + # 使用系统代理但禁用SSL验证 + {'proxies': None, 'verify': False}, + # 明确禁用代理 + {'proxies': {'http': None, 'https': None}, 'verify': True}, + {'proxies': {'http': None, 'https': None}, 'verify': False}, + ] + + for i, config in enumerate(proxy_configs): + try: + logger.info(f"尝试网络配置 {i+1}/{len(proxy_configs)}") + + headers = {"Authorization": f"Bearer {api_key}"} + + # 创建会话以便更好地控制连接 + session = requests.Session() + if config['proxies'] is not None: + session.proxies.update(config['proxies']) + + response = session.get( + "https://api.siliconflow.cn/v1/models", + headers=headers, + timeout=30, + verify=config['verify'] + ) + + if response.status_code == 200: + data = response.json() + all_models = data.get('data', []) + logger.info(f"SiliconFlow API返回 {len(all_models)} 个模型") + + models = [] + excluded_keywords = ['embedding', 'stable-diffusion', 'bge-', 'rerank', 'whisper'] + + for model in all_models: + model_id = model.get('id', '') + if not model_id: + continue + + # 排除明显的非聊天模型 + if any(keyword in model_id.lower() for keyword in excluded_keywords): + continue + + # 包含常见的聊天模型关键词或者包含chat、instruct等 + chat_keywords = ['chat', 'instruct', 'qwen', 'glm', 'internlm', 'baichuan', 'llama', 'mistral', 'claude', 'gpt', 'yi'] + if any(keyword in model_id.lower() for keyword in chat_keywords): + models.append({ + 'id': model_id, + 'name': model_id, + 'description': model.get('description', ''), + }) + + logger.info(f"获取SiliconFlow模型列表成功 - 总数: {len(all_models)}, 聊天模型: {len(models)}") + + # 如果过滤后的模型太少,返回所有模型(除了明确的排除项) + if len(models) < 10: + logger.warning("过滤后的聊天模型数量过少,返回所有非排除模型") + models = [] + for model in all_models: + model_id = model.get('id', '') + if model_id and not any(keyword in model_id.lower() for keyword in excluded_keywords): + models.append({ + 'id': model_id, + 'name': model_id, + 'description': model.get('description', ''), + }) + + return models + else: + logger.warning(f"网络配置 {i+1} 失败: {response.status_code} - {response.text}") + + except Exception as e: + logger.warning(f"网络配置 {i+1} 异常: {str(e)}") + continue + + # 所有配置都失败了 + logger.error("所有网络配置都失败,无法获取模型列表") + raise Exception("无法连接到SiliconFlow API服务器") + + @classmethod + def _get_fallback_models(cls): + """ + 当API调用失败时,返回预定义的常用模型列表 + """ + logger.info("使用预定义的模型列表作为备选方案") + return [ + { + 'id': 'Qwen/Qwen2.5-7B-Instruct', + 'name': 'Qwen2.5-7B-Instruct', + 'description': '通义千问2.5 7B指令模型' + }, + { + 'id': 'Qwen/Qwen2.5-14B-Instruct', + 'name': 'Qwen2.5-14B-Instruct', + 'description': '通义千问2.5 14B指令模型' + }, + { + 'id': 'Qwen/Qwen2.5-32B-Instruct', + 'name': 'Qwen2.5-32B-Instruct', + 'description': '通义千问2.5 32B指令模型' + }, + { + 'id': 'Qwen/Qwen2.5-72B-Instruct', + 'name': 'Qwen2.5-72B-Instruct', + 'description': '通义千问2.5 72B指令模型' + }, + { + 'id': 'Qwen/QwQ-32B-Preview', + 'name': 'QwQ-32B-Preview', + 'description': '通义千问推理模型预览版' + }, + { + 'id': 'deepseek-ai/DeepSeek-V2.5', + 'name': 'DeepSeek-V2.5', + 'description': 'DeepSeek V2.5 模型' + }, + { + 'id': 'meta-llama/Llama-3.1-8B-Instruct', + 'name': 'Llama-3.1-8B-Instruct', + 'description': 'Meta Llama 3.1 8B指令模型' + }, + { + 'id': 'meta-llama/Llama-3.1-70B-Instruct', + 'name': 'Llama-3.1-70B-Instruct', + 'description': 'Meta Llama 3.1 70B指令模型' + }, + { + 'id': 'THUDM/glm-4-9b-chat', + 'name': 'GLM-4-9B-Chat', + 'description': '智谱GLM-4 9B对话模型' + }, + { + 'id': 'internlm/internlm2_5-7b-chat', + 'name': 'InternLM2.5-7B-Chat', + 'description': '书生浦语2.5 7B对话模型' + } + ] \ No newline at end of file diff --git a/apps/rlhf/urls.py b/apps/rlhf/urls.py new file mode 100644 index 0000000..3064636 --- /dev/null +++ b/apps/rlhf/urls.py @@ -0,0 +1,29 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import ( + ConversationViewSet, MessageViewSet, FeedbackViewSet, + FeedbackTagViewSet, DetailedFeedbackViewSet, ConversationSubmissionViewSet, + ConversationEvaluationViewSet, SystemConfigViewSet +) + +router = DefaultRouter() +router.register(r'conversations', ConversationViewSet) +router.register(r'messages', MessageViewSet) +router.register(r'feedback', FeedbackViewSet) +router.register(r'feedback-tags', FeedbackTagViewSet) +router.register(r'detailed-feedback', DetailedFeedbackViewSet) +router.register(r'submissions', ConversationSubmissionViewSet) +router.register(r'evaluations', ConversationEvaluationViewSet) +router.register(r'system-config', SystemConfigViewSet) + +urlpatterns = [ + path('', include(router.urls)), + # 额外的RLHF相关API端点 + path('conversation//messages/', ConversationViewSet.as_view({'get': 'messages'}), name='conversation-messages'), + path('conversation//message/', ConversationViewSet.as_view({'post': 'message'}), name='send-message'), + path('conversation//submit/', ConversationViewSet.as_view({'post': 'submit'}), name='submit-conversation'), + path('conversation//resume/', ConversationViewSet.as_view({'post': 'resume'}), name='resume-conversation'), + path('submission//review/', ConversationSubmissionViewSet.as_view({'post': 'review'}), name='review-submission'), + path('models/', SystemConfigViewSet.as_view({'get': 'models'}), name='models-list'), + path('model/', SystemConfigViewSet.as_view({'get': 'model', 'post': 'model'}), name='current-model'), +] \ No newline at end of file diff --git a/apps/rlhf/views.py b/apps/rlhf/views.py new file mode 100644 index 0000000..eb3166b --- /dev/null +++ b/apps/rlhf/views.py @@ -0,0 +1,559 @@ +from django.shortcuts import render, get_object_or_404 +from rest_framework import viewsets, status +from rest_framework.response import Response +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.pagination import PageNumberPagination +from .models import ( + Conversation, Message, Feedback, FeedbackTag, DetailedFeedback, + ConversationSubmission, ConversationEvaluation, SystemConfig +) +from .serializers import ( + ConversationSerializer, MessageSerializer, FeedbackSerializer, + FeedbackTagSerializer, DetailedFeedbackSerializer, ConversationSubmissionSerializer, + ConversationEvaluationSerializer, SystemConfigSerializer +) +from apps.user.models import User, UserActivityLog, AnnotationStats +from django.utils import timezone +import uuid +import json +from django.db.models import Count, Avg, Sum, Q, F +from datetime import datetime, timedelta +from django.db import transaction +from django.db.models.functions import TruncDate +from apps.user.authentication import CustomTokenAuthentication + +class ConversationViewSet(viewsets.ModelViewSet): + queryset = Conversation.objects.all() + serializer_class = ConversationSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + return Conversation.objects.filter(user=user).order_by('-created_at') + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + @action(detail=True, methods=['get']) + def messages(self, request, pk=None): + conversation = self.get_object() + messages = Message.objects.filter(conversation=conversation).order_by('timestamp') + serializer = MessageSerializer(messages, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['post']) + def message(self, request, pk=None): + conversation = self.get_object() + content = request.data.get('content') + + if not content: + return Response({'error': '消息内容不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # 创建用户消息 + user_message = Message.objects.create( + id=str(uuid.uuid4()), + conversation=conversation, + role='user', + content=content + ) + + # 这里需要调用AI服务获取回复 + # 示例:调用SiliconFlow或其他AI服务 + ai_response = self._generate_ai_response(user_message.content, conversation) + + # 创建AI回复消息 + ai_message = Message.objects.create( + id=str(uuid.uuid4()), + conversation=conversation, + role='assistant', + content=ai_response + ) + + # 更新用户的标注统计 + self._update_annotation_stats(request.user.id) + + messages = [ + MessageSerializer(user_message).data, + MessageSerializer(ai_message).data + ] + + return Response(messages) + + @action(detail=True, methods=['post']) + def submit(self, request, pk=None): + conversation = self.get_object() + title = request.data.get('title', '') + description = request.data.get('description', '') + + if conversation.is_submitted: + return Response({'error': '该对话已提交'}, status=status.HTTP_400_BAD_REQUEST) + + # 更新对话为已提交状态 + conversation.is_submitted = True + conversation.save() + + # 创建提交记录 + submission = ConversationSubmission.objects.create( + id=str(uuid.uuid4()), + conversation=conversation, + user=request.user, + title=title, + description=description, + status='submitted', + submitted_at=timezone.now() + ) + + # 记录活动日志 + UserActivityLog.objects.create( + user=request.user, + action_type='conversation_submit', + target_type='conversation', + target_id=str(conversation.id), + details={'title': title} + ) + + return Response({ + 'message': '对话提交成功', + 'submission_id': submission.id + }) + + @action(detail=True, methods=['post']) + def resume(self, request, pk=None): + conversation = self.get_object() + + if not conversation.is_submitted: + return Response({'error': '该对话未提交,无需恢复'}, status=status.HTTP_400_BAD_REQUEST) + + # 更新对话为未提交状态 + conversation.is_submitted = False + conversation.save() + + # 获取最新的提交记录 + submission = ConversationSubmission.objects.filter( + conversation=conversation + ).order_by('-submitted_at').first() + + if submission and submission.status == 'submitted': + submission.status = 'rejected' + submission.save() + + # 记录活动日志 + UserActivityLog.objects.create( + user=request.user, + action_type='conversation_resume', + target_type='conversation', + target_id=str(conversation.id) + ) + + return Response({'message': '对话已恢复为未提交状态'}) + + def _generate_ai_response(self, user_message, conversation): + """ + 生成AI回复 + 这里只是一个示例,实际应用中需要对接真实的AI服务 + """ + # 从系统配置获取当前使用的模型 + model_config = SystemConfig.objects.filter(config_key='current_model').first() + model_name = model_config.config_value if model_config else "默认模型" + + # 获取历史消息作为上下文 + history_messages = Message.objects.filter(conversation=conversation).order_by('timestamp') + history = [] + for msg in history_messages: + history.append({"role": msg.role, "content": msg.content}) + + # 在这里调用实际的AI API + # 例如,如果使用SiliconFlow API + # response = sf_client.chat(user_message, history) + + # 这里仅作为示例,返回一个固定的回复 + return f"这是AI({model_name})的回复:我已收到您的消息「{user_message}」。根据您的问题,我的建议是..." + + def _update_annotation_stats(self, user_id): + """更新用户的标注统计信息""" + today = timezone.now().date() + + # 获取或创建今天的统计记录 + stats, created = AnnotationStats.objects.get_or_create( + user_id=user_id, + date=today, + defaults={ + 'id': str(uuid.uuid4()), + 'total_annotations': 0, + 'positive_annotations': 0, + 'negative_annotations': 0, + 'conversations_count': 0, + 'messages_count': 0 + } + ) + + # 更新消息计数 + stats.messages_count += 1 + stats.save() + + +class MessageViewSet(viewsets.ModelViewSet): + queryset = Message.objects.all() + serializer_class = MessageSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + return Message.objects.filter(conversation__user=user).order_by('timestamp') + + +class FeedbackViewSet(viewsets.ModelViewSet): + queryset = Feedback.objects.all() + serializer_class = FeedbackSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + return Feedback.objects.filter(user=user).order_by('-timestamp') + + def create(self, request, *args, **kwargs): + message_id = request.data.get('message_id') + conversation_id = request.data.get('conversation_id') + feedback_value = request.data.get('feedback_value') + + if not message_id or not conversation_id: + return Response({'error': '消息ID和对话ID不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + try: + message = Message.objects.get(id=message_id) + conversation = Conversation.objects.get(id=conversation_id) + except (Message.DoesNotExist, Conversation.DoesNotExist): + return Response({'error': '消息或对话不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 创建或更新反馈 + feedback, created = Feedback.objects.update_or_create( + message_id=message_id, + conversation_id=conversation_id, + user=request.user, + defaults={ + 'id': str(uuid.uuid4()) if created else F('id'), + 'feedback_value': feedback_value, + 'timestamp': timezone.now() + } + ) + + # 更新用户的标注统计 + self._update_annotation_stats(request.user.id, feedback_value) + + return Response(FeedbackSerializer(feedback).data) + + def _update_annotation_stats(self, user_id, feedback_value): + """更新用户的标注统计信息""" + today = timezone.now().date() + + # 获取或创建今天的统计记录 + stats, created = AnnotationStats.objects.get_or_create( + user_id=user_id, + date=today, + defaults={ + 'id': str(uuid.uuid4()), + 'total_annotations': 0, + 'positive_annotations': 0, + 'negative_annotations': 0, + 'conversations_count': 0, + 'messages_count': 0 + } + ) + + # 更新统计 + stats.total_annotations += 1 + if feedback_value > 0: + stats.positive_annotations += 1 + elif feedback_value < 0: + stats.negative_annotations += 1 + + stats.save() + + +class FeedbackTagViewSet(viewsets.ModelViewSet): + queryset = FeedbackTag.objects.all() + serializer_class = FeedbackTagSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + tag_type = self.request.query_params.get('type') + if tag_type and tag_type in ['positive', 'negative']: + return FeedbackTag.objects.filter(tag_type=tag_type) + return FeedbackTag.objects.all() + + +class DetailedFeedbackViewSet(viewsets.ModelViewSet): + queryset = DetailedFeedback.objects.all() + serializer_class = DetailedFeedbackSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + return DetailedFeedback.objects.filter(user=user).order_by('-created_at') + + def create(self, request, *args, **kwargs): + message_id = request.data.get('message_id') + conversation_id = request.data.get('conversation_id') + feedback_type = request.data.get('feedback_type') + feedback_tags = request.data.get('feedback_tags', []) + custom_tags = request.data.get('custom_tags', '') + custom_content = request.data.get('custom_content', '') + is_inline = request.data.get('is_inline', True) + + if not message_id or not conversation_id or not feedback_type: + return Response({ + 'error': '消息ID、对话ID和反馈类型不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + message = Message.objects.get(id=message_id) + conversation = Conversation.objects.get(id=conversation_id) + except (Message.DoesNotExist, Conversation.DoesNotExist): + return Response({'error': '消息或对话不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 将标签列表转换为JSON字符串 + if isinstance(feedback_tags, list): + feedback_tags = json.dumps(feedback_tags) + + # 创建详细反馈 + detailed_feedback = DetailedFeedback.objects.create( + id=str(uuid.uuid4()), + message=message, + conversation=conversation, + user=request.user, + feedback_type=feedback_type, + feedback_tags=feedback_tags, + custom_tags=custom_tags, + custom_content=custom_content, + is_inline=is_inline, + created_at=timezone.now(), + updated_at=timezone.now() + ) + + # 记录活动日志 + UserActivityLog.objects.create( + user=request.user, + action_type='detailed_feedback_submit', + target_type='message', + target_id=message_id, + details={ + 'feedback_type': feedback_type, + 'is_inline': is_inline + } + ) + + # 更新用户的标注统计 + self._update_annotation_stats(request.user.id, feedback_type) + + return Response(DetailedFeedbackSerializer(detailed_feedback).data) + + def _update_annotation_stats(self, user_id, feedback_type): + """更新用户的标注统计信息""" + today = timezone.now().date() + + # 获取或创建今天的统计记录 + stats, created = AnnotationStats.objects.get_or_create( + user_id=user_id, + date=today, + defaults={ + 'id': str(uuid.uuid4()), + 'total_annotations': 0, + 'positive_annotations': 0, + 'negative_annotations': 0, + 'conversations_count': 0, + 'messages_count': 0 + } + ) + + # 更新统计 + stats.total_annotations += 1 + if feedback_type == 'positive': + stats.positive_annotations += 1 + elif feedback_type == 'negative': + stats.negative_annotations += 1 + + stats.save() + + +class ConversationSubmissionViewSet(viewsets.ModelViewSet): + queryset = ConversationSubmission.objects.all() + serializer_class = ConversationSubmissionSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + + # 管理员可以查看所有提交 + if user.role == 'admin': + queryset = ConversationSubmission.objects.all() + else: + # 普通用户只能查看自己的提交 + queryset = ConversationSubmission.objects.filter(user=user) + + # 过滤状态 + status_filter = self.request.query_params.get('status') + if status_filter: + queryset = queryset.filter(status=status_filter) + + return queryset.order_by('-submitted_at') + + @action(detail=True, methods=['post']) + def review(self, request, pk=None): + if request.user.role != 'admin': + return Response({'error': '只有管理员可以进行审核'}, status=status.HTTP_403_FORBIDDEN) + + submission = self.get_object() + status_value = request.data.get('status') + quality_score = request.data.get('quality_score') + reviewer_notes = request.data.get('reviewer_notes', '') + + if not status_value or status_value not in ['accepted', 'rejected']: + return Response({'error': '状态值无效'}, status=status.HTTP_400_BAD_REQUEST) + + if quality_score is not None and (quality_score < 1 or quality_score > 5): + return Response({'error': '质量分数必须在1-5之间'}, status=status.HTTP_400_BAD_REQUEST) + + # 更新提交状态 + submission.status = status_value + submission.quality_score = quality_score + submission.reviewer = request.user + submission.reviewer_notes = reviewer_notes + submission.reviewed_at = timezone.now() + submission.save() + + # 记录活动日志 + UserActivityLog.objects.create( + user=request.user, + action_type='submission_review', + target_type='submission', + target_id=submission.id, + details={ + 'status': status_value, + 'quality_score': quality_score + } + ) + + return Response({ + 'message': '审核完成', + 'submission': ConversationSubmissionSerializer(submission).data + }) + + +class ConversationEvaluationViewSet(viewsets.ModelViewSet): + queryset = ConversationEvaluation.objects.all() + serializer_class = ConversationEvaluationSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + user = self.request.user + return ConversationEvaluation.objects.filter(user=user).order_by('-created_at') + + def create(self, request, *args, **kwargs): + conversation_id = request.data.get('conversation_id') + overall_feeling = request.data.get('overall_feeling', '') + has_logical_issues = request.data.get('has_logical_issues') + needs_satisfied = request.data.get('needs_satisfied') + + if not conversation_id or not has_logical_issues or not needs_satisfied: + return Response({ + 'error': '对话ID、逻辑问题和需求满足度不能为空' + }, status=status.HTTP_400_BAD_REQUEST) + + try: + conversation = Conversation.objects.get(id=conversation_id) + except Conversation.DoesNotExist: + return Response({'error': '对话不存在'}, status=status.HTTP_404_NOT_FOUND) + + # 创建或更新评估 + evaluation, created = ConversationEvaluation.objects.update_or_create( + conversation_id=conversation_id, + user=request.user, + defaults={ + 'id': str(uuid.uuid4()) if created else F('id'), + 'overall_feeling': overall_feeling, + 'has_logical_issues': has_logical_issues, + 'needs_satisfied': needs_satisfied, + 'updated_at': timezone.now() + } + ) + + # 记录活动日志 + UserActivityLog.objects.create( + user=request.user, + action_type='conversation_evaluation', + target_type='conversation', + target_id=conversation_id, + details={ + 'has_logical_issues': has_logical_issues, + 'needs_satisfied': needs_satisfied + } + ) + + return Response(ConversationEvaluationSerializer(evaluation).data) + + +class SystemConfigViewSet(viewsets.ModelViewSet): + queryset = SystemConfig.objects.all() + serializer_class = SystemConfigSerializer + authentication_classes = [CustomTokenAuthentication] + permission_classes = [IsAuthenticated] + + def get_queryset(self): + # 只有管理员可以查看所有配置 + if self.request.user.role == 'admin': + return SystemConfig.objects.all() + # 其他用户只能查看公开配置 + return SystemConfig.objects.filter(config_key__startswith='public_') + + @action(detail=False, methods=['get', 'post']) + def model(self, request): + if request.method == 'GET': + # 获取当前模型 + model_config = SystemConfig.objects.filter(config_key='current_model').first() + if model_config: + return Response({'model': model_config.config_value}) + return Response({'model': '默认模型'}) + + elif request.method == 'POST': + # 设置当前模型 + if request.user.role != 'admin': + return Response({'error': '只有管理员可以更改模型'}, status=status.HTTP_403_FORBIDDEN) + + model_name = request.data.get('model') + if not model_name: + return Response({'error': '模型名称不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # 更新或创建配置 + config, created = SystemConfig.objects.update_or_create( + config_key='current_model', + defaults={ + 'id': str(uuid.uuid4()) if created else F('id'), + 'config_value': model_name, + 'config_type': 'string', + 'description': '当前使用的AI模型', + 'updated_at': timezone.now() + } + ) + + return Response({'model': model_name}) + + @action(detail=False, methods=['get']) + def models(self, request): + # 返回可用的模型列表 + return Response({ + 'models': [ + {'id': 'model1', 'name': 'GPT-3.5'}, + {'id': 'model2', 'name': 'GPT-4'}, + {'id': 'model3', 'name': 'Claude'}, + {'id': 'model4', 'name': 'LLaMA'}, + {'id': 'model5', 'name': 'Qwen'} + ] + }) \ No newline at end of file diff --git a/apps/user/migrations/0002_annotationquality_annotationstats_useractivitylog_and_more.py b/apps/user/migrations/0002_annotationquality_annotationstats_useractivitylog_and_more.py new file mode 100644 index 0000000..2a9b827 --- /dev/null +++ b/apps/user/migrations/0002_annotationquality_annotationstats_useractivitylog_and_more.py @@ -0,0 +1,155 @@ +# Generated by Django 5.1.5 on 2025-06-09 08:28 + +import django.db.models.deletion +import django.utils.timezone +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('user', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AnnotationQuality', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('message_id', models.CharField(max_length=36)), + ('quality_score', models.FloatField(default=0.0)), + ('consistency_score', models.FloatField(default=0.0)), + ('review_status', models.CharField(choices=[('pending', '待审核'), ('approved', '已通过'), ('rejected', '已拒绝')], default='pending', max_length=20)), + ('review_notes', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('reviewed_at', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'verbose_name': '标注质量', + 'verbose_name_plural': '标注质量', + }, + ), + migrations.CreateModel( + name='AnnotationStats', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('date', models.DateField()), + ('total_annotations', models.IntegerField(default=0)), + ('positive_annotations', models.IntegerField(default=0)), + ('negative_annotations', models.IntegerField(default=0)), + ('conversations_count', models.IntegerField(default=0)), + ('messages_count', models.IntegerField(default=0)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('updated_at', models.DateTimeField(default=django.utils.timezone.now)), + ], + options={ + 'verbose_name': '标注统计', + 'verbose_name_plural': '标注统计', + }, + ), + migrations.CreateModel( + name='UserActivityLog', + fields=[ + ('id', models.CharField(default=uuid.uuid4, editable=False, max_length=36, primary_key=True, serialize=False)), + ('action_type', models.CharField(max_length=50)), + ('target_type', models.CharField(blank=True, max_length=50, null=True)), + ('target_id', models.CharField(blank=True, max_length=36, null=True)), + ('details', models.TextField(blank=True, null=True)), + ('ip_address', models.CharField(blank=True, max_length=45, null=True)), + ('user_agent', models.TextField(blank=True, null=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ], + options={ + 'verbose_name': '用户活动日志', + 'verbose_name_plural': '用户活动日志', + }, + ), + migrations.AddField( + model_name='user', + name='full_name', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='user', + name='groups', + field=models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups'), + ), + migrations.AddField( + model_name='user', + name='profile_image', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name='user', + name='role', + field=models.CharField(choices=[('user', '普通用户'), ('annotator', '标注员'), ('admin', '管理员')], default='annotator', max_length=50), + ), + migrations.AddField( + model_name='user', + name='user_permissions', + field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), + ), + migrations.AddField( + model_name='user', + name='username', + field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='用户名'), + ), + migrations.AddField( + model_name='usertoken', + name='ip_address', + field=models.CharField(blank=True, max_length=100, null=True), + ), + migrations.AddField( + model_name='usertoken', + name='last_accessed', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AddField( + model_name='usertoken', + name='user_agent', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddIndex( + model_name='user', + index=models.Index(fields=['username'], name='users_usernam_baeb4b_idx'), + ), + migrations.AddIndex( + model_name='user', + index=models.Index(fields=['email'], name='users_email_4b85f2_idx'), + ), + migrations.AddIndex( + model_name='usertoken', + index=models.Index(fields=['token'], name='user_token_token_deff65_idx'), + ), + migrations.AddIndex( + model_name='usertoken', + index=models.Index(fields=['user'], name='user_token_user_id_209416_idx'), + ), + migrations.AddField( + model_name='annotationquality', + name='reviewer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviews', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='annotationquality', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='annotation_quality', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='annotationstats', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='annotation_stats', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='useractivitylog', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_logs', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterUniqueTogether( + name='annotationstats', + unique_together={('user', 'date')}, + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index fe2acec..b349a4f 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -1,7 +1,8 @@ from django.db import models from django.utils import timezone from datetime import timedelta -from django.contrib.auth.models import AbstractBaseUser, BaseUserManager +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +import uuid class UserManager(BaseUserManager): def create_user(self, email, password=None, **extra_fields): @@ -17,6 +18,7 @@ class UserManager(BaseUserManager): def create_superuser(self, email, password=None, **extra_fields): extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault('role', 'admin') if extra_fields.get('is_staff') is not True: raise ValueError('超级用户必须设置is_staff=True') @@ -25,12 +27,22 @@ class UserManager(BaseUserManager): return self.create_user(email, password, **extra_fields) -class User(AbstractBaseUser): +class User(AbstractBaseUser, PermissionsMixin): """用户模型,用于登录和账户管理""" + ROLE_CHOICES = ( + ('user', '普通用户'), + ('annotator', '标注员'), + ('admin', '管理员'), + ) + email = models.EmailField(max_length=255, unique=True, verbose_name="电子邮箱") password = models.CharField(max_length=255, verbose_name="密码") + username = models.CharField(max_length=255, unique=True, null=True, blank=True, verbose_name="用户名") company = models.CharField(max_length=255, blank=True, null=True, verbose_name="公司名称") name = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户姓名") + full_name = models.CharField(max_length=255, null=True, blank=True) + role = models.CharField(max_length=50, choices=ROLE_CHOICES, default='annotator') + profile_image = models.CharField(max_length=255, null=True, blank=True) is_first_login = models.BooleanField(default=True, verbose_name="是否首次登录") last_login = models.DateTimeField(blank=True, null=True, verbose_name="最近登录时间") is_active = models.BooleanField(default=True) @@ -44,12 +56,17 @@ class User(AbstractBaseUser): objects = UserManager() USERNAME_FIELD = 'email' + EMAIL_FIELD = 'email' REQUIRED_FIELDS = [] class Meta: verbose_name = "用户" verbose_name_plural = verbose_name db_table = "users" + indexes = [ + models.Index(fields=['username']), + models.Index(fields=['email']), + ] def __str__(self): return self.email @@ -71,6 +88,9 @@ class UserToken(models.Model): token = models.CharField(max_length=40, unique=True) created_at = models.DateTimeField(auto_now_add=True) expired_at = models.DateTimeField() + last_accessed = models.DateTimeField(default=timezone.now) + ip_address = models.CharField(max_length=100, null=True, blank=True) + user_agent = models.CharField(max_length=255, null=True, blank=True) def save(self, *args, **kwargs): if not self.expired_at: @@ -83,3 +103,66 @@ class UserToken(models.Model): class Meta: db_table = 'user_token' + indexes = [ + models.Index(fields=['token']), + models.Index(fields=['user']), + ] + +class UserActivityLog(models.Model): + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='activity_logs') + action_type = models.CharField(max_length=50) + target_type = models.CharField(max_length=50, blank=True, null=True) + target_id = models.CharField(max_length=36, blank=True, null=True) + details = models.TextField(blank=True, null=True) + ip_address = models.CharField(max_length=45, blank=True, null=True) + user_agent = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '用户活动日志' + verbose_name_plural = '用户活动日志' + + +class AnnotationStats(models.Model): + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='annotation_stats') + date = models.DateField() + total_annotations = models.IntegerField(default=0) + positive_annotations = models.IntegerField(default=0) + negative_annotations = models.IntegerField(default=0) + conversations_count = models.IntegerField(default=0) + messages_count = models.IntegerField(default=0) + created_at = models.DateTimeField(default=timezone.now) + updated_at = models.DateTimeField(default=timezone.now) + + class Meta: + + verbose_name = '标注统计' + verbose_name_plural = '标注统计' + unique_together = ('user', 'date') + + +class AnnotationQuality(models.Model): + REVIEW_STATUS_CHOICES = ( + ('pending', '待审核'), + ('approved', '已通过'), + ('rejected', '已拒绝'), + ) + + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4, editable=False) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='annotation_quality') + message_id = models.CharField(max_length=36) + quality_score = models.FloatField(default=0.0) + consistency_score = models.FloatField(default=0.0) + review_status = models.CharField(max_length=20, choices=REVIEW_STATUS_CHOICES, default='pending') + reviewer = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='reviews') + review_notes = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(default=timezone.now) + reviewed_at = models.DateTimeField(null=True, blank=True) + + class Meta: + + verbose_name = '标注质量' + verbose_name_plural = '标注质量' \ No newline at end of file diff --git a/apps/user/serializers.py b/apps/user/serializers.py new file mode 100644 index 0000000..d3923b8 --- /dev/null +++ b/apps/user/serializers.py @@ -0,0 +1,81 @@ +from rest_framework import serializers +from .models import User, UserToken, UserActivityLog, AnnotationStats, AnnotationQuality + +class UserSerializer(serializers.ModelSerializer): + """用户序列化器""" + class Meta: + model = User + fields = ['id', 'email', 'username', 'company', 'name', 'full_name', + 'role', 'profile_image', 'is_first_login', 'last_login', + 'is_active', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at', 'last_login'] + +class UserCreateSerializer(serializers.ModelSerializer): + """用户创建序列化器""" + password = serializers.CharField(write_only=True, required=True, style={'input_type': 'password'}) + + class Meta: + model = User + fields = ['email', 'password', 'username', 'company', 'name', 'full_name', 'role'] + + def create(self, validated_data): + return User.objects.create_user(**validated_data) + +class UserUpdateSerializer(serializers.ModelSerializer): + """用户更新序列化器""" + password = serializers.CharField(write_only=True, required=False, style={'input_type': 'password'}) + + class Meta: + model = User + fields = ['email', 'password', 'username', 'company', 'name', 'full_name', 'profile_image'] + read_only_fields = ['email'] + + def update(self, instance, validated_data): + password = validated_data.pop('password', None) + user = super().update(instance, validated_data) + + if password: + user.set_password(password) + user.save() + + return user + +class UserTokenSerializer(serializers.ModelSerializer): + """用户令牌序列化器""" + user = UserSerializer(read_only=True) + is_expired = serializers.BooleanField(read_only=True, source='is_expired') + + class Meta: + model = UserToken + fields = ['id', 'user', 'token', 'created_at', 'expired_at', 'last_accessed', 'ip_address', 'user_agent', 'is_expired'] + read_only_fields = ['id', 'user', 'token', 'created_at', 'expired_at', 'last_accessed'] + +class UserActivityLogSerializer(serializers.ModelSerializer): + """用户活动日志序列化器""" + user = UserSerializer(read_only=True) + + class Meta: + model = UserActivityLog + fields = ['id', 'user', 'action_type', 'target_type', 'target_id', 'details', 'ip_address', 'user_agent', 'created_at'] + read_only_fields = ['id', 'created_at'] + +class AnnotationStatsSerializer(serializers.ModelSerializer): + """标注统计序列化器""" + user = UserSerializer(read_only=True) + + class Meta: + model = AnnotationStats + fields = ['id', 'user', 'date', 'total_annotations', 'positive_annotations', 'negative_annotations', + 'conversations_count', 'messages_count', 'created_at', 'updated_at'] + read_only_fields = ['id', 'created_at', 'updated_at'] + +class AnnotationQualitySerializer(serializers.ModelSerializer): + """标注质量序列化器""" + user = UserSerializer(read_only=True) + reviewer = UserSerializer(read_only=True) + + class Meta: + model = AnnotationQuality + fields = ['id', 'user', 'message_id', 'quality_score', 'consistency_score', 'review_status', + 'reviewer', 'review_notes', 'created_at', 'reviewed_at'] + read_only_fields = ['id', 'created_at', 'reviewed_at'] \ No newline at end of file diff --git a/daren/settings.py b/daren/settings.py index daefaa8..e2e3d4a 100644 --- a/daren/settings.py +++ b/daren/settings.py @@ -56,6 +56,7 @@ INSTALLED_APPS = [ "apps.feishu.apps.FeishuConfig", 'django_celery_beat', # Celery定时任务 'django_celery_results', # Celery结果存储 + "apps.rlhf.apps.RlhfConfig", ] MIDDLEWARE = [ diff --git a/daren/urls.py b/daren/urls.py index aedced5..0b27e88 100644 --- a/daren/urls.py +++ b/daren/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ path('api/gmail/', include('apps.gmail.urls')), path('api/feishu/', include('apps.feishu.urls')), path('api/knowledge-bases/', include('apps.knowledge_base.urls')), + path('api/rlhf/', include('apps.rlhf.urls')), ] # 在开发环境中提供媒体文件服务