@things-factory/integration-label-studio 9.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/EXTERNAL_DATA_SOURCING.md +484 -0
  3. package/IMPLEMENTATION_GUIDE.md +469 -0
  4. package/INTEGRATION.md +279 -0
  5. package/README.md +1014 -0
  6. package/SETUP_GUIDE.md +577 -0
  7. package/TEST_GUIDE.md +387 -0
  8. package/UI_CUSTOMIZATION.md +395 -0
  9. package/USER_SYNC_GUIDE.md +514 -0
  10. package/client/bootstrap.ts +1 -0
  11. package/client/index.ts +1 -0
  12. package/client/label-studio-label-page.ts +52 -0
  13. package/client/label-studio-project-create.ts +216 -0
  14. package/client/label-studio-project-list.ts +214 -0
  15. package/client/label-studio-wrapper.ts +294 -0
  16. package/client/route.ts +15 -0
  17. package/client/tsconfig.json +13 -0
  18. package/config/config.development.js +124 -0
  19. package/config/config.production.js +182 -0
  20. package/dist-client/bootstrap.d.ts +1 -0
  21. package/dist-client/bootstrap.js +2 -0
  22. package/dist-client/bootstrap.js.map +1 -0
  23. package/dist-client/index.d.ts +1 -0
  24. package/dist-client/index.js +2 -0
  25. package/dist-client/index.js.map +1 -0
  26. package/dist-client/label-studio-label-page.d.ts +8 -0
  27. package/dist-client/label-studio-label-page.js +54 -0
  28. package/dist-client/label-studio-label-page.js.map +1 -0
  29. package/dist-client/label-studio-project-create.d.ts +16 -0
  30. package/dist-client/label-studio-project-create.js +235 -0
  31. package/dist-client/label-studio-project-create.js.map +1 -0
  32. package/dist-client/label-studio-project-list.d.ts +16 -0
  33. package/dist-client/label-studio-project-list.js +222 -0
  34. package/dist-client/label-studio-project-list.js.map +1 -0
  35. package/dist-client/label-studio-wrapper.d.ts +57 -0
  36. package/dist-client/label-studio-wrapper.js +304 -0
  37. package/dist-client/label-studio-wrapper.js.map +1 -0
  38. package/dist-client/route.d.ts +1 -0
  39. package/dist-client/route.js +14 -0
  40. package/dist-client/route.js.map +1 -0
  41. package/dist-client/tsconfig.tsbuildinfo +1 -0
  42. package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
  43. package/dist-server/controller/label-studio-role-mapper.js +65 -0
  44. package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
  45. package/dist-server/controller/user-provisioning-service.d.ts +66 -0
  46. package/dist-server/controller/user-provisioning-service.js +264 -0
  47. package/dist-server/controller/user-provisioning-service.js.map +1 -0
  48. package/dist-server/index.d.ts +7 -0
  49. package/dist-server/index.js +19 -0
  50. package/dist-server/index.js.map +1 -0
  51. package/dist-server/route/label-studio-sso.d.ts +2 -0
  52. package/dist-server/route/label-studio-sso.js +156 -0
  53. package/dist-server/route/label-studio-sso.js.map +1 -0
  54. package/dist-server/route/webhook.d.ts +65 -0
  55. package/dist-server/route/webhook.js +248 -0
  56. package/dist-server/route/webhook.js.map +1 -0
  57. package/dist-server/route.d.ts +1 -0
  58. package/dist-server/route.js +21 -0
  59. package/dist-server/route.js.map +1 -0
  60. package/dist-server/service/ai-prediction-service.d.ts +27 -0
  61. package/dist-server/service/ai-prediction-service.js +222 -0
  62. package/dist-server/service/ai-prediction-service.js.map +1 -0
  63. package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
  64. package/dist-server/service/dataset-labeling-integration.js +512 -0
  65. package/dist-server/service/dataset-labeling-integration.js.map +1 -0
  66. package/dist-server/service/external-data-source-service.d.ts +78 -0
  67. package/dist-server/service/external-data-source-service.js +415 -0
  68. package/dist-server/service/external-data-source-service.js.map +1 -0
  69. package/dist-server/service/index.d.ts +12 -0
  70. package/dist-server/service/index.js +27 -0
  71. package/dist-server/service/index.js.map +1 -0
  72. package/dist-server/service/label-studio-sso-service.d.ts +38 -0
  73. package/dist-server/service/label-studio-sso-service.js +98 -0
  74. package/dist-server/service/label-studio-sso-service.js.map +1 -0
  75. package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
  76. package/dist-server/service/ml/ml-backend-service.js +153 -0
  77. package/dist-server/service/ml/ml-backend-service.js.map +1 -0
  78. package/dist-server/service/prediction/prediction-management.d.ts +32 -0
  79. package/dist-server/service/prediction/prediction-management.js +299 -0
  80. package/dist-server/service/prediction/prediction-management.js.map +1 -0
  81. package/dist-server/service/project/project-management.d.ts +36 -0
  82. package/dist-server/service/project/project-management.js +309 -0
  83. package/dist-server/service/project/project-management.js.map +1 -0
  84. package/dist-server/service/task/task-management.d.ts +42 -0
  85. package/dist-server/service/task/task-management.js +372 -0
  86. package/dist-server/service/task/task-management.js.map +1 -0
  87. package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
  88. package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
  89. package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
  90. package/dist-server/service/webhook/webhook-management.d.ts +21 -0
  91. package/dist-server/service/webhook/webhook-management.js +134 -0
  92. package/dist-server/service/webhook/webhook-management.js.map +1 -0
  93. package/dist-server/tsconfig.tsbuildinfo +1 -0
  94. package/dist-server/types/dataset-labeling-types.d.ts +71 -0
  95. package/dist-server/types/dataset-labeling-types.js +259 -0
  96. package/dist-server/types/dataset-labeling-types.js.map +1 -0
  97. package/dist-server/types/label-studio-types.d.ts +128 -0
  98. package/dist-server/types/label-studio-types.js +494 -0
  99. package/dist-server/types/label-studio-types.js.map +1 -0
  100. package/dist-server/types/prediction-types.d.ts +39 -0
  101. package/dist-server/types/prediction-types.js +121 -0
  102. package/dist-server/types/prediction-types.js.map +1 -0
  103. package/dist-server/utils/annotation-exporter.d.ts +104 -0
  104. package/dist-server/utils/annotation-exporter.js +261 -0
  105. package/dist-server/utils/annotation-exporter.js.map +1 -0
  106. package/dist-server/utils/label-config-builder.d.ts +117 -0
  107. package/dist-server/utils/label-config-builder.js +286 -0
  108. package/dist-server/utils/label-config-builder.js.map +1 -0
  109. package/dist-server/utils/label-studio-api-client.d.ts +180 -0
  110. package/dist-server/utils/label-studio-api-client.js +401 -0
  111. package/dist-server/utils/label-studio-api-client.js.map +1 -0
  112. package/dist-server/utils/media-url-extractor.d.ts +45 -0
  113. package/dist-server/utils/media-url-extractor.js +152 -0
  114. package/dist-server/utils/media-url-extractor.js.map +1 -0
  115. package/dist-server/utils/task-transformer.d.ts +108 -0
  116. package/dist-server/utils/task-transformer.js +260 -0
  117. package/dist-server/utils/task-transformer.js.map +1 -0
  118. package/package.json +47 -0
  119. package/server/SERVER_STRUCTURE.md +351 -0
  120. package/server/controller/label-studio-role-mapper.ts +76 -0
  121. package/server/controller/user-provisioning-service.ts +340 -0
  122. package/server/index.ts +19 -0
  123. package/server/route/label-studio-sso.ts +194 -0
  124. package/server/route/webhook.ts +304 -0
  125. package/server/route.ts +35 -0
  126. package/server/service/ai-prediction-service.ts +239 -0
  127. package/server/service/dataset-labeling-integration.ts +590 -0
  128. package/server/service/external-data-source-service.ts +438 -0
  129. package/server/service/index.ts +24 -0
  130. package/server/service/label-studio-sso-service.ts +108 -0
  131. package/server/service/labeling-scenario-service.ts.deprecated +566 -0
  132. package/server/service/ml/ml-backend-service.ts +127 -0
  133. package/server/service/prediction/prediction-management.ts +281 -0
  134. package/server/service/project/project-management.ts +284 -0
  135. package/server/service/task/task-management.ts +363 -0
  136. package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
  137. package/server/service/webhook/webhook-management.ts +109 -0
  138. package/server/tsconfig.json +11 -0
  139. package/server/types/dataset-labeling-types.ts +181 -0
  140. package/server/types/global.d.ts +23 -0
  141. package/server/types/label-studio-types.ts +346 -0
  142. package/server/types/prediction-types.ts +86 -0
  143. package/server/types/scenario-types.ts.deprecated +362 -0
  144. package/server/utils/annotation-exporter.ts +340 -0
  145. package/server/utils/label-config-builder.ts +340 -0
  146. package/server/utils/label-studio-api-client.ts +487 -0
  147. package/server/utils/media-url-extractor.ts +193 -0
  148. package/server/utils/task-transformer.ts +342 -0
  149. package/test-ai-prediction.js +268 -0
  150. package/test-dataset-integration.js +449 -0
  151. package/test-simple.js +89 -0
  152. package/things-factory.config.js +12 -0
