import axios, { AxiosInstance, AxiosRequestConfig } from 'axios' import { config } from '@things-factory/env' /** * Label Studio API Client * Provides methods to interact with Label Studio REST API */ export class LabelStudioApiClient { private client: AxiosInstance | null = null private serverUrl: string | null = null private apiToken: string | null = null constructor() { // Defer initialization until first use } private ensureInitialized() { if (this.client) { return } const labelStudioConfig = config.get('labelStudio', {}) this.serverUrl = labelStudioConfig.serverUrl || 'http://localhost:8080' this.apiToken = labelStudioConfig.apiToken || '' this.client = axios.create({ baseURL: this.serverUrl, headers: { Authorization: `Token ${this.apiToken}`, 'Content-Type': 'application/json' }, timeout: 30000 }) } // ============================================================================ // Project Methods // ============================================================================ /** * Get all projects */ async getProjects(): Promise { this.ensureInitialized() const response = await this.client!.get('/api/projects') return response.data.results || response.data } /** * Get project by ID */ async getProject(projectId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/projects/${projectId}`) return response.data } /** * Create new project */ async createProject(data: { title: string description?: string label_config: string expert_instruction?: string }): Promise { this.ensureInitialized() const response = await this.client!.post('/api/projects', data) return response.data } /** * Update project */ async updateProject(projectId: number, data: any): Promise { this.ensureInitialized() const response = await this.client!.patch(`/api/projects/${projectId}`, data) return response.data } /** * Delete project */ async deleteProject(projectId: number): Promise { this.ensureInitialized() await this.client!.delete(`/api/projects/${projectId}`) } // ============================================================================ // Task Methods // ============================================================================ /** * Get tasks for a project */ async getTasks(projectId: number, params?: { page?: number; page_size?: number }): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/projects/${projectId}/tasks`, { params }) return response.data } /** * Get single task */ async getTask(taskId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/tasks/${taskId}`) return response.data } /** * Create task */ async createTask(projectId: number, data: any): Promise { this.ensureInitialized() const response = await this.client!.post(`/api/projects/${projectId}/tasks`, data) return response.data } /** * Import tasks in bulk */ async importTasks(projectId: number, tasks: any[]): Promise { this.ensureInitialized() const response = await this.client!.post(`/api/projects/${projectId}/import`, tasks, { headers: { 'Content-Type': 'application/json' } }) return response.data } /** * Delete task */ async deleteTask(taskId: number): Promise { this.ensureInitialized() await this.client!.delete(`/api/tasks/${taskId}`) } // ============================================================================ // Annotation Methods // ============================================================================ /** * Get annotations for a task */ async getAnnotations(taskId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/tasks/${taskId}/annotations`) return response.data } /** * Create annotation */ async createAnnotation(taskId: number, annotation: any): Promise { this.ensureInitialized() const response = await this.client!.post(`/api/tasks/${taskId}/annotations`, annotation) return response.data } /** * Update annotation */ async updateAnnotation(annotationId: number, data: any): Promise { this.ensureInitialized() const response = await this.client!.patch(`/api/annotations/${annotationId}`, data) return response.data } /** * Delete annotation */ async deleteAnnotation(annotationId: number): Promise { this.ensureInitialized() await this.client!.delete(`/api/annotations/${annotationId}`) } // ============================================================================ // Prediction Methods // ============================================================================ /** * Get all predictions for a task */ async getPredictions(taskId: number): Promise { this.ensureInitialized() const response = await this.client!.get('/api/predictions', { params: { task: taskId } }) return response.data.results || response.data } /** * Get predictions for a project */ async getProjectPredictions(projectId: number): Promise { this.ensureInitialized() const response = await this.client!.get('/api/predictions', { params: { project: projectId } }) return response.data.results || response.data } /** * Get single prediction by ID */ async getPrediction(predictionId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/predictions/${predictionId}`) return response.data } /** * Create a prediction for a task */ async createPrediction(data: { task: number result: any[] score?: number model_version?: string }): Promise { this.ensureInitialized() const response = await this.client!.post('/api/predictions', data) return response.data } /** * Create predictions in bulk */ async createPredictions(predictions: Array<{ task: number result: any[] score?: number model_version?: string }>): Promise { this.ensureInitialized() const results = await Promise.all( predictions.map(prediction => this.createPrediction(prediction)) ) return { created: results.length, predictions: results } } /** * Update prediction */ async updatePrediction(predictionId: number, data: any): Promise { this.ensureInitialized() const response = await this.client!.patch(`/api/predictions/${predictionId}`, data) return response.data } /** * Delete prediction */ async deletePrediction(predictionId: number): Promise { this.ensureInitialized() await this.client!.delete(`/api/predictions/${predictionId}`) } /** * Import predictions for a project * This is useful for bulk importing AI model predictions */ async importPredictions(projectId: number, predictions: any[]): Promise { this.ensureInitialized() const response = await this.client!.post(`/api/projects/${projectId}/import/predictions`, predictions) return response.data } // ============================================================================ // Export Methods // ============================================================================ /** * Export annotations */ async exportAnnotations( projectId: number, format: 'JSON' | 'JSON_MIN' | 'CSV' | 'TSV' | 'CONLL2003' | 'COCO' | 'VOC' | 'YOLO' = 'JSON' ): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/projects/${projectId}/export`, { params: { exportType: format } }) return response.data } /** * Get export files */ async getExportFiles(projectId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/projects/${projectId}/exports`) return response.data } // ============================================================================ // ML Backend Methods // ============================================================================ /** * Get ML backends for project */ async getMLBackends(projectId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/ml`, { params: { project: projectId } }) return response.data.results || response.data } /** * Add ML backend to project */ async addMLBackend(data: { project: number url: string title: string is_interactive?: boolean }): Promise { this.ensureInitialized() const response = await this.client!.post('/api/ml', data) return response.data } /** * Delete ML backend */ async deleteMLBackend(mlBackendId: number): Promise { this.ensureInitialized() await this.client!.delete(`/api/ml/${mlBackendId}`) } /** * Trigger predictions for tasks */ async triggerPredictions(projectId: number, taskIds?: number[]): Promise { this.ensureInitialized() const payload = taskIds ? { task_ids: taskIds } : {} const response = await this.client!.post(`/api/projects/${projectId}/predict`, payload) return response.data } /** * Train ML model */ async trainModel(mlBackendId: number): Promise { this.ensureInitialized() const response = await this.client!.post(`/api/ml/${mlBackendId}/train`) return response.data } // ============================================================================ // Statistics Methods // ============================================================================ /** * Get project statistics */ async getProjectStats(projectId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/projects/${projectId}`) const project = response.data // Get detailed statistics const tasksResponse = await this.client!.get(`/api/projects/${projectId}/tasks`, { params: { page_size: 1 } }) return { totalTasks: project.task_number || 0, completedTasks: project.finished_task_number || 0, totalAnnotations: project.total_annotations_number || 0, avgAnnotationsPerTask: project.task_number > 0 ? (project.total_annotations_number || 0) / project.task_number : 0, completionRate: project.task_number > 0 ? (project.finished_task_number || 0) / project.task_number : 0 } } /** * Get annotator statistics */ async getAnnotatorStats(projectId: number): Promise { this.ensureInitialized() const response = await this.client!.get(`/api/projects/${projectId}/annotations`) const annotations = response.data.results || response.data // Group by annotator const statsByUser: { [email: string]: any } = {} for (const annotation of annotations) { const email = annotation.completed_by?.email || 'unknown' if (!statsByUser[email]) { statsByUser[email] = { email, annotationCount: 0, totalTime: 0, lastAnnotationDate: null } } statsByUser[email].annotationCount++ if (annotation.lead_time) { statsByUser[email].totalTime += annotation.lead_time } const annotationDate = new Date(annotation.created_at) if ( !statsByUser[email].lastAnnotationDate || annotationDate > new Date(statsByUser[email].lastAnnotationDate) ) { statsByUser[email].lastAnnotationDate = annotation.created_at } } return Object.values(statsByUser).map(stat => ({ ...stat, avgTime: stat.annotationCount > 0 ? stat.totalTime / stat.annotationCount : 0 })) } // ============================================================================ // Webhook Methods // ============================================================================ /** * Create webhook */ async createWebhook(data: { project: number url: string send_payload?: boolean send_for_all_actions?: boolean headers?: { [key: string]: string } }): Promise { this.ensureInitialized() const response = await this.client!.post('/api/webhooks', data) return response.data } /** * Get webhooks for project */ async getWebhooks(projectId: number): Promise { this.ensureInitialized() const response = await this.client!.get('/api/webhooks', { params: { project: projectId } }) return response.data.results || response.data } /** * Delete webhook */ async deleteWebhook(webhookId: number): Promise { this.ensureInitialized() await this.client!.delete(`/api/webhooks/${webhookId}`) } } // Singleton instance (lazy initialization) let _instance: LabelStudioApiClient | null = null function getLabelStudioApiClient(): LabelStudioApiClient { if (!_instance) { _instance = new LabelStudioApiClient() } return _instance } // Export singleton accessor export const labelStudioApi = new Proxy({} as LabelStudioApiClient, { get(_target, prop) { const instance = getLabelStudioApiClient() const value = (instance as any)[prop] if (typeof value === 'function') { return value.bind(instance) } return value } })