@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.
- package/CHANGELOG.md +85 -0
- package/EXTERNAL_DATA_SOURCING.md +484 -0
- package/IMPLEMENTATION_GUIDE.md +469 -0
- package/INTEGRATION.md +279 -0
- package/README.md +1014 -0
- package/SETUP_GUIDE.md +577 -0
- package/TEST_GUIDE.md +387 -0
- package/UI_CUSTOMIZATION.md +395 -0
- package/USER_SYNC_GUIDE.md +514 -0
- package/client/bootstrap.ts +1 -0
- package/client/index.ts +1 -0
- package/client/label-studio-label-page.ts +52 -0
- package/client/label-studio-project-create.ts +216 -0
- package/client/label-studio-project-list.ts +214 -0
- package/client/label-studio-wrapper.ts +294 -0
- package/client/route.ts +15 -0
- package/client/tsconfig.json +13 -0
- package/config/config.development.js +124 -0
- package/config/config.production.js +182 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/label-studio-label-page.d.ts +8 -0
- package/dist-client/label-studio-label-page.js +54 -0
- package/dist-client/label-studio-label-page.js.map +1 -0
- package/dist-client/label-studio-project-create.d.ts +16 -0
- package/dist-client/label-studio-project-create.js +235 -0
- package/dist-client/label-studio-project-create.js.map +1 -0
- package/dist-client/label-studio-project-list.d.ts +16 -0
- package/dist-client/label-studio-project-list.js +222 -0
- package/dist-client/label-studio-project-list.js.map +1 -0
- package/dist-client/label-studio-wrapper.d.ts +57 -0
- package/dist-client/label-studio-wrapper.js +304 -0
- package/dist-client/label-studio-wrapper.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +14 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/controller/label-studio-role-mapper.d.ts +35 -0
- package/dist-server/controller/label-studio-role-mapper.js +65 -0
- package/dist-server/controller/label-studio-role-mapper.js.map +1 -0
- package/dist-server/controller/user-provisioning-service.d.ts +66 -0
- package/dist-server/controller/user-provisioning-service.js +264 -0
- package/dist-server/controller/user-provisioning-service.js.map +1 -0
- package/dist-server/index.d.ts +7 -0
- package/dist-server/index.js +19 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/route/label-studio-sso.d.ts +2 -0
- package/dist-server/route/label-studio-sso.js +156 -0
- package/dist-server/route/label-studio-sso.js.map +1 -0
- package/dist-server/route/webhook.d.ts +65 -0
- package/dist-server/route/webhook.js +248 -0
- package/dist-server/route/webhook.js.map +1 -0
- package/dist-server/route.d.ts +1 -0
- package/dist-server/route.js +21 -0
- package/dist-server/route.js.map +1 -0
- package/dist-server/service/ai-prediction-service.d.ts +27 -0
- package/dist-server/service/ai-prediction-service.js +222 -0
- package/dist-server/service/ai-prediction-service.js.map +1 -0
- package/dist-server/service/dataset-labeling-integration.d.ts +44 -0
- package/dist-server/service/dataset-labeling-integration.js +512 -0
- package/dist-server/service/dataset-labeling-integration.js.map +1 -0
- package/dist-server/service/external-data-source-service.d.ts +78 -0
- package/dist-server/service/external-data-source-service.js +415 -0
- package/dist-server/service/external-data-source-service.js.map +1 -0
- package/dist-server/service/index.d.ts +12 -0
- package/dist-server/service/index.js +27 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/label-studio-sso-service.d.ts +38 -0
- package/dist-server/service/label-studio-sso-service.js +98 -0
- package/dist-server/service/label-studio-sso-service.js.map +1 -0
- package/dist-server/service/ml/ml-backend-service.d.ts +23 -0
- package/dist-server/service/ml/ml-backend-service.js +153 -0
- package/dist-server/service/ml/ml-backend-service.js.map +1 -0
- package/dist-server/service/prediction/prediction-management.d.ts +32 -0
- package/dist-server/service/prediction/prediction-management.js +299 -0
- package/dist-server/service/prediction/prediction-management.js.map +1 -0
- package/dist-server/service/project/project-management.d.ts +36 -0
- package/dist-server/service/project/project-management.js +309 -0
- package/dist-server/service/project/project-management.js.map +1 -0
- package/dist-server/service/task/task-management.d.ts +42 -0
- package/dist-server/service/task/task-management.js +372 -0
- package/dist-server/service/task/task-management.js.map +1 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.d.ts +28 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js +111 -0
- package/dist-server/service/user-provisioning/user-sync-mutation.js.map +1 -0
- package/dist-server/service/webhook/webhook-management.d.ts +21 -0
- package/dist-server/service/webhook/webhook-management.js +134 -0
- package/dist-server/service/webhook/webhook-management.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/dist-server/types/dataset-labeling-types.d.ts +71 -0
- package/dist-server/types/dataset-labeling-types.js +259 -0
- package/dist-server/types/dataset-labeling-types.js.map +1 -0
- package/dist-server/types/label-studio-types.d.ts +128 -0
- package/dist-server/types/label-studio-types.js +494 -0
- package/dist-server/types/label-studio-types.js.map +1 -0
- package/dist-server/types/prediction-types.d.ts +39 -0
- package/dist-server/types/prediction-types.js +121 -0
- package/dist-server/types/prediction-types.js.map +1 -0
- package/dist-server/utils/annotation-exporter.d.ts +104 -0
- package/dist-server/utils/annotation-exporter.js +261 -0
- package/dist-server/utils/annotation-exporter.js.map +1 -0
- package/dist-server/utils/label-config-builder.d.ts +117 -0
- package/dist-server/utils/label-config-builder.js +286 -0
- package/dist-server/utils/label-config-builder.js.map +1 -0
- package/dist-server/utils/label-studio-api-client.d.ts +180 -0
- package/dist-server/utils/label-studio-api-client.js +401 -0
- package/dist-server/utils/label-studio-api-client.js.map +1 -0
- package/dist-server/utils/media-url-extractor.d.ts +45 -0
- package/dist-server/utils/media-url-extractor.js +152 -0
- package/dist-server/utils/media-url-extractor.js.map +1 -0
- package/dist-server/utils/task-transformer.d.ts +108 -0
- package/dist-server/utils/task-transformer.js +260 -0
- package/dist-server/utils/task-transformer.js.map +1 -0
- package/package.json +47 -0
- package/server/SERVER_STRUCTURE.md +351 -0
- package/server/controller/label-studio-role-mapper.ts +76 -0
- package/server/controller/user-provisioning-service.ts +340 -0
- package/server/index.ts +19 -0
- package/server/route/label-studio-sso.ts +194 -0
- package/server/route/webhook.ts +304 -0
- package/server/route.ts +35 -0
- package/server/service/ai-prediction-service.ts +239 -0
- package/server/service/dataset-labeling-integration.ts +590 -0
- package/server/service/external-data-source-service.ts +438 -0
- package/server/service/index.ts +24 -0
- package/server/service/label-studio-sso-service.ts +108 -0
- package/server/service/labeling-scenario-service.ts.deprecated +566 -0
- package/server/service/ml/ml-backend-service.ts +127 -0
- package/server/service/prediction/prediction-management.ts +281 -0
- package/server/service/project/project-management.ts +284 -0
- package/server/service/task/task-management.ts +363 -0
- package/server/service/user-provisioning/user-sync-mutation.ts +80 -0
- package/server/service/webhook/webhook-management.ts +109 -0
- package/server/tsconfig.json +11 -0
- package/server/types/dataset-labeling-types.ts +181 -0
- package/server/types/global.d.ts +23 -0
- package/server/types/label-studio-types.ts +346 -0
- package/server/types/prediction-types.ts +86 -0
- package/server/types/scenario-types.ts.deprecated +362 -0
- package/server/utils/annotation-exporter.ts +340 -0
- package/server/utils/label-config-builder.ts +340 -0
- package/server/utils/label-studio-api-client.ts +487 -0
- package/server/utils/media-url-extractor.ts +193 -0
- package/server/utils/task-transformer.ts +342 -0
- package/test-ai-prediction.js +268 -0
- package/test-dataset-integration.js +449 -0
- package/test-simple.js +89 -0
- 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
|
+
}
|