@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,566 @@
1
+ import { Resolver, Query, Mutation, Arg, Ctx, Int, Directive } from 'type-graphql'
2
+ import { v4 as uuidv4 } from 'uuid'
3
+ import {
4
+ CreateScenarioRequest,
5
+ ExecuteScenarioRequest,
6
+ LabelingScenario,
7
+ ScenarioExecution,
8
+ ScenarioExecutionResult,
9
+ ScenarioExecutionStep,
10
+ LabelingScenarioList,
11
+ ScenarioExecutionList,
12
+ ScenarioStatus,
13
+ ScenarioStepType,
14
+ TriggerType
15
+ } from '../types/scenario-types.js'
16
+ import { DatasetLabelingIntegration } from './dataset-labeling-integration.js'
17
+ import { ExternalDataSourceService } from './external-data-source-service.js'
18
+ import { LabelStudioAIPredictionService } from './ai-prediction-service.js'
19
+
20
+ /**
21
+ * Labeling Scenario Service
22
+ *
23
+ * Orchestrates end-to-end labeling workflows with multiple steps
24
+ *
25
+ * Features:
26
+ * - Define multi-step labeling workflows
27
+ * - Auto-execute workflows based on triggers
28
+ * - Track execution progress
29
+ * - Error handling and retry logic
30
+ * - Conditional execution
31
+ */
32
+ @Resolver()
33
+ export class LabelingScenarioService {
34
+ // In-memory storage (in production, use database)
35
+ private scenarios: Map<string, LabelingScenario> = new Map()
36
+ private executions: Map<string, ScenarioExecution> = new Map()
37
+
38
+ // Service dependencies (lazy initialization)
39
+ private datasetIntegration: DatasetLabelingIntegration | null = null
40
+ private externalDataSource: ExternalDataSourceService | null = null
41
+ private aiPredictionService: LabelStudioAIPredictionService | null = null
42
+
43
+ private getDatasetIntegration(): DatasetLabelingIntegration {
44
+ if (!this.datasetIntegration) {
45
+ this.datasetIntegration = new DatasetLabelingIntegration()
46
+ }
47
+ return this.datasetIntegration
48
+ }
49
+
50
+ private getExternalDataSource(): ExternalDataSourceService {
51
+ if (!this.externalDataSource) {
52
+ this.externalDataSource = new ExternalDataSourceService()
53
+ }
54
+ return this.externalDataSource
55
+ }
56
+
57
+ private getAIPredictionService(): LabelStudioAIPredictionService {
58
+ if (!this.aiPredictionService) {
59
+ this.aiPredictionService = new LabelStudioAIPredictionService()
60
+ }
61
+ return this.aiPredictionService
62
+ }
63
+
64
+ /**
65
+ * Create a new labeling scenario
66
+ */
67
+ @Mutation(returns => LabelingScenario, {
68
+ description: 'Create a new labeling workflow scenario'
69
+ })
70
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
71
+ async createLabelingScenario(
72
+ @Arg('input') input: CreateScenarioRequest,
73
+ @Ctx() context: ResolverContext
74
+ ): Promise<LabelingScenario> {
75
+ console.log(`[Scenario] Creating scenario: ${input.name}`)
76
+
77
+ const scenario: LabelingScenario = {
78
+ id: uuidv4(),
79
+ name: input.name,
80
+ description: input.description,
81
+ projectId: input.projectId,
82
+ triggerType: input.triggerType,
83
+ triggerConfig: input.triggerConfig,
84
+ steps: input.steps.map((step, index) => ({
85
+ name: step.name,
86
+ type: step.type,
87
+ config: step.config,
88
+ condition: step.condition,
89
+ continueOnError: step.continueOnError !== undefined ? step.continueOnError : true,
90
+ maxRetries: step.maxRetries,
91
+ order: index + 1
92
+ })),
93
+ status: input.autoStart ? ScenarioStatus.Active : ScenarioStatus.Draft,
94
+ createdAt: new Date(),
95
+ updatedAt: new Date()
96
+ }
97
+
98
+ this.scenarios.set(scenario.id, scenario)
99
+
100
+ console.log(`[Scenario] Created scenario ${scenario.id} with ${scenario.steps.length} steps`)
101
+
102
+ return scenario
103
+ }
104
+
105
+ /**
106
+ * Execute a scenario
107
+ */
108
+ @Mutation(returns => ScenarioExecutionResult, {
109
+ description: 'Execute a labeling workflow scenario'
110
+ })
111
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
112
+ async executeScenario(
113
+ @Arg('input') input: ExecuteScenarioRequest,
114
+ @Ctx() context: ResolverContext
115
+ ): Promise<ScenarioExecutionResult> {
116
+ const scenario = this.scenarios.get(input.scenarioId)
117
+
118
+ if (!scenario) {
119
+ throw new Error(`Scenario not found: ${input.scenarioId}`)
120
+ }
121
+
122
+ console.log(`[Scenario] Executing scenario: ${scenario.name} (${scenario.id})`)
123
+
124
+ const execution: ScenarioExecution = {
125
+ id: uuidv4(),
126
+ scenarioId: scenario.id,
127
+ scenarioName: scenario.name,
128
+ status: 'running',
129
+ steps: scenario.steps.map(step => ({
130
+ stepName: step.name,
131
+ stepType: step.type,
132
+ status: 'pending',
133
+ output: null,
134
+ error: null,
135
+ startedAt: null,
136
+ completedAt: null,
137
+ durationMs: null
138
+ })),
139
+ summary: null,
140
+ startedAt: new Date(),
141
+ completedAt: null,
142
+ totalDurationMs: null,
143
+ error: null
144
+ }
145
+
146
+ this.executions.set(execution.id, execution)
147
+
148
+ // Execute asynchronously
149
+ this.executeScenarioAsync(execution, scenario, context, input.parameters).catch(error => {
150
+ console.error(`[Scenario] Execution failed:`, error)
151
+ execution.status = 'failed'
152
+ execution.error = error.message
153
+ execution.completedAt = new Date()
154
+ })
155
+
156
+ return {
157
+ executionId: execution.id,
158
+ status: 'running',
159
+ summary: `Started execution of ${scenario.steps.length} steps`
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get scenario details
165
+ */
166
+ @Query(returns => LabelingScenario, {
167
+ description: 'Get labeling scenario details'
168
+ })
169
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
170
+ async labelingScenario(
171
+ @Arg('scenarioId') scenarioId: string,
172
+ @Ctx() context: ResolverContext
173
+ ): Promise<LabelingScenario> {
174
+ const scenario = this.scenarios.get(scenarioId)
175
+
176
+ if (!scenario) {
177
+ throw new Error(`Scenario not found: ${scenarioId}`)
178
+ }
179
+
180
+ return scenario
181
+ }
182
+
183
+ /**
184
+ * List all scenarios
185
+ */
186
+ @Query(returns => LabelingScenarioList, {
187
+ description: 'List all labeling scenarios'
188
+ })
189
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
190
+ async labelingScenarios(
191
+ @Arg('projectId', type => Int, { nullable: true }) projectId: number,
192
+ @Ctx() context: ResolverContext
193
+ ): Promise<LabelingScenarioList> {
194
+ let scenarios = Array.from(this.scenarios.values())
195
+
196
+ if (projectId) {
197
+ scenarios = scenarios.filter(s => s.projectId === projectId)
198
+ }
199
+
200
+ return {
201
+ items: scenarios,
202
+ total: scenarios.length
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Get execution status
208
+ */
209
+ @Query(returns => ScenarioExecution, {
210
+ description: 'Get scenario execution status'
211
+ })
212
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
213
+ async scenarioExecution(
214
+ @Arg('executionId') executionId: string,
215
+ @Ctx() context: ResolverContext
216
+ ): Promise<ScenarioExecution> {
217
+ const execution = this.executions.get(executionId)
218
+
219
+ if (!execution) {
220
+ throw new Error(`Execution not found: ${executionId}`)
221
+ }
222
+
223
+ return execution
224
+ }
225
+
226
+ /**
227
+ * List executions for a scenario
228
+ */
229
+ @Query(returns => ScenarioExecutionList, {
230
+ description: 'List executions for a scenario'
231
+ })
232
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
233
+ async scenarioExecutions(
234
+ @Arg('scenarioId') scenarioId: string,
235
+ @Ctx() context: ResolverContext
236
+ ): Promise<ScenarioExecutionList> {
237
+ const executions = Array.from(this.executions.values()).filter(e => e.scenarioId === scenarioId)
238
+
239
+ // Sort by startedAt descending
240
+ executions.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())
241
+
242
+ return {
243
+ items: executions,
244
+ total: executions.length
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Pause a scenario
250
+ */
251
+ @Mutation(returns => LabelingScenario, {
252
+ description: 'Pause a scenario'
253
+ })
254
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
255
+ async pauseScenario(
256
+ @Arg('scenarioId') scenarioId: string,
257
+ @Ctx() context: ResolverContext
258
+ ): Promise<LabelingScenario> {
259
+ const scenario = this.scenarios.get(scenarioId)
260
+
261
+ if (!scenario) {
262
+ throw new Error(`Scenario not found: ${scenarioId}`)
263
+ }
264
+
265
+ scenario.status = ScenarioStatus.Paused
266
+ scenario.updatedAt = new Date()
267
+
268
+ console.log(`[Scenario] Paused scenario: ${scenario.name}`)
269
+
270
+ return scenario
271
+ }
272
+
273
+ /**
274
+ * Resume a scenario
275
+ */
276
+ @Mutation(returns => LabelingScenario, {
277
+ description: 'Resume a paused scenario'
278
+ })
279
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
280
+ async resumeScenario(
281
+ @Arg('scenarioId') scenarioId: string,
282
+ @Ctx() context: ResolverContext
283
+ ): Promise<LabelingScenario> {
284
+ const scenario = this.scenarios.get(scenarioId)
285
+
286
+ if (!scenario) {
287
+ throw new Error(`Scenario not found: ${scenarioId}`)
288
+ }
289
+
290
+ scenario.status = ScenarioStatus.Active
291
+ scenario.updatedAt = new Date()
292
+
293
+ console.log(`[Scenario] Resumed scenario: ${scenario.name}`)
294
+
295
+ return scenario
296
+ }
297
+
298
+ // ============================================================================
299
+ // Private Methods - Scenario Execution Engine
300
+ // ============================================================================
301
+
302
+ private async executeScenarioAsync(
303
+ execution: ScenarioExecution,
304
+ scenario: LabelingScenario,
305
+ context: ResolverContext,
306
+ parametersJson?: string
307
+ ): Promise<void> {
308
+ const startTime = Date.now()
309
+ const parameters = parametersJson ? JSON.parse(parametersJson) : {}
310
+
311
+ console.log(`[Scenario] Starting execution ${execution.id}`)
312
+
313
+ try {
314
+ for (const [index, scenarioStep] of scenario.steps.entries()) {
315
+ const executionStep = execution.steps[index]
316
+ executionStep.status = 'running'
317
+ executionStep.startedAt = new Date()
318
+
319
+ console.log(`[Scenario] Executing step ${index + 1}/${scenario.steps.length}: ${scenarioStep.name}`)
320
+
321
+ try {
322
+ // Check condition if specified
323
+ if (scenarioStep.condition) {
324
+ const conditionMet = this.evaluateCondition(scenarioStep.condition, parameters)
325
+ if (!conditionMet) {
326
+ console.log(`[Scenario] Step ${scenarioStep.name} condition not met, skipping`)
327
+ executionStep.status = 'skipped'
328
+ executionStep.completedAt = new Date()
329
+ continue
330
+ }
331
+ }
332
+
333
+ // Execute step
334
+ const stepConfig = JSON.parse(scenarioStep.config)
335
+ const stepResult = await this.executeStep(
336
+ scenarioStep.type,
337
+ stepConfig,
338
+ scenario.projectId,
339
+ context,
340
+ parameters
341
+ )
342
+
343
+ executionStep.status = 'completed'
344
+ executionStep.output = JSON.stringify(stepResult)
345
+ executionStep.completedAt = new Date()
346
+ executionStep.durationMs = executionStep.completedAt.getTime() - executionStep.startedAt.getTime()
347
+
348
+ console.log(`[Scenario] Step ${scenarioStep.name} completed in ${executionStep.durationMs}ms`)
349
+
350
+ // Store step result in parameters for next steps
351
+ parameters[`step_${index}_result`] = stepResult
352
+ } catch (stepError) {
353
+ console.error(`[Scenario] Step ${scenarioStep.name} failed:`, stepError)
354
+
355
+ executionStep.status = 'failed'
356
+ executionStep.error = stepError.message
357
+ executionStep.completedAt = new Date()
358
+ executionStep.durationMs = executionStep.completedAt.getTime() - executionStep.startedAt.getTime()
359
+
360
+ // Check if we should continue on error
361
+ if (!scenarioStep.continueOnError) {
362
+ throw new Error(`Step ${scenarioStep.name} failed: ${stepError.message}`)
363
+ }
364
+
365
+ console.log(`[Scenario] Continuing despite error (continueOnError=true)`)
366
+ }
367
+ }
368
+
369
+ // All steps completed
370
+ execution.status = 'completed'
371
+ execution.summary = this.generateExecutionSummary(execution)
372
+ execution.completedAt = new Date()
373
+ execution.totalDurationMs = Date.now() - startTime
374
+
375
+ console.log(`[Scenario] Execution completed in ${execution.totalDurationMs}ms`)
376
+
377
+ // Update scenario status
378
+ scenario.lastExecutedAt = new Date()
379
+ if (scenario.triggerType === TriggerType.Schedule) {
380
+ // Calculate next execution time (simplified, needs proper cron parser)
381
+ // scenario.nextExecutionAt = ...
382
+ }
383
+ } catch (error) {
384
+ console.error(`[Scenario] Execution failed:`, error)
385
+
386
+ execution.status = 'failed'
387
+ execution.error = error.message
388
+ execution.summary = `Failed: ${error.message}`
389
+ execution.completedAt = new Date()
390
+ execution.totalDurationMs = Date.now() - startTime
391
+ }
392
+ }
393
+
394
+ private async executeStep(
395
+ stepType: ScenarioStepType,
396
+ config: any,
397
+ projectId: number,
398
+ context: ResolverContext,
399
+ parameters: any
400
+ ): Promise<any> {
401
+ switch (stepType) {
402
+ case ScenarioStepType.ImportData:
403
+ return await this.executeImportDataStep(config, projectId, context, parameters)
404
+
405
+ case ScenarioStepType.GeneratePredictions:
406
+ return await this.executeGeneratePredictionsStep(config, projectId, context, parameters)
407
+
408
+ case ScenarioStepType.WaitForAnnotations:
409
+ return await this.executeWaitForAnnotationsStep(config, projectId, context)
410
+
411
+ case ScenarioStepType.SyncAnnotations:
412
+ return await this.executeSyncAnnotationsStep(config, projectId, context, parameters)
413
+
414
+ case ScenarioStepType.ValidateQuality:
415
+ return await this.executeValidateQualityStep(config, projectId, context, parameters)
416
+
417
+ case ScenarioStepType.Notification:
418
+ return await this.executeNotificationStep(config, parameters)
419
+
420
+ default:
421
+ throw new Error(`Unknown step type: ${stepType}`)
422
+ }
423
+ }
424
+
425
+ private async executeImportDataStep(
426
+ config: any,
427
+ projectId: number,
428
+ context: ResolverContext,
429
+ parameters: any
430
+ ): Promise<any> {
431
+ if (config.sourceType === 'dataset') {
432
+ return await this.getDatasetIntegration().createLabelingTasksFromDataset(
433
+ {
434
+ projectId,
435
+ dataSetId: config.dataSetId,
436
+ imageField: config.imageField,
437
+ autoGeneratePredictions: config.autoGeneratePredictions || false,
438
+ limit: config.limit
439
+ },
440
+ context
441
+ )
442
+ } else {
443
+ return await this.getExternalDataSource().importFromExternalSource(
444
+ {
445
+ projectId,
446
+ source: {
447
+ sourceType: config.sourceType,
448
+ sourceUrl: config.sourceUrl,
449
+ authHeader: config.authHeader,
450
+ dataPath: config.dataPath
451
+ },
452
+ imageField: config.imageField,
453
+ limit: config.limit
454
+ },
455
+ context
456
+ )
457
+ }
458
+ }
459
+
460
+ private async executeGeneratePredictionsStep(
461
+ config: any,
462
+ projectId: number,
463
+ context: ResolverContext,
464
+ parameters: any
465
+ ): Promise<any> {
466
+ const dataSetId = parameters.dataSetId || config.dataSetId
467
+
468
+ if (!dataSetId) {
469
+ throw new Error('dataSetId required for GeneratePredictions step')
470
+ }
471
+
472
+ return await this.getDatasetIntegration().generatePredictionsForDataset(
473
+ {
474
+ dataSetId,
475
+ projectId,
476
+ modelId: config.modelId,
477
+ confidenceThreshold: config.confidenceThreshold,
478
+ forceRegenerate: config.forceRegenerate || false
479
+ },
480
+ context
481
+ )
482
+ }
483
+
484
+ private async executeWaitForAnnotationsStep(config: any, projectId: number, context: ResolverContext): Promise<any> {
485
+ // This is a simplified implementation
486
+ // In production, this should poll or use webhooks
487
+ console.log(`[Scenario] WaitForAnnotations step - checking criteria`)
488
+
489
+ // TODO: Implement actual waiting/polling logic
490
+ return {
491
+ status: 'waiting',
492
+ message: 'WaitForAnnotations step - manual check required'
493
+ }
494
+ }
495
+
496
+ private async executeSyncAnnotationsStep(
497
+ config: any,
498
+ projectId: number,
499
+ context: ResolverContext,
500
+ parameters: any
501
+ ): Promise<any> {
502
+ const dataSetId = parameters.dataSetId || config.dataSetId
503
+
504
+ if (!dataSetId) {
505
+ throw new Error('dataSetId required for SyncAnnotations step')
506
+ }
507
+
508
+ return await this.getDatasetIntegration().syncAnnotationsToDataset(
509
+ {
510
+ projectId,
511
+ dataSetId,
512
+ completedOnly: config.completedOnly !== false,
513
+ sinceDate: config.sinceDate
514
+ },
515
+ context
516
+ )
517
+ }
518
+
519
+ private async executeValidateQualityStep(
520
+ config: any,
521
+ projectId: number,
522
+ context: ResolverContext,
523
+ parameters: any
524
+ ): Promise<any> {
525
+ console.log(`[Scenario] ValidateQuality step`)
526
+
527
+ // TODO: Implement quality validation logic
528
+ return {
529
+ status: 'validated',
530
+ message: 'Quality validation - not yet implemented'
531
+ }
532
+ }
533
+
534
+ private async executeNotificationStep(config: any, parameters: any): Promise<any> {
535
+ console.log(`[Scenario] Notification step: ${config.message}`)
536
+
537
+ // TODO: Implement actual notification (email, webhook, etc.)
538
+ return {
539
+ status: 'sent',
540
+ message: config.message,
541
+ recipients: config.recipients
542
+ }
543
+ }
544
+
545
+ private evaluateCondition(condition: string, parameters: any): boolean {
546
+ // Simple condition evaluation
547
+ // In production, use a proper expression evaluator
548
+ try {
549
+ const conditionObj = JSON.parse(condition)
550
+ // Example: { "step_0_result.tasksCreated": { "$gt": 0 } }
551
+ // For now, always return true
552
+ return true
553
+ } catch (error) {
554
+ console.warn(`[Scenario] Failed to evaluate condition: ${condition}`)
555
+ return true
556
+ }
557
+ }
558
+
559
+ private generateExecutionSummary(execution: ScenarioExecution): string {
560
+ const completed = execution.steps.filter(s => s.status === 'completed').length
561
+ const failed = execution.steps.filter(s => s.status === 'failed').length
562
+ const skipped = execution.steps.filter(s => s.status === 'skipped').length
563
+
564
+ return `Completed ${completed}/${execution.steps.length} steps (${failed} failed, ${skipped} skipped)`
565
+ }
566
+ }
@@ -0,0 +1,127 @@
1
+ import { Resolver, Mutation, Query, Arg, Ctx, Int, Directive } from 'type-graphql'
2
+ import { labelStudioApi } from '../../utils/label-studio-api-client.js'
3
+ import { MLBackend, AddMLBackendInput } from '../../types/label-studio-types.js'
4
+
5
+ @Resolver()
6
+ export class MLBackendService {
7
+ /**
8
+ * Get ML backends for a project
9
+ */
10
+ @Query(returns => [MLBackend], {
11
+ description: 'Get all ML backends connected to a Label Studio project'
12
+ })
13
+ @Directive('@privilege(category: "label-studio", privilege: "query")')
14
+ async labelStudioMLBackends(
15
+ @Arg('projectId', type => Int) projectId: number,
16
+ @Ctx() context: ResolverContext
17
+ ): Promise<MLBackend[]> {
18
+ try {
19
+ const backends = await labelStudioApi.getMLBackends(projectId)
20
+
21
+ return backends.map(backend => ({
22
+ id: backend.id,
23
+ url: backend.url,
24
+ title: backend.title,
25
+ isInteractive: backend.is_interactive || false,
26
+ modelVersion: backend.model_version || 'unknown'
27
+ }))
28
+ } catch (error) {
29
+ console.error(`Failed to fetch ML backends for project ${projectId}:`, error)
30
+ throw new Error(`Failed to fetch ML backends: ${error.message}`)
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Add ML backend to project
36
+ */
37
+ @Mutation(returns => MLBackend, {
38
+ description: 'Add ML backend to Label Studio project for predictions and training'
39
+ })
40
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
41
+ async addMLBackendToProject(
42
+ @Arg('projectId', type => Int) projectId: number,
43
+ @Arg('input') input: AddMLBackendInput,
44
+ @Ctx() context: ResolverContext
45
+ ): Promise<MLBackend> {
46
+ try {
47
+ const backend = await labelStudioApi.addMLBackend({
48
+ project: projectId,
49
+ url: input.url,
50
+ title: input.title,
51
+ is_interactive: input.isInteractive || false
52
+ })
53
+
54
+ return {
55
+ id: backend.id,
56
+ url: backend.url,
57
+ title: backend.title,
58
+ isInteractive: backend.is_interactive || false,
59
+ modelVersion: backend.model_version || 'unknown'
60
+ }
61
+ } catch (error) {
62
+ console.error(`Failed to add ML backend to project ${projectId}:`, error)
63
+ throw new Error(`Failed to add ML backend: ${error.message}`)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Delete ML backend
69
+ */
70
+ @Mutation(returns => Boolean, {
71
+ description: 'Remove ML backend from project'
72
+ })
73
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
74
+ async deleteMLBackend(
75
+ @Arg('mlBackendId', type => Int) mlBackendId: number,
76
+ @Ctx() context: ResolverContext
77
+ ): Promise<boolean> {
78
+ try {
79
+ await labelStudioApi.deleteMLBackend(mlBackendId)
80
+ return true
81
+ } catch (error) {
82
+ console.error(`Failed to delete ML backend ${mlBackendId}:`, error)
83
+ throw new Error(`Failed to delete ML backend: ${error.message}`)
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Trigger predictions for tasks
89
+ */
90
+ @Mutation(returns => Boolean, {
91
+ description: 'Trigger ML predictions for tasks in a project'
92
+ })
93
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
94
+ async triggerLabelStudioPredictions(
95
+ @Arg('projectId', type => Int) projectId: number,
96
+ @Ctx() context: ResolverContext,
97
+ @Arg('taskIds', type => [Int], { nullable: true }) taskIds?: number[]
98
+ ): Promise<boolean> {
99
+ try {
100
+ await labelStudioApi.triggerPredictions(projectId, taskIds)
101
+ return true
102
+ } catch (error) {
103
+ console.error(`Failed to trigger predictions for project ${projectId}:`, error)
104
+ throw new Error(`Failed to trigger predictions: ${error.message}`)
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Train ML model
110
+ */
111
+ @Mutation(returns => Boolean, {
112
+ description: 'Trigger ML model training with current annotations'
113
+ })
114
+ @Directive('@privilege(category: "label-studio", privilege: "mutation")')
115
+ async trainLabelStudioModel(
116
+ @Arg('mlBackendId', type => Int) mlBackendId: number,
117
+ @Ctx() context: ResolverContext
118
+ ): Promise<boolean> {
119
+ try {
120
+ await labelStudioApi.trainModel(mlBackendId)
121
+ return true
122
+ } catch (error) {
123
+ console.error(`Failed to train model ${mlBackendId}:`, error)
124
+ throw new Error(`Failed to train model: ${error.message}`)
125
+ }
126
+ }
127
+ }