@@ -0,0 +1,281 @@
1
+ import { Resolver, Query, Mutation, Arg, Ctx, Int, Directive } from 'type-graphql'
2
+ import { labelStudioApi } from '../../utils/label-studio-api-client.js'
3
+ import {
4
+ LabelStudioPrediction,
5
+ PredictionInput,
6
+ PredictionImportResult,
7
+ BulkPredictionInput
8
+ } from '../../types/label-studio-types.js'
9
+
10
+ @Resolver()
11
+ export class PredictionManagement {
12
+ /**
13
+ * Get predictions for a task
14
+ */
15
+ @Query(returns => [LabelStudioPrediction], {
16
+ description: 'Get all predictions for a Label Studio task'
17
+ })
18
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
19
+ async labelStudioTaskPredictions(
20
+ @Arg('taskId', type => Int) taskId: number,
21
+ @Ctx() context: ResolverContext
22
+ ): Promise<LabelStudioPrediction[]> {
23
+ try {
24
+ const predictions = await labelStudioApi.getPredictions(taskId)
25
+
26
+ return predictions.map((prediction: any) => ({
27
+ id: prediction.id,
28
+ taskId: prediction.task || taskId,
29
+ result: JSON.stringify(prediction.result),
30
+ score: prediction.score || undefined,
31
+ modelVersion: prediction.model_version || undefined,
32
+ createdAt: prediction.created_at ? new Date(prediction.created_at) : undefined
33
+ }))
34
+ } catch (error) {
35
+ console.error(`Failed to fetch predictions for task ${taskId}:`, error)
36
+ throw new Error(`Failed to fetch predictions: ${error.message}`)
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get predictions for a project
42
+ */
43
+ @Query(returns => [LabelStudioPrediction], {
44
+ description: 'Get all predictions for a Label Studio project'
45
+ })
46
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
47
+ async labelStudioProjectPredictions(
48
+ @Arg('projectId', type => Int) projectId: number,
49
+ @Ctx() context: ResolverContext
50
+ ): Promise<LabelStudioPrediction[]> {
51
+ try {
52
+ const predictions = await labelStudioApi.getProjectPredictions(projectId)
53
+
54
+ return predictions.map((prediction: any) => ({
55
+ id: prediction.id,
56
+ taskId: prediction.task,
57
+ result: JSON.stringify(prediction.result),
58
+ score: prediction.score || undefined,
59
+ modelVersion: prediction.model_version || undefined,
60
+ createdAt: prediction.created_at ? new Date(prediction.created_at) : undefined
61
+ }))
62
+ } catch (error) {
63
+ console.error(`Failed to fetch predictions for project ${projectId}:`, error)
64
+ throw new Error(`Failed to fetch predictions: ${error.message}`)
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get single prediction by ID
70
+ */
71
+ @Query(returns => LabelStudioPrediction, {
72
+ description: 'Get a single prediction by ID',
73
+ nullable: true
74
+ })
75
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
76
+ async labelStudioPrediction(
77
+ @Arg('predictionId', type => Int) predictionId: number,
78
+ @Ctx() context: ResolverContext
79
+ ): Promise<LabelStudioPrediction | null> {
80
+ try {
81
+ const prediction = await labelStudioApi.getPrediction(predictionId)
82
+
83
+ if (!prediction) {
84
+ return null
85
+ }
86
+
87
+ return {
88
+ id: prediction.id,
89
+ taskId: prediction.task,
90
+ result: JSON.stringify(prediction.result),
91
+ score: prediction.score || undefined,
92
+ modelVersion: prediction.model_version || undefined,
93
+ createdAt: prediction.created_at ? new Date(prediction.created_at) : undefined
94
+ }
95
+ } catch (error) {
96
+ console.error(`Failed to fetch prediction ${predictionId}:`, error)
97
+ return null
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Create a prediction for a task
103
+ */
104
+ @Mutation(returns => LabelStudioPrediction, {
105
+ description: 'Create a prediction for a Label Studio task'
106
+ })
107
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
108
+ async createLabelStudioPrediction(
109
+ @Arg('input') input: PredictionInput,
110
+ @Ctx() context: ResolverContext
111
+ ): Promise<LabelStudioPrediction> {
112
+ try {
113
+ // Parse result JSON
114
+ const result = JSON.parse(input.result)
115
+
116
+ // Create prediction
117
+ const prediction = await labelStudioApi.createPrediction({
118
+ task: input.taskId,
119
+ result,
120
+ score: input.score,
121
+ model_version: input.modelVersion
122
+ })
123
+
124
+ return {
125
+ id: prediction.id,
126
+ taskId: prediction.task,
127
+ result: JSON.stringify(prediction.result),
128
+ score: prediction.score || undefined,
129
+ modelVersion: prediction.model_version || undefined,
130
+ createdAt: prediction.created_at ? new Date(prediction.created_at) : undefined
131
+ }
132
+ } catch (error) {
133
+ console.error(`Failed to create prediction for task ${input.taskId}:`, error)
134
+ throw new Error(`Failed to create prediction: ${error.message}`)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Create predictions in bulk
140
+ */
141
+ @Mutation(returns => PredictionImportResult, {
142
+ description: 'Create multiple predictions at once'
143
+ })
144
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
145
+ async createBulkPredictions(
146
+ @Arg('predictions', type => [BulkPredictionInput]) predictions: BulkPredictionInput[],
147
+ @Ctx() context: ResolverContext
148
+ ): Promise<PredictionImportResult> {
149
+ const errors: string[] = []
150
+ let created = 0
151
+ let failed = 0
152
+
153
+ try {
154
+ // Parse and validate predictions
155
+ const parsedPredictions = predictions.map((pred, index) => {
156
+ try {
157
+ return {
158
+ task: pred.taskId,
159
+ result: JSON.parse(pred.result),
160
+ score: pred.score,
161
+ model_version: pred.modelVersion
162
+ }
163
+ } catch (e) {
164
+ failed++
165
+ errors.push(`Invalid JSON in prediction ${index}: ${pred.result}`)
166
+ return null
167
+ }
168
+ })
169
+
170
+ const validPredictions = parsedPredictions.filter(p => p !== null)
171
+
172
+ if (validPredictions.length === 0) {
173
+ return {
174
+ created: 0,
175
+ failed: predictions.length,
176
+ errors
177
+ }
178
+ }
179
+
180
+ // Create predictions
181
+ const response = await labelStudioApi.createPredictions(validPredictions)
182
+ created = response.created || validPredictions.length
183
+
184
+ return {
185
+ created,
186
+ failed,
187
+ errors: errors.length > 0 ? errors : undefined
188
+ }
189
+ } catch (error) {
190
+ console.error('Failed to create bulk predictions:', error)
191
+ throw new Error(`Failed to create predictions: ${error.message}`)
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Import predictions for a project
197
+ * This is the recommended way to bulk import AI model predictions
198
+ */
199
+ @Mutation(returns => PredictionImportResult, {
200
+ description: 'Import predictions for a Label Studio project in bulk'
201
+ })
202
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
203
+ async importPredictionsToProject(
204
+ @Arg('projectId', type => Int) projectId: number,
205
+ @Arg('predictions', type => [BulkPredictionInput]) predictions: BulkPredictionInput[],
206
+ @Ctx() context: ResolverContext
207
+ ): Promise<PredictionImportResult> {
208
+ const errors: string[] = []
209
+ let created = 0
210
+ let failed = 0
211
+
212
+ try {
213
+ // Parse and validate predictions
214
+ const parsedPredictions = predictions.map((pred, index) => {
215
+ try {
216
+ return {
217
+ task: pred.taskId,
218
+ result: JSON.parse(pred.result),
219
+ score: pred.score,
220
+ model_version: pred.modelVersion
221
+ }
222
+ } catch (e) {
223
+ failed++
224
+ errors.push(`Invalid JSON in prediction ${index}: ${pred.result}`)
225
+ return null
226
+ }
227
+ })
228
+
229
+ const validPredictions = parsedPredictions.filter(p => p !== null)
230
+
231
+ if (validPredictions.length === 0) {
232
+ return {
233
+ created: 0,
234
+ failed: predictions.length,
235
+ errors
236
+ }
237
+ }
238
+
239
+ // Import predictions using project endpoint
240
+ const response = await labelStudioApi.importPredictions(projectId, validPredictions)
241
+
242
+ // Parse response
243
+ if (response.created !== undefined) {
244
+ created = response.created
245
+ } else if (response.task_count !== undefined) {
246
+ created = response.task_count
247
+ } else if (Array.isArray(response)) {
248
+ created = response.length
249
+ }
250
+
251
+ return {
252
+ created,
253
+ failed,
254
+ errors: errors.length > 0 ? errors : undefined
255
+ }
256
+ } catch (error) {
257
+ console.error(`Failed to import predictions to project ${projectId}:`, error)
258
+ throw new Error(`Failed to import predictions: ${error.message}`)
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Delete prediction
264
+ */
265
+ @Mutation(returns => Boolean, {
266
+ description: 'Delete a prediction from Label Studio'
267
+ })
268
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
269
+ async deleteLabelStudioPrediction(
270
+ @Arg('predictionId', type => Int) predictionId: number,
271
+ @Ctx() context: ResolverContext
272
+ ): Promise<boolean> {
273
+ try {
274
+ await labelStudioApi.deletePrediction(predictionId)
275
+ return true
276
+ } catch (error) {
277
+ console.error(`Failed to delete prediction ${predictionId}:`, error)
278
+ throw new Error(`Failed to delete prediction: ${error.message}`)
279
+ }
280
+ }
281
+ }
@@ -0,0 +1,284 @@
1
+ import { Resolver, Query, Mutation, Arg, Ctx, Int, Directive } from 'type-graphql'
2
+ import { labelStudioApi } from '../../utils/label-studio-api-client.js'
3
+ import { LabelConfigBuilder } from '../../utils/label-config-builder.js'
4
+ import {
5
+ LabelStudioProject,
6
+ CreateProjectInput,
7
+ CreateProjectWithSpecInput,
8
+ ProjectMetrics,
9
+ AnnotatorStats
10
+ } from '../../types/label-studio-types.js'
11
+
12
+ @Resolver()
13
+ export class ProjectManagement {
14
+ /**
15
+ * Get JWT token for Label Studio SSO
16
+ */
17
+ @Query(returns => String, {
18
+ description: 'Get JWT token for Label Studio authentication',
19
+ nullable: true
20
+ })
21
+ async labelStudioToken(@Ctx() context: ResolverContext): Promise<string | null> {
22
+ try {
23
+ const { user } = context.state
24
+ if (!user) {
25
+ return null
26
+ }
27
+
28
+ // Generate JWT token using User.sign() method
29
+ const token = await user.sign()
30
+ return token
31
+ } catch (error) {
32
+ console.error('Failed to generate Label Studio token:', error)
33
+ return null
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Get all Label Studio projects
39
+ */
40
+ @Query(returns => [LabelStudioProject], {
41
+ description: 'Get all Label Studio projects accessible to the user'
42
+ })
43
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
44
+ async labelStudioProjects(@Ctx() context: ResolverContext): Promise<LabelStudioProject[]> {
45
+ try {
46
+ const projects = await labelStudioApi.getProjects()
47
+
48
+ return projects.map(project => ({
49
+ id: project.id,
50
+ title: project.title,
51
+ description: project.description || '',
52
+ labelConfig: project.label_config || '',
53
+ expertInstruction: project.expert_instruction || '',
54
+ taskCount: project.task_number || 0,
55
+ completedTaskCount: project.finished_task_number || 0,
56
+ completionRate:
57
+ project.task_number > 0 ? (project.finished_task_number || 0) / project.task_number : 0,
58
+ createdAt: new Date(project.created_at),
59
+ updatedAt: project.updated_at ? new Date(project.updated_at) : new Date(project.created_at)
60
+ }))
61
+ } catch (error) {
62
+ console.error('Failed to fetch Label Studio projects:', error)
63
+ throw new Error(`Failed to fetch projects: ${error.message}`)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Get single Label Studio project by ID
69
+ */
70
+ @Query(returns => LabelStudioProject, {
71
+ description: 'Get a single Label Studio project by ID',
72
+ nullable: true
73
+ })
74
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
75
+ async labelStudioProject(
76
+ @Arg('projectId', type => Int) projectId: number,
77
+ @Ctx() context: ResolverContext
78
+ ): Promise<LabelStudioProject | null> {
79
+ try {
80
+ const project = await labelStudioApi.getProject(projectId)
81
+
82
+ if (!project) {
83
+ return null
84
+ }
85
+
86
+ return {
87
+ id: project.id,
88
+ title: project.title,
89
+ description: project.description || '',
90
+ labelConfig: project.label_config || '',
91
+ expertInstruction: project.expert_instruction || '',
92
+ taskCount: project.task_number || 0,
93
+ completedTaskCount: project.finished_task_number || 0,
94
+ completionRate:
95
+ project.task_number > 0 ? (project.finished_task_number || 0) / project.task_number : 0,
96
+ createdAt: new Date(project.created_at),
97
+ updatedAt: project.updated_at ? new Date(project.updated_at) : new Date(project.created_at)
98
+ }
99
+ } catch (error) {
100
+ console.error(`Failed to fetch Label Studio project ${projectId}:`, error)
101
+ return null
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Create new Label Studio project
107
+ */
108
+ @Mutation(returns => LabelStudioProject, {
109
+ description: 'Create a new Label Studio project'
110
+ })
111
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
112
+ async createLabelStudioProject(
113
+ @Arg('input') input: CreateProjectInput,
114
+ @Ctx() context: ResolverContext
115
+ ): Promise<LabelStudioProject> {
116
+ try {
117
+ const project = await labelStudioApi.createProject({
118
+ title: input.title,
119
+ description: input.description,
120
+ label_config: input.labelConfig,
121
+ expert_instruction: input.expertInstruction
122
+ })
123
+
124
+ return {
125
+ id: project.id,
126
+ title: project.title,
127
+ description: project.description || '',
128
+ labelConfig: project.label_config || '',
129
+ expertInstruction: project.expert_instruction || '',
130
+ taskCount: 0,
131
+ completedTaskCount: 0,
132
+ completionRate: 0,
133
+ createdAt: new Date(project.created_at),
134
+ updatedAt: new Date(project.updated_at)
135
+ }
136
+ } catch (error) {
137
+ console.error('Failed to create Label Studio project:', error)
138
+ throw new Error(`Failed to create project: ${error.message}`)
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Update Label Studio project
144
+ */
145
+ @Mutation(returns => LabelStudioProject, {
146
+ description: 'Update an existing Label Studio project'
147
+ })
148
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
149
+ async updateLabelStudioProject(
150
+ @Arg('projectId', type => Int) projectId: number,
151
+ @Arg('input') input: CreateProjectInput,
152
+ @Ctx() context: ResolverContext
153
+ ): Promise<LabelStudioProject> {
154
+ try {
155
+ const project = await labelStudioApi.updateProject(projectId, {
156
+ title: input.title,
157
+ description: input.description,
158
+ label_config: input.labelConfig,
159
+ expert_instruction: input.expertInstruction
160
+ })
161
+
162
+ return {
163
+ id: project.id,
164
+ title: project.title,
165
+ description: project.description || '',
166
+ labelConfig: project.label_config || '',
167
+ expertInstruction: project.expert_instruction || '',
168
+ taskCount: project.task_number || 0,
169
+ completedTaskCount: project.finished_task_number || 0,
170
+ completionRate:
171
+ project.task_number > 0 ? (project.finished_task_number || 0) / project.task_number : 0,
172
+ createdAt: new Date(project.created_at),
173
+ updatedAt: project.updated_at ? new Date(project.updated_at) : new Date(project.created_at)
174
+ }
175
+ } catch (error) {
176
+ console.error(`Failed to update Label Studio project ${projectId}:`, error)
177
+ throw new Error(`Failed to update project: ${error.message}`)
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Delete Label Studio project
183
+ */
184
+ @Mutation(returns => Boolean, {
185
+ description: 'Delete a Label Studio project'
186
+ })
187
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
188
+ async deleteLabelStudioProject(
189
+ @Arg('projectId', type => Int) projectId: number,
190
+ @Ctx() context: ResolverContext
191
+ ): Promise<boolean> {
192
+ try {
193
+ await labelStudioApi.deleteProject(projectId)
194
+ return true
195
+ } catch (error) {
196
+ console.error(`Failed to delete Label Studio project ${projectId}:`, error)
197
+ throw new Error(`Failed to delete project: ${error.message}`)
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Get project metrics and statistics
203
+ */
204
+ @Query(returns => ProjectMetrics, {
205
+ description: 'Get metrics and statistics for a Label Studio project'
206
+ })
207
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
208
+ async labelStudioProjectMetrics(
209
+ @Arg('projectId', type => Int) projectId: number,
210
+ @Ctx() context: ResolverContext
211
+ ): Promise<ProjectMetrics> {
212
+ try {
213
+ const stats = await labelStudioApi.getProjectStats(projectId)
214
+ const annotatorStats = await labelStudioApi.getAnnotatorStats(projectId)
215
+
216
+ return {
217
+ totalTasks: stats.totalTasks,
218
+ completedTasks: stats.completedTasks,
219
+ totalAnnotations: stats.totalAnnotations,
220
+ avgAnnotationsPerTask: stats.avgAnnotationsPerTask,
221
+ completionRate: stats.completionRate,
222
+ avgTimePerTask: null, // 계산 필요
223
+ annotatorStats: annotatorStats.map((stat: any) => ({
224
+ email: stat.email,
225
+ annotationCount: stat.annotationCount,
226
+ avgTime: stat.avgTime,
227
+ lastAnnotationDate: new Date(stat.lastAnnotationDate)
228
+ }))
229
+ }
230
+ } catch (error) {
231
+ console.error(`Failed to fetch project metrics for ${projectId}:`, error)
232
+ throw new Error(`Failed to fetch project metrics: ${error.message}`)
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Create project with flexible config specification
238
+ * Uses LabelConfigBuilder for declarative template generation
239
+ */
240
+ @Mutation(returns => LabelStudioProject, {
241
+ description: 'Create Label Studio project with flexible config specification'
242
+ })
243
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
244
+ async createLabelStudioProjectWithSpec(
245
+ @Arg('input') input: CreateProjectWithSpecInput,
246
+ @Ctx() context: ResolverContext
247
+ ): Promise<LabelStudioProject> {
248
+ try {
249
+ // Parse controls JSON
250
+ const controls = JSON.parse(input.labelConfigSpec.controls)
251
+
252
+ // Build label config using LabelConfigBuilder
253
+ const labelConfig = LabelConfigBuilder.build({
254
+ dataType: input.labelConfigSpec.dataType as any,
255
+ dataName: input.labelConfigSpec.dataName,
256
+ controls
257
+ })
258
+
259
+ // Create project
260
+ const project = await labelStudioApi.createProject({
261
+ title: input.title,
262
+ description: input.description,
263
+ label_config: labelConfig,
264
+ expert_instruction: input.expertInstruction
265
+ })
266
+
267
+ return {
268
+ id: project.id,
269
+ title: project.title,
270
+ description: project.description || '',
271
+ labelConfig: project.label_config || '',
272
+ expertInstruction: project.expert_instruction || '',
273
+ taskCount: 0,
274
+ completedTaskCount: 0,
275
+ completionRate: 0,
276
+ createdAt: new Date(project.created_at),
277
+ updatedAt: new Date(project.updated_at)
278
+ }
279
+ } catch (error) {
280
+ console.error('Failed to create Label Studio project with spec:', error)
281
+ throw new Error(`Failed to create project: ${error.message}`)
282
+ }
283
+ }
284
+ }