@things-factory/mlops 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/README.md +341 -0
- package/client/bootstrap.ts +41 -0
- package/client/index.ts +2 -0
- package/client/pages/ml-deployment/ml-deployment-list-page.ts +46 -0
- package/client/pages/ml-model/ml-model-list-page.ts +46 -0
- package/client/pages/ml-pipeline/ml-pipeline-list-page.ts +46 -0
- package/client/pages/mlops-dashboard/mlops-dashboard-page.ts +90 -0
- package/client/route.ts +19 -0
- package/client/tsconfig.json +12 -0
- package/dist-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +40 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +2 -0
- package/dist-client/index.js +3 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/pages/ml-deployment/ml-deployment-list-page.d.ts +5 -0
- package/dist-client/pages/ml-deployment/ml-deployment-list-page.js +49 -0
- package/dist-client/pages/ml-deployment/ml-deployment-list-page.js.map +1 -0
- package/dist-client/pages/ml-model/ml-model-list-page.d.ts +5 -0
- package/dist-client/pages/ml-model/ml-model-list-page.js +49 -0
- package/dist-client/pages/ml-model/ml-model-list-page.js.map +1 -0
- package/dist-client/pages/ml-pipeline/ml-pipeline-list-page.d.ts +5 -0
- package/dist-client/pages/ml-pipeline/ml-pipeline-list-page.js +49 -0
- package/dist-client/pages/ml-pipeline/ml-pipeline-list-page.js.map +1 -0
- package/dist-client/pages/mlops-dashboard/mlops-dashboard-page.d.ts +5 -0
- package/dist-client/pages/mlops-dashboard/mlops-dashboard-page.js +93 -0
- package/dist-client/pages/mlops-dashboard/mlops-dashboard-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +17 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +2 -0
- package/dist-server/index.js +6 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/routes.d.ts +5 -0
- package/dist-server/routes.js +12 -0
- package/dist-server/routes.js.map +1 -0
- package/dist-server/service/index.d.ts +8 -0
- package/dist-server/service/index.js +31 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/ml-deployment/index.d.ts +3 -0
- package/dist-server/service/ml-deployment/index.js +8 -0
- package/dist-server/service/ml-deployment/index.js.map +1 -0
- package/dist-server/service/ml-deployment/ml-deployment.d.ts +48 -0
- package/dist-server/service/ml-deployment/ml-deployment.js +153 -0
- package/dist-server/service/ml-deployment/ml-deployment.js.map +1 -0
- package/dist-server/service/ml-model/index.d.ts +5 -0
- package/dist-server/service/ml-model/index.js +11 -0
- package/dist-server/service/ml-model/index.js.map +1 -0
- package/dist-server/service/ml-model/ml-model-mutation.d.ts +24 -0
- package/dist-server/service/ml-model/ml-model-mutation.js +177 -0
- package/dist-server/service/ml-model/ml-model-mutation.js.map +1 -0
- package/dist-server/service/ml-model/ml-model-query.d.ts +23 -0
- package/dist-server/service/ml-model/ml-model-query.js +123 -0
- package/dist-server/service/ml-model/ml-model-query.js.map +1 -0
- package/dist-server/service/ml-model/ml-model-types.d.ts +27 -0
- package/dist-server/service/ml-model/ml-model-types.js +105 -0
- package/dist-server/service/ml-model/ml-model-types.js.map +1 -0
- package/dist-server/service/ml-model/ml-model.d.ts +52 -0
- package/dist-server/service/ml-model/ml-model.js +161 -0
- package/dist-server/service/ml-model/ml-model.js.map +1 -0
- package/dist-server/service/ml-pipeline/index.d.ts +3 -0
- package/dist-server/service/ml-pipeline/index.js +8 -0
- package/dist-server/service/ml-pipeline/index.js.map +1 -0
- package/dist-server/service/ml-pipeline/ml-pipeline.d.ts +50 -0
- package/dist-server/service/ml-pipeline/ml-pipeline.js +163 -0
- package/dist-server/service/ml-pipeline/ml-pipeline.js.map +1 -0
- package/dist-server/service/ml-pipeline-run/index.d.ts +3 -0
- package/dist-server/service/ml-pipeline-run/index.js +8 -0
- package/dist-server/service/ml-pipeline-run/index.js.map +1 -0
- package/dist-server/service/ml-pipeline-run/ml-pipeline-run.d.ts +40 -0
- package/dist-server/service/ml-pipeline-run/ml-pipeline-run.js +136 -0
- package/dist-server/service/ml-pipeline-run/ml-pipeline-run.js.map +1 -0
- package/dist-server/tsconfig.json +11 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +52 -0
- package/server/index.ts +3 -0
- package/server/routes.ts +13 -0
- package/server/service/index.ts +29 -0
- package/server/service/ml-deployment/index.ts +6 -0
- package/server/service/ml-deployment/ml-deployment.ts +136 -0
- package/server/service/ml-model/index.ts +9 -0
- package/server/service/ml-model/ml-model-mutation.ts +171 -0
- package/server/service/ml-model/ml-model-query.ts +97 -0
- package/server/service/ml-model/ml-model-types.ts +71 -0
- package/server/service/ml-model/ml-model.ts +143 -0
- package/server/service/ml-pipeline/index.ts +6 -0
- package/server/service/ml-pipeline/ml-pipeline.ts +144 -0
- package/server/service/ml-pipeline-run/index.ts +6 -0
- package/server/service/ml-pipeline-run/ml-pipeline-run.ts +118 -0
- package/server/tsconfig.json +11 -0
- package/things-factory.config.js +13 -0
- package/translations/en.json +30 -0
- package/translations/ko.json +30 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@things-factory/mlops",
|
|
3
|
+
"version": "9.1.19",
|
|
4
|
+
"main": "dist-server/index.js",
|
|
5
|
+
"browser": "dist-client/index.js",
|
|
6
|
+
"things-factory": true,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Things-Factory Team",
|
|
9
|
+
"description": "MLOps module - ML lifecycle orchestration platform for Things-Factory",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"@things-factory:registry": "https://registry.npmjs.org"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/hatiolab/things-factory.git",
|
|
17
|
+
"directory": "packages/mlops"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npm run build:server && npm run build:client",
|
|
21
|
+
"copy:files:client": "copyfiles -e \"./client/**/*.{ts,js,json}\" -u 1 \"./client/**/*\" dist-client",
|
|
22
|
+
"copy:files:server": "copyfiles -e \"./server/**/*.{ts,js}\" -u 1 \"./server/**/*\" dist-server",
|
|
23
|
+
"build:client": "npm run copy:files:client && npx tsc --p ./client/tsconfig.json",
|
|
24
|
+
"build:server": "npm run copy:files:server && npx tsc --p ./server/tsconfig.json",
|
|
25
|
+
"clean:client": "npx rimraf dist-client",
|
|
26
|
+
"clean:server": "npx rimraf dist-server",
|
|
27
|
+
"clean": "npm run clean:server && npm run clean:client",
|
|
28
|
+
"migration:create": "node ../../node_modules/typeorm/cli.js migration:create ./server/migrations/migration"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@operato/app": "^9.0.0",
|
|
32
|
+
"@operato/data-grist": "^9.0.0",
|
|
33
|
+
"@operato/graphql": "^9.0.0",
|
|
34
|
+
"@operato/i18n": "^9.0.0",
|
|
35
|
+
"@operato/layout": "^9.0.0",
|
|
36
|
+
"@operato/shell": "^9.0.0",
|
|
37
|
+
"@things-factory/auth-base": "^9.1.19",
|
|
38
|
+
"@things-factory/integration-label-studio": "^9.1.19",
|
|
39
|
+
"@things-factory/integration-mlflow": "^9.1.19",
|
|
40
|
+
"@things-factory/shell": "^9.1.19"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"things-factory",
|
|
44
|
+
"mlops",
|
|
45
|
+
"machine-learning",
|
|
46
|
+
"ml-platform",
|
|
47
|
+
"model-management",
|
|
48
|
+
"mlflow",
|
|
49
|
+
"label-studio"
|
|
50
|
+
],
|
|
51
|
+
"gitHead": "078438034dbe19915108e89ff24024f7044a85a9"
|
|
52
|
+
}
|
package/server/index.ts
ADDED
package/server/routes.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MLOps Module Routes
|
|
3
|
+
*
|
|
4
|
+
* Register additional routes if needed (beyond GraphQL)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Example: Health check endpoint, webhooks, etc.
|
|
8
|
+
// import Koa from 'koa'
|
|
9
|
+
// import Router from 'koa-router'
|
|
10
|
+
|
|
11
|
+
// process.on('bootstrap-module-domain-private-route', (app: Koa, router: Router) => {
|
|
12
|
+
// // Add custom routes here
|
|
13
|
+
// })
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* EXPORT ENTITY TYPES */
|
|
2
|
+
export * from './ml-model/ml-model'
|
|
3
|
+
export * from './ml-pipeline/ml-pipeline'
|
|
4
|
+
export * from './ml-pipeline-run/ml-pipeline-run'
|
|
5
|
+
export * from './ml-deployment/ml-deployment'
|
|
6
|
+
|
|
7
|
+
/* IMPORT ENTITIES AND RESOLVERS */
|
|
8
|
+
import { entities as MLModelEntities, resolvers as MLModelResolvers } from './ml-model'
|
|
9
|
+
import { entities as MLPipelineEntities, resolvers as MLPipelineResolvers } from './ml-pipeline'
|
|
10
|
+
import { entities as MLPipelineRunEntities, resolvers as MLPipelineRunResolvers } from './ml-pipeline-run'
|
|
11
|
+
import { entities as MLDeploymentEntities, resolvers as MLDeploymentResolvers } from './ml-deployment'
|
|
12
|
+
|
|
13
|
+
export const entities = [
|
|
14
|
+
/* ENTITIES */
|
|
15
|
+
...MLModelEntities,
|
|
16
|
+
...MLPipelineEntities,
|
|
17
|
+
...MLPipelineRunEntities,
|
|
18
|
+
...MLDeploymentEntities
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
export const schema = {
|
|
22
|
+
resolverClasses: [
|
|
23
|
+
/* RESOLVER CLASSES */
|
|
24
|
+
...MLModelResolvers,
|
|
25
|
+
...MLPipelineResolvers,
|
|
26
|
+
...MLPipelineRunResolvers,
|
|
27
|
+
...MLDeploymentResolvers
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateDateColumn,
|
|
3
|
+
UpdateDateColumn,
|
|
4
|
+
DeleteDateColumn,
|
|
5
|
+
Entity,
|
|
6
|
+
Index,
|
|
7
|
+
Column,
|
|
8
|
+
RelationId,
|
|
9
|
+
ManyToOne,
|
|
10
|
+
PrimaryGeneratedColumn
|
|
11
|
+
} from 'typeorm'
|
|
12
|
+
import { ObjectType, Field, Int, ID, registerEnumType } from 'type-graphql'
|
|
13
|
+
import { Domain } from '@things-factory/shell'
|
|
14
|
+
import { User } from '@things-factory/auth-base'
|
|
15
|
+
import { MLModel } from '../ml-model/ml-model'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Deployment Status
|
|
19
|
+
*/
|
|
20
|
+
export enum DeploymentStatus {
|
|
21
|
+
DEPLOYING = 'DEPLOYING',
|
|
22
|
+
ACTIVE = 'ACTIVE',
|
|
23
|
+
FAILED = 'FAILED',
|
|
24
|
+
STOPPED = 'STOPPED'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
registerEnumType(DeploymentStatus, {
|
|
28
|
+
name: 'DeploymentStatus',
|
|
29
|
+
description: 'Model deployment status'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Deployment Environment
|
|
34
|
+
*/
|
|
35
|
+
export enum DeploymentEnvironment {
|
|
36
|
+
DEVELOPMENT = 'DEVELOPMENT',
|
|
37
|
+
STAGING = 'STAGING',
|
|
38
|
+
PRODUCTION = 'PRODUCTION'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
registerEnumType(DeploymentEnvironment, {
|
|
42
|
+
name: 'DeploymentEnvironment',
|
|
43
|
+
description: 'Deployment environment'
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ML Deployment Entity
|
|
48
|
+
*
|
|
49
|
+
* Tracks model deployments to various environments
|
|
50
|
+
*/
|
|
51
|
+
@Entity('ml_deployments')
|
|
52
|
+
@Index('ix_ml_deployment_0', ['domain', 'model'])
|
|
53
|
+
@Index('ix_ml_deployment_1', ['environment', 'status'])
|
|
54
|
+
@ObjectType({ description: 'ML Model deployment configuration' })
|
|
55
|
+
export class MLDeployment {
|
|
56
|
+
@PrimaryGeneratedColumn('uuid')
|
|
57
|
+
@Field(type => ID, { description: 'Deployment ID' })
|
|
58
|
+
id: string
|
|
59
|
+
|
|
60
|
+
@ManyToOne(() => MLModel)
|
|
61
|
+
@Field(type => MLModel, { description: 'Deployed model' })
|
|
62
|
+
model: MLModel
|
|
63
|
+
|
|
64
|
+
@RelationId((deployment: MLDeployment) => deployment.model)
|
|
65
|
+
modelId: string
|
|
66
|
+
|
|
67
|
+
@Column({ nullable: true, comment: 'Deployment name' })
|
|
68
|
+
@Field({ description: 'Deployment name' })
|
|
69
|
+
name: string
|
|
70
|
+
|
|
71
|
+
@Column({ type: 'varchar', default: DeploymentEnvironment.DEVELOPMENT, comment: 'Environment' })
|
|
72
|
+
@Field(type => DeploymentEnvironment, { description: 'Deployment environment' })
|
|
73
|
+
environment: DeploymentEnvironment
|
|
74
|
+
|
|
75
|
+
@Column({ type: 'varchar', default: DeploymentStatus.DEPLOYING, comment: 'Status' })
|
|
76
|
+
@Field(type => DeploymentStatus, { description: 'Current deployment status' })
|
|
77
|
+
status: DeploymentStatus
|
|
78
|
+
|
|
79
|
+
@Column({ nullable: true, comment: 'Endpoint URL' })
|
|
80
|
+
@Field({ nullable: true, description: 'API endpoint URL' })
|
|
81
|
+
endpoint?: string
|
|
82
|
+
|
|
83
|
+
@Column({ type: 'simple-json', nullable: true, comment: 'Scaling configuration' })
|
|
84
|
+
@Field(type => String, { nullable: true, description: 'Scaling config as JSON' })
|
|
85
|
+
scalingConfig?: string
|
|
86
|
+
|
|
87
|
+
@Column({ type: 'simple-json', nullable: true, comment: 'Resource allocation' })
|
|
88
|
+
@Field(type => String, { nullable: true, description: 'Resource config as JSON' })
|
|
89
|
+
resources?: string
|
|
90
|
+
|
|
91
|
+
@Column({ type: 'timestamp', nullable: true, comment: 'Deployment time' })
|
|
92
|
+
@Field({ nullable: true, description: 'Deployed at' })
|
|
93
|
+
deployedAt?: Date
|
|
94
|
+
|
|
95
|
+
@Column({ type: 'int', default: 0, comment: 'Total prediction count' })
|
|
96
|
+
@Field(type => Int, { description: 'Total predictions served' })
|
|
97
|
+
predictionCount: number
|
|
98
|
+
|
|
99
|
+
@Column({ type: 'float', nullable: true, comment: 'Average latency (ms)' })
|
|
100
|
+
@Field({ nullable: true, description: 'Average latency in ms' })
|
|
101
|
+
avgLatency?: number
|
|
102
|
+
|
|
103
|
+
@Column({ type: 'float', nullable: true, comment: 'Error rate' })
|
|
104
|
+
@Field({ nullable: true, description: 'Error rate (0-1)' })
|
|
105
|
+
errorRate?: number
|
|
106
|
+
|
|
107
|
+
@Column({ type: 'simple-json', nullable: true, comment: 'Deployment metadata' })
|
|
108
|
+
@Field(type => String, { nullable: true, description: 'Metadata as JSON' })
|
|
109
|
+
metadata?: string
|
|
110
|
+
|
|
111
|
+
@ManyToOne(() => Domain)
|
|
112
|
+
@Field({ nullable: true, description: 'Domain' })
|
|
113
|
+
domain?: Domain
|
|
114
|
+
|
|
115
|
+
@RelationId((deployment: MLDeployment) => deployment.domain)
|
|
116
|
+
domainId?: string
|
|
117
|
+
|
|
118
|
+
@ManyToOne(() => User, { nullable: true })
|
|
119
|
+
@Field(type => User, { nullable: true, description: 'Deployed by' })
|
|
120
|
+
deployedBy?: User
|
|
121
|
+
|
|
122
|
+
@RelationId((deployment: MLDeployment) => deployment.deployedBy)
|
|
123
|
+
deployedById?: string
|
|
124
|
+
|
|
125
|
+
@CreateDateColumn()
|
|
126
|
+
@Field({ description: 'Created at' })
|
|
127
|
+
createdAt: Date
|
|
128
|
+
|
|
129
|
+
@UpdateDateColumn()
|
|
130
|
+
@Field({ description: 'Updated at' })
|
|
131
|
+
updatedAt: Date
|
|
132
|
+
|
|
133
|
+
@DeleteDateColumn()
|
|
134
|
+
@Field({ nullable: true, description: 'Deleted at' })
|
|
135
|
+
deletedAt?: Date
|
|
136
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MLModel } from './ml-model'
|
|
2
|
+
import { MLModelQuery } from './ml-model-query'
|
|
3
|
+
import { MLModelMutation } from './ml-model-mutation'
|
|
4
|
+
|
|
5
|
+
export const entities = [MLModel]
|
|
6
|
+
export const resolvers = [MLModelQuery, MLModelMutation]
|
|
7
|
+
|
|
8
|
+
// export * from './ml-model'
|
|
9
|
+
// export * from './ml-model-types'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
|
|
2
|
+
import { getRepository } from 'typeorm'
|
|
3
|
+
import { MLModel, ModelStage } from './ml-model'
|
|
4
|
+
import { RegisterMLModelInput, UpdateMLModelInput, PromoteModelInput } from './ml-model-types'
|
|
5
|
+
|
|
6
|
+
@Resolver(MLModel)
|
|
7
|
+
export class MLModelMutation {
|
|
8
|
+
/**
|
|
9
|
+
* Register new ML model
|
|
10
|
+
*/
|
|
11
|
+
@Mutation(returns => MLModel, {
|
|
12
|
+
description: 'Register new ML model in the system'
|
|
13
|
+
})
|
|
14
|
+
@Directive('@privilege(category: "mlops", privilege: "mutation")')
|
|
15
|
+
async registerMLModel(@Arg('input') input: RegisterMLModelInput, @Ctx() context: ResolverContext): Promise<MLModel> {
|
|
16
|
+
const { domain, user } = context.state
|
|
17
|
+
|
|
18
|
+
// Check if model with same name and version exists
|
|
19
|
+
const existing = await getRepository(MLModel).findOne({
|
|
20
|
+
where: {
|
|
21
|
+
domain: { id: domain.id },
|
|
22
|
+
name: input.name,
|
|
23
|
+
version: input.version
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (existing) {
|
|
28
|
+
throw new Error(`Model ${input.name} version ${input.version} already exists`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const model = getRepository(MLModel).create({
|
|
32
|
+
...input,
|
|
33
|
+
stage: input.stage || ModelStage.NONE,
|
|
34
|
+
domain,
|
|
35
|
+
creator: user,
|
|
36
|
+
updater: user
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return await getRepository(MLModel).save(model)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update ML model
|
|
44
|
+
*/
|
|
45
|
+
@Mutation(returns => MLModel, {
|
|
46
|
+
description: 'Update ML model information'
|
|
47
|
+
})
|
|
48
|
+
@Directive('@privilege(category: "mlops", privilege: "mutation")')
|
|
49
|
+
async updateMLModel(
|
|
50
|
+
@Arg('id') id: string,
|
|
51
|
+
@Arg('input') input: UpdateMLModelInput,
|
|
52
|
+
@Ctx() context: ResolverContext
|
|
53
|
+
): Promise<MLModel> {
|
|
54
|
+
const { domain, user } = context.state
|
|
55
|
+
|
|
56
|
+
const model = await getRepository(MLModel).findOne({
|
|
57
|
+
where: { id, domain: { id: domain.id } }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (!model) {
|
|
61
|
+
throw new Error(`Model ${id} not found`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Object.assign(model, input, { updater: user })
|
|
65
|
+
|
|
66
|
+
return await getRepository(MLModel).save(model)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Promote model to target stage
|
|
71
|
+
*/
|
|
72
|
+
@Mutation(returns => MLModel, {
|
|
73
|
+
description: 'Promote model to target deployment stage (Staging/Production)'
|
|
74
|
+
})
|
|
75
|
+
@Directive('@privilege(category: "mlops", privilege: "mutation")')
|
|
76
|
+
async promoteMLModel(@Arg('input') input: PromoteModelInput, @Ctx() context: ResolverContext): Promise<MLModel> {
|
|
77
|
+
const { domain, user } = context.state
|
|
78
|
+
|
|
79
|
+
const model = await getRepository(MLModel).findOne({
|
|
80
|
+
where: { id: input.modelId, domain: { id: domain.id } }
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
if (!model) {
|
|
84
|
+
throw new Error(`Model ${input.modelId} not found`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate stage transition
|
|
88
|
+
if (model.stage === ModelStage.ARCHIVED) {
|
|
89
|
+
throw new Error('Cannot promote archived model')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (input.targetStage === ModelStage.NONE) {
|
|
93
|
+
throw new Error('Cannot promote to None stage')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If promoting to Production, demote other Production models with same name
|
|
97
|
+
if (input.targetStage === ModelStage.PRODUCTION) {
|
|
98
|
+
await getRepository(MLModel).update(
|
|
99
|
+
{
|
|
100
|
+
domain: { id: domain.id },
|
|
101
|
+
name: model.name,
|
|
102
|
+
stage: ModelStage.PRODUCTION
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
stage: ModelStage.STAGING
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
model.stage = input.targetStage
|
|
111
|
+
model.updater = user
|
|
112
|
+
|
|
113
|
+
return await getRepository(MLModel).save(model)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Archive ML model
|
|
118
|
+
*/
|
|
119
|
+
@Mutation(returns => Boolean, {
|
|
120
|
+
description: 'Archive ML model (soft delete)'
|
|
121
|
+
})
|
|
122
|
+
@Directive('@privilege(category: "mlops", privilege: "mutation")')
|
|
123
|
+
async archiveMLModel(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
|
|
124
|
+
const { domain } = context.state
|
|
125
|
+
|
|
126
|
+
const model = await getRepository(MLModel).findOne({
|
|
127
|
+
where: { id, domain: { id: domain.id } }
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if (!model) {
|
|
131
|
+
throw new Error(`Model ${id} not found`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (model.stage === ModelStage.PRODUCTION) {
|
|
135
|
+
throw new Error('Cannot archive production model. Demote it first.')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
model.stage = ModelStage.ARCHIVED
|
|
139
|
+
|
|
140
|
+
await getRepository(MLModel).save(model)
|
|
141
|
+
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delete ML model
|
|
147
|
+
*/
|
|
148
|
+
@Mutation(returns => Boolean, {
|
|
149
|
+
description: 'Delete ML model permanently'
|
|
150
|
+
})
|
|
151
|
+
@Directive('@privilege(category: "mlops", privilege: "mutation")')
|
|
152
|
+
async deleteMLModel(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
|
|
153
|
+
const { domain } = context.state
|
|
154
|
+
|
|
155
|
+
const model = await getRepository(MLModel).findOne({
|
|
156
|
+
where: { id, domain: { id: domain.id } }
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (!model) {
|
|
160
|
+
throw new Error(`Model ${id} not found`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (model.stage === ModelStage.PRODUCTION) {
|
|
164
|
+
throw new Error('Cannot delete production model')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await getRepository(MLModel).softDelete({ id })
|
|
168
|
+
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Resolver, Query, Arg, Ctx, Directive } from 'type-graphql'
|
|
2
|
+
import { getRepository, In } from 'typeorm'
|
|
3
|
+
import { MLModel, ModelStage } from './ml-model'
|
|
4
|
+
|
|
5
|
+
@Resolver(MLModel)
|
|
6
|
+
export class MLModelQuery {
|
|
7
|
+
/**
|
|
8
|
+
* Get all ML models
|
|
9
|
+
*/
|
|
10
|
+
@Query(returns => [MLModel], {
|
|
11
|
+
description: 'Get all ML models in the system'
|
|
12
|
+
})
|
|
13
|
+
@Directive('@privilege(category: "mlops", privilege: "query")')
|
|
14
|
+
async mlModels(@Ctx() context: ResolverContext): Promise<MLModel[]> {
|
|
15
|
+
const { domain } = context.state
|
|
16
|
+
|
|
17
|
+
return await getRepository(MLModel).find({
|
|
18
|
+
where: { domain: { id: domain.id } },
|
|
19
|
+
order: { createdAt: 'DESC' },
|
|
20
|
+
relations: ['creator', 'updater']
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get ML model by ID
|
|
26
|
+
*/
|
|
27
|
+
@Query(returns => MLModel, {
|
|
28
|
+
description: 'Get ML model by ID',
|
|
29
|
+
nullable: true
|
|
30
|
+
})
|
|
31
|
+
@Directive('@privilege(category: "mlops", privilege: "query")')
|
|
32
|
+
async mlModel(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<MLModel | undefined> {
|
|
33
|
+
const { domain } = context.state
|
|
34
|
+
|
|
35
|
+
return await getRepository(MLModel).findOne({
|
|
36
|
+
where: { id, domain: { id: domain.id } },
|
|
37
|
+
relations: ['creator', 'updater']
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get models by stage
|
|
43
|
+
*/
|
|
44
|
+
@Query(returns => [MLModel], {
|
|
45
|
+
description: 'Get models by deployment stage'
|
|
46
|
+
})
|
|
47
|
+
@Directive('@privilege(category: "mlops", privilege: "query")')
|
|
48
|
+
async mlModelsByStage(
|
|
49
|
+
@Arg('stage', type => ModelStage) stage: ModelStage,
|
|
50
|
+
@Ctx() context: ResolverContext
|
|
51
|
+
): Promise<MLModel[]> {
|
|
52
|
+
const { domain } = context.state
|
|
53
|
+
|
|
54
|
+
return await getRepository(MLModel).find({
|
|
55
|
+
where: { domain: { id: domain.id }, stage },
|
|
56
|
+
order: { updatedAt: 'DESC' },
|
|
57
|
+
relations: ['creator', 'updater']
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get models by name (all versions)
|
|
63
|
+
*/
|
|
64
|
+
@Query(returns => [MLModel], {
|
|
65
|
+
description: 'Get all versions of a model by name'
|
|
66
|
+
})
|
|
67
|
+
@Directive('@privilege(category: "mlops", privilege: "query")')
|
|
68
|
+
async mlModelVersions(@Arg('name') name: string, @Ctx() context: ResolverContext): Promise<MLModel[]> {
|
|
69
|
+
const { domain } = context.state
|
|
70
|
+
|
|
71
|
+
return await getRepository(MLModel).find({
|
|
72
|
+
where: { domain: { id: domain.id }, name },
|
|
73
|
+
order: { createdAt: 'DESC' },
|
|
74
|
+
relations: ['creator', 'updater']
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get models by Label Studio project
|
|
80
|
+
*/
|
|
81
|
+
@Query(returns => [MLModel], {
|
|
82
|
+
description: 'Get models associated with Label Studio project'
|
|
83
|
+
})
|
|
84
|
+
@Directive('@privilege(category: "mlops", privilege: "query")')
|
|
85
|
+
async mlModelsByLabelStudioProject(
|
|
86
|
+
@Arg('projectId', type => Number) projectId: number,
|
|
87
|
+
@Ctx() context: ResolverContext
|
|
88
|
+
): Promise<MLModel[]> {
|
|
89
|
+
const { domain } = context.state
|
|
90
|
+
|
|
91
|
+
return await getRepository(MLModel).find({
|
|
92
|
+
where: { domain: { id: domain.id }, labelStudioProjectId: projectId },
|
|
93
|
+
order: { createdAt: 'DESC' },
|
|
94
|
+
relations: ['creator', 'updater']
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { InputType, Field, Int } from 'type-graphql'
|
|
2
|
+
import { ModelStage, ModelFramework } from './ml-model'
|
|
3
|
+
|
|
4
|
+
@InputType({ description: 'Input for registering ML model' })
|
|
5
|
+
export class RegisterMLModelInput {
|
|
6
|
+
@Field({ description: 'Model name' })
|
|
7
|
+
name: string
|
|
8
|
+
|
|
9
|
+
@Field({ description: 'Model version' })
|
|
10
|
+
version: string
|
|
11
|
+
|
|
12
|
+
@Field({ nullable: true, description: 'Model description' })
|
|
13
|
+
description?: string
|
|
14
|
+
|
|
15
|
+
@Field(type => ModelStage, { nullable: true, description: 'Initial stage' })
|
|
16
|
+
stage?: ModelStage
|
|
17
|
+
|
|
18
|
+
@Field(type => ModelFramework, { nullable: true, description: 'ML framework' })
|
|
19
|
+
framework?: ModelFramework
|
|
20
|
+
|
|
21
|
+
@Field({ nullable: true, description: 'Model metrics as JSON string' })
|
|
22
|
+
metrics?: string
|
|
23
|
+
|
|
24
|
+
@Field({ nullable: true, description: 'Model metadata as JSON string' })
|
|
25
|
+
metadata?: string
|
|
26
|
+
|
|
27
|
+
@Field({ nullable: true, description: 'Artifact URI' })
|
|
28
|
+
artifactUri?: string
|
|
29
|
+
|
|
30
|
+
@Field({ nullable: true, description: 'MLflow run ID' })
|
|
31
|
+
mlflowRunId?: string
|
|
32
|
+
|
|
33
|
+
@Field({ nullable: true, description: 'MLflow experiment ID' })
|
|
34
|
+
mlflowExperimentId?: string
|
|
35
|
+
|
|
36
|
+
@Field(type => Int, { nullable: true, description: 'Label Studio project ID' })
|
|
37
|
+
labelStudioProjectId?: number
|
|
38
|
+
|
|
39
|
+
@Field({ nullable: true, description: 'Tags as JSON array string' })
|
|
40
|
+
tags?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@InputType({ description: 'Input for updating ML model' })
|
|
44
|
+
export class UpdateMLModelInput {
|
|
45
|
+
@Field({ nullable: true, description: 'Model description' })
|
|
46
|
+
description?: string
|
|
47
|
+
|
|
48
|
+
@Field(type => ModelStage, { nullable: true, description: 'Stage' })
|
|
49
|
+
stage?: ModelStage
|
|
50
|
+
|
|
51
|
+
@Field({ nullable: true, description: 'Model metrics as JSON string' })
|
|
52
|
+
metrics?: string
|
|
53
|
+
|
|
54
|
+
@Field({ nullable: true, description: 'Model metadata as JSON string' })
|
|
55
|
+
metadata?: string
|
|
56
|
+
|
|
57
|
+
@Field({ nullable: true, description: 'Tags as JSON array string' })
|
|
58
|
+
tags?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@InputType({ description: 'Input for promoting model stage' })
|
|
62
|
+
export class PromoteModelInput {
|
|
63
|
+
@Field({ description: 'Model ID to promote' })
|
|
64
|
+
modelId: string
|
|
65
|
+
|
|
66
|
+
@Field(type => ModelStage, { description: 'Target stage' })
|
|
67
|
+
targetStage: ModelStage
|
|
68
|
+
|
|
69
|
+
@Field({ nullable: true, description: 'Promotion notes' })
|
|
70
|
+
notes?: string
|
|
71
|
+
}
|