Added file ingestion retry and file link

This commit is contained in:
Viswamedha Nalabotu 2026-03-22 09:09:14 +00:00
parent f9073d53b6
commit 8cce790b2f
3 changed files with 66 additions and 20 deletions

View file

@ -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

View file

@ -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/',

View file

@ -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
}, },
}, },
] ]