Added file ingestion retry and file link
This commit is contained in:
parent
f9073d53b6
commit
8cce790b2f
3 changed files with 66 additions and 20 deletions
|
|
@ -2,13 +2,15 @@ from django.db.models import Q
|
||||||
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
|
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
|
||||||
from rest_framework.parsers import FormParser, MultiPartParser
|
from rest_framework.parsers import FormParser, MultiPartParser
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
from apps.accounts.models import Organization, Role
|
from apps.accounts.models import Organization, Role
|
||||||
from apps.accounts.permissions import can_manage_organization
|
from apps.accounts.permissions import can_manage_organization
|
||||||
from apps.knowledge.models import RoleRagDocument, TrainingFile
|
from apps.knowledge.models import RoleRagDocument, TrainingFile
|
||||||
from apps.knowledge.serializers import RoleRagDocumentSerializer, TrainingFileSerializer
|
from apps.knowledge.serializers import RoleRagDocumentSerializer, TrainingFileSerializer
|
||||||
from apps.knowledge.tasks import update_agent_prompts_from_file_task
|
from apps.knowledge.tasks import ingest_training_file_task, update_agent_prompts_from_file_task
|
||||||
|
|
||||||
class TrainingFileViewSet(ModelViewSet):
|
class TrainingFileViewSet(ModelViewSet):
|
||||||
queryset = TrainingFile.objects.all()
|
queryset = TrainingFile.objects.all()
|
||||||
|
|
@ -80,14 +82,28 @@ class TrainingFileViewSet(ModelViewSet):
|
||||||
file_type=uploaded_file.content_type,
|
file_type=uploaded_file.content_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'], url_path='retry')
|
||||||
|
def retry(self, request, *args, **kwargs):
|
||||||
|
instance: TrainingFile = self.get_object()
|
||||||
|
|
||||||
|
if not can_manage_organization(request.user, instance.organization):
|
||||||
|
raise PermissionDenied('Permission denied')
|
||||||
|
|
||||||
|
if instance.status != 'failed':
|
||||||
|
raise ValidationError({'status': 'Only failed files can be retried.'})
|
||||||
|
|
||||||
|
instance.status = 'ingesting'
|
||||||
|
instance.is_processed = False
|
||||||
|
instance.save(update_fields=['status', 'is_processed'])
|
||||||
|
ingest_training_file_task.delay(str(instance.uuid))
|
||||||
|
|
||||||
|
serializer = self.get_serializer(instance)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance: TrainingFile = self.get_object()
|
||||||
|
|
||||||
is_uploader = instance.uploaded_by == request.user
|
if not can_manage_organization(request.user, instance.organization):
|
||||||
is_org_owner = instance.organization.owner == request.user
|
|
||||||
is_org_manager = bool(request.user.is_manager) and instance.organization.members.filter(id=request.user.id).exists()
|
|
||||||
|
|
||||||
if not (is_uploader or is_org_owner or is_org_manager):
|
|
||||||
raise PermissionDenied('Permission denied')
|
raise PermissionDenied('Permission denied')
|
||||||
|
|
||||||
role_uuid = str(instance.role.uuid) if instance.role_id else None
|
role_uuid = str(instance.role.uuid) if instance.role_id else None
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,7 @@ export const API = {
|
||||||
trainingFiles: {
|
trainingFiles: {
|
||||||
list: () => 'training-file/',
|
list: () => 'training-file/',
|
||||||
byId: (uuid: string) => `training-file/${uuid}/`,
|
byId: (uuid: string) => `training-file/${uuid}/`,
|
||||||
|
retry: (uuid: string) => `training-file/${uuid}/retry/`,
|
||||||
},
|
},
|
||||||
roleRagDocuments: {
|
roleRagDocuments: {
|
||||||
list: () => 'role-rag-document/',
|
list: () => 'role-rag-document/',
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import type { User } from '../types/user'
|
||||||
import type { InviteToken } from '../types/organization'
|
import type { InviteToken } from '../types/organization'
|
||||||
import type { Role } from '../types/organization'
|
import type { Role } from '../types/organization'
|
||||||
import type { TrainingFile } from '../types/organization'
|
import type { TrainingFile } from '../types/organization'
|
||||||
import { InboxOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
import { InboxOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -269,6 +269,18 @@ const deleteTrainingFile = async (uuid: string, fileName: string) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const retryIngestion = async (uuid: string) => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post<TrainingFile>(API.knowledge.trainingFiles.retry(uuid))
|
||||||
|
const idx = trainingFiles.value.findIndex((f) => f.uuid === uuid)
|
||||||
|
if (idx !== -1) trainingFiles.value[idx] = response.data
|
||||||
|
message.success('Ingestion re-queued')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to retry ingestion:', error)
|
||||||
|
message.error('Failed to retry ingestion')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const canDeleteTrainingFile = (record: TrainingFile): boolean => {
|
const canDeleteTrainingFile = (record: TrainingFile): boolean => {
|
||||||
if (auth.user?.uuid === record.uploaded_by?.uuid) return true
|
if (auth.user?.uuid === record.uploaded_by?.uuid) return true
|
||||||
if (organization.value?.owner?.uuid === auth.user?.uuid) return true
|
if (organization.value?.owner?.uuid === auth.user?.uuid) return true
|
||||||
|
|
@ -286,8 +298,9 @@ const formatFileSize = (bytes: number) => {
|
||||||
const trainingFileColumns = [
|
const trainingFileColumns = [
|
||||||
{
|
{
|
||||||
title: 'File Name',
|
title: 'File Name',
|
||||||
dataIndex: 'file_name',
|
|
||||||
key: 'file_name',
|
key: 'file_name',
|
||||||
|
customRender: ({ record }: { record: TrainingFile }) =>
|
||||||
|
h('a', { href: record.file_url, target: '_blank', rel: 'noopener noreferrer' }, record.file_name),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Uploaded By',
|
title: 'Uploaded By',
|
||||||
|
|
@ -333,8 +346,23 @@ const trainingFileColumns = [
|
||||||
title: 'Action',
|
title: 'Action',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
customRender: ({ record }: { record: TrainingFile }) => {
|
customRender: ({ record }: { record: TrainingFile }) => {
|
||||||
|
const buttons: ReturnType<typeof h>[] = []
|
||||||
|
if (record.status === 'failed' && canDeleteTrainingFile(record)) {
|
||||||
|
buttons.push(
|
||||||
|
h(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
size: 'small',
|
||||||
|
icon: h(ReloadOutlined),
|
||||||
|
onClick: () => retryIngestion(record.uuid),
|
||||||
|
},
|
||||||
|
() => 'Retry',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (canDeleteTrainingFile(record)) {
|
if (canDeleteTrainingFile(record)) {
|
||||||
return h(
|
buttons.push(
|
||||||
|
h(
|
||||||
Button,
|
Button,
|
||||||
{
|
{
|
||||||
danger: true,
|
danger: true,
|
||||||
|
|
@ -343,9 +371,10 @@ const trainingFileColumns = [
|
||||||
onClick: () => deleteTrainingFile(record.uuid, record.file_name),
|
onClick: () => deleteTrainingFile(record.uuid, record.file_name),
|
||||||
},
|
},
|
||||||
() => 'Delete',
|
() => 'Delete',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
return buttons.length ? h(Space, { size: 'small' }, () => buttons) : null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue