import { Resolver, Query, Mutation, Arg, Ctx, Int, Directive } from 'type-graphql' import { labelStudioApi } from '../../utils/label-studio-api-client.js' import { TaskTransformer } from '../../utils/task-transformer.js' import { exportAnnotations } from '../../utils/annotation-exporter.js' import { LabelStudioTask, TaskDataInput, TaskImportResult, LabelStudioAnnotation, ExportResult, ImportTasksWithTransformInput, ExportAnnotationsInput, AnnotationExportResult } from '../../types/label-studio-types.js' @Resolver() export class TaskManagement { /** * Get tasks for a project */ @Query(returns => [LabelStudioTask], { description: 'Get all tasks for a Label Studio project' }) @Directive('@privilege(category: "label-studio", privilege: "query")') async labelStudioTasks( @Arg('projectId', type => Int) projectId: number, @Arg('page', type => Int, { nullable: true, defaultValue: 1 }) page: number, @Arg('pageSize', type => Int, { nullable: true, defaultValue: 100 }) pageSize: number, @Ctx() context: ResolverContext ): Promise { try { const response = await labelStudioApi.getTasks(projectId, { page, page_size: pageSize }) const tasks = response.tasks || response.results || response return tasks.map((task: any) => ({ id: task.id, data: JSON.stringify(task.data), annotationCount: task.annotations?.length || task.total_annotations || 0, isCompleted: task.is_labeled || false, createdAt: task.created_at ? new Date(task.created_at) : undefined })) } catch (error) { console.error(`Failed to fetch tasks for project ${projectId}:`, error) throw new Error(`Failed to fetch tasks: ${error.message}`) } } /** * Get single task by ID */ @Query(returns => LabelStudioTask, { description: 'Get a single task by ID', nullable: true }) @Directive('@privilege(category: "label-studio", privilege: "query")') async labelStudioTask( @Arg('taskId', type => Int) taskId: number, @Ctx() context: ResolverContext ): Promise { try { const task = await labelStudioApi.getTask(taskId) if (!task) { return null } return { id: task.id, data: JSON.stringify(task.data), annotationCount: task.annotations?.length || task.total_annotations || 0, isCompleted: task.is_labeled || false, createdAt: task.created_at ? new Date(task.created_at) : undefined } } catch (error) { console.error(`Failed to fetch task ${taskId}:`, error) return null } } /** * Import tasks to Label Studio project */ @Mutation(returns => TaskImportResult, { description: 'Import tasks to a Label Studio project in bulk' }) @Directive('@privilege(category: "label-studio", privilege: "mutation")') async importTasksToLabelStudio( @Arg('projectId', type => Int) projectId: number, @Arg('tasks', type => [TaskDataInput]) tasks: TaskDataInput[], @Ctx() context: ResolverContext ): Promise { const errors: string[] = [] const taskIds: number[] = [] let imported = 0 let failed = 0 try { // Parse task data const parsedTasks = tasks.map(task => { try { return JSON.parse(task.data) } catch (e) { failed++ errors.push(`Invalid JSON in task data: ${task.data}`) return null } }) const validTasks = parsedTasks.filter(task => task !== null) if (validTasks.length === 0) { return { imported: 0, failed: tasks.length, taskIds: [], errors } } // Import tasks using Label Studio API const response = await labelStudioApi.importTasks(projectId, validTasks) // Parse response if (response.task_count !== undefined) { imported = response.task_count } else if (response.annotation_count !== undefined) { imported = response.annotation_count } else if (Array.isArray(response)) { imported = response.length taskIds.push(...response.map((t: any) => t.id)) } return { imported, failed, taskIds, errors: errors.length > 0 ? errors : undefined } } catch (error) { console.error(`Failed to import tasks to project ${projectId}:`, error) throw new Error(`Failed to import tasks: ${error.message}`) } } /** * Delete task */ @Mutation(returns => Boolean, { description: 'Delete a task from Label Studio' }) @Directive('@privilege(category: "label-studio", privilege: "mutation")') async deleteLabelStudioTask( @Arg('taskId', type => Int) taskId: number, @Ctx() context: ResolverContext ): Promise { try { await labelStudioApi.deleteTask(taskId) return true } catch (error) { console.error(`Failed to delete task ${taskId}:`, error) throw new Error(`Failed to delete task: ${error.message}`) } } /** * Get annotations for a task */ @Query(returns => [LabelStudioAnnotation], { description: 'Get all annotations for a task' }) @Directive('@privilege(category: "label-studio", privilege: "query")') async labelStudioTaskAnnotations( @Arg('taskId', type => Int) taskId: number, @Ctx() context: ResolverContext ): Promise { try { const annotations = await labelStudioApi.getAnnotations(taskId) return annotations.map((annotation: any) => ({ id: annotation.id, taskId: annotation.task || taskId, result: JSON.stringify(annotation.result), completedBy: annotation.completed_by?.email || 'unknown', createdAt: new Date(annotation.created_at), leadTime: annotation.lead_time || undefined })) } catch (error) { console.error(`Failed to fetch annotations for task ${taskId}:`, error) throw new Error(`Failed to fetch annotations: ${error.message}`) } } /** * Export annotations from project */ @Mutation(returns => ExportResult, { description: 'Export annotations from a Label Studio project' }) @Directive('@privilege(category: "label-studio", privilege: "query")') async exportLabelStudioAnnotations( @Arg('projectId', type => Int) projectId: number, @Arg('format', { defaultValue: 'JSON' }) format: 'JSON' | 'JSON_MIN' | 'CSV' | 'TSV' | 'CONLL2003' | 'COCO' | 'VOC' | 'YOLO', @Ctx() context: ResolverContext ): Promise { try { const exportData = await labelStudioApi.exportAnnotations(projectId, format) // For JSON exports, we get the data directly // For file exports, we might get a download URL const annotationCount = Array.isArray(exportData) ? exportData.length : 0 return { exportPath: JSON.stringify(exportData), // In practice, this would be a file path or URL annotationCount, format } } catch (error) { console.error(`Failed to export annotations from project ${projectId}:`, error) throw new Error(`Failed to export annotations: ${error.message}`) } } /** * Sync completed annotations to Things-Factory database * This allows storing annotations in TF for further processing */ @Mutation(returns => Int, { description: 'Sync completed annotations from Label Studio to Things-Factory database' }) @Directive('@privilege(category: "label-studio", privilege: "mutation")') async syncAnnotationsToDatabase( @Arg('projectId', type => Int) projectId: number, @Ctx() context: ResolverContext ): Promise { try { const { domain } = context.state // Export annotations const annotations = await labelStudioApi.exportAnnotations(projectId, 'JSON') // TODO: Store annotations in Things-Factory database // This would typically involve: // 1. Creating an Annotation entity // 2. Saving each annotation with project reference // 3. Linking to Things-Factory business objects if needed // For now, just return count const count = Array.isArray(annotations) ? annotations.length : 0 console.log(`Synced ${count} annotations from project ${projectId} to database`) return count } catch (error) { console.error(`Failed to sync annotations from project ${projectId}:`, error) throw new Error(`Failed to sync annotations: ${error.message}`) } } /** * Import tasks with flexible data transformation * Uses TaskTransformer for mapping source data to Label Studio format */ @Mutation(returns => TaskImportResult, { description: 'Import tasks to Label Studio with flexible data transformation' }) @Directive('@privilege(category: "label-studio", privilege: "mutation")') async importTasksWithTransform( @Arg('projectId', type => Int) projectId: number, @Arg('input') input: ImportTasksWithTransformInput, @Ctx() context: ResolverContext ): Promise { const errors: string[] = [] const taskIds: number[] = [] let imported = 0 let failed = 0 try { // Parse source data const sourceData = JSON.parse(input.sourceData) if (!Array.isArray(sourceData)) { throw new Error('Source data must be an array') } // Parse transform rule const dataFields = JSON.parse(input.transformRule.dataFields) const predictions = input.transformRule.predictions ? JSON.parse(input.transformRule.predictions) : undefined const meta = input.transformRule.meta ? JSON.parse(input.transformRule.meta) : undefined // Transform data using TaskTransformer const lsTasks = TaskTransformer.transform(sourceData, { dataFields, predictions, meta }) // Import to Label Studio const response = await labelStudioApi.importTasks(projectId, lsTasks) if (response.task_count) { imported = response.task_count taskIds.push(...(response.task_ids || [])) } return { imported, failed, taskIds, errors: errors.length > 0 ? errors : undefined } } catch (error) { console.error(`Failed to import tasks with transform for project ${projectId}:`, error) throw new Error(`Failed to import tasks: ${error.message}`) } } /** * Export annotations with flexible format conversion * Uses AnnotationExporter for custom format support */ @Mutation(returns => AnnotationExportResult, { description: 'Export annotations from Label Studio with flexible format conversion' }) @Directive('@privilege(category: "label-studio", privilege: "query")') async exportAnnotationsWithFormat( @Arg('projectId', type => Int) projectId: number, @Arg('input') input: ExportAnnotationsInput, @Ctx() context: ResolverContext ): Promise { try { // Get annotations from Label Studio let annotations = await labelStudioApi.exportAnnotations(projectId, 'JSON') // Filter by task IDs if specified if (input.taskIds) { const taskIds = JSON.parse(input.taskIds) as number[] annotations = annotations.filter((a: any) => taskIds.includes(a.task)) } // Export using custom format const exportedData = await exportAnnotations(annotations, input.format, { projectId, exportedAt: new Date().toISOString() }) // Convert to string if not already const dataString = typeof exportedData === 'string' ? exportedData : JSON.stringify(exportedData) return { data: dataString, count: annotations.length, format: input.format } } catch (error) { console.error(`Failed to export annotations for project ${projectId}:`, error) throw new Error(`Failed to export annotations: ${error.message}`) } } }