Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: local engine management #4334

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/browser/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Model = 'model',
SystemMonitoring = 'systemMonitoring',
HuggingFace = 'huggingFace',
Engine = 'engine',
}

export interface ExtensionType {
Expand Down Expand Up @@ -102,7 +103,7 @@
* @property {Array} platform
*/
compatibility(): Compatibility | undefined {
return undefined

Check warning on line 106 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

106 line is not covered with tests
}

/**
Expand All @@ -110,8 +111,8 @@
* @param models
*/
async registerModels(models: Model[]): Promise<void> {
for (const model of models) {
ModelManager.instance().register(model)

Check warning on line 115 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

114-115 lines are not covered with tests
}
}

Expand All @@ -122,8 +123,8 @@
*/
async registerSettings(settings: SettingComponentProps[]): Promise<void> {
if (!this.name) {
console.error('Extension name is not defined')
return

Check warning on line 127 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

126-127 lines are not covered with tests
}

const extensionSettingFolderPath = await joinPath([
Expand All @@ -141,18 +142,18 @@

// Persists new settings
if (await fs.existsSync(settingFilePath)) {
const oldSettings = JSON.parse(await fs.readFileSync(settingFilePath, 'utf-8'))
settings.forEach((setting) => {

Check warning on line 146 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

145-146 lines are not covered with tests
// Keep setting value
if (setting.controllerProps && Array.isArray(oldSettings))
setting.controllerProps.value = oldSettings.find(
(e: any) => e.key === setting.key

Check warning on line 150 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

148-150 lines are not covered with tests
)?.controllerProps?.value
})
}
await fs.writeFileSync(settingFilePath, JSON.stringify(settings, null, 2))
} catch (err) {
console.error(err)

Check warning on line 156 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

156 line is not covered with tests
}
}

Expand Down Expand Up @@ -196,23 +197,23 @@
* @returns
*/
async getSettings(): Promise<SettingComponentProps[]> {
if (!this.name) return []

Check warning on line 200 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

200 line is not covered with tests

const settingPath = await joinPath([

Check warning on line 202 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

202 line is not covered with tests
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])

try {
if (!(await fs.existsSync(settingPath))) return []
const content = await fs.readFileSync(settingPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
return settings

Check warning on line 213 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

209-213 lines are not covered with tests
} catch (err) {
console.warn(err)
return []

Check warning on line 216 in core/src/browser/extension.ts

View workflow job for this annotation

GitHub Actions / coverage-check

215-216 lines are not covered with tests
}
}

Expand Down
91 changes: 91 additions & 0 deletions core/src/browser/extensions/enginesManagement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
InferenceEngine,
Engines,
EngineVariant,
EngineReleased,
DefaultEngineVariant,
} from '../../types'
import { BaseExtension, ExtensionTypeEnum } from '../extension'

/**
* Engine management extension. Persists and retrieves engine management.
* @abstract
* @extends BaseExtension
*/
export abstract class EngineManagementExtension extends BaseExtension {
type(): ExtensionTypeEnum | undefined {
return ExtensionTypeEnum.Engine
}

/**
* @returns A Promise that resolves to an object of list engines.
*/
abstract getEngines(): Promise<Engines>

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to an array of installed engine.
*/
abstract getInstalledEngines(name: InferenceEngine): Promise<EngineVariant[]>

/**
* @param name - Inference engine name.
* @param version - Version of the engine.
* @param platform - Optional to sort by operating system. macOS, linux, windows.
* @returns A Promise that resolves to an array of latest released engine by version.
*/
abstract getReleasedEnginesByVersion(
name: InferenceEngine,
version: string,
platform?: string
): Promise<EngineReleased[]>

/**
* @param name - Inference engine name.
* @param platform - Optional to sort by operating system. macOS, linux, windows.
* @returns A Promise that resolves to an array of latest released engine.
*/
abstract getLatestReleasedEngine(
name: InferenceEngine,
platform?: string
): Promise<EngineReleased[]>

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to intall of engine.
*/
abstract installEngine(
name: InferenceEngine,
engineConfig: { variant: string; version?: string }
): Promise<{ messages: string }>

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to unintall of engine.
*/
abstract uninstallEngine(
name: InferenceEngine,
engineConfig: { variant: string; version: string }
): Promise<{ messages: string }>

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to an object of default engine.
*/
abstract getDefaultEngineVariant(name: InferenceEngine): Promise<DefaultEngineVariant>

/**
* @body variant - string
* @body version - string
* @returns A Promise that resolves to set default engine.
*/
abstract setDefaultEngineVariant(
name: InferenceEngine,
engineConfig: { variant: string; version: string }
): Promise<{ messages: string }>

/**
* @returns A Promise that resolves to update engine.
*/
abstract updateEngine(name: InferenceEngine): Promise<{ messages: string }>
}
5 changes: 5 additions & 0 deletions core/src/browser/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@ export { ModelExtension } from './model'
* Base AI Engines.
*/
export * from './engines'

/**
* Engines Management
*/
export * from './enginesManagement'
28 changes: 28 additions & 0 deletions core/src/types/engine/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { InferenceEngine } from '../../types'

export type Engines = {
[key in InferenceEngine]: EngineVariant[]
}

export type EngineVariant = {
engine: InferenceEngine
name: string
version: string
}

export type DefaultEngineVariant = {
engine: InferenceEngine
variant: string
version: string
}

export type EngineReleased = {
created_at: string
download_count: number
name: string
size: number
}

export enum EngineEvent {
OnEngineUpdate = 'OnEngineUpdate',
}
1 change: 1 addition & 0 deletions core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './huggingface'
export * from './miscellaneous'
export * from './api'
export * from './setting'
export * from './engine'
5 changes: 5 additions & 0 deletions extensions/engine-management-extension/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
}
39 changes: 39 additions & 0 deletions extensions/engine-management-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@janhq/engine-management-extension",
"productName": "Engine Management",
"version": "1.0.0",
"description": "This extension enables manage engines",
"main": "dist/index.js",
"author": "Jan <[email protected]>",
"license": "MIT",
"scripts": {
"test": "jest",
"build": "tsc -b . && webpack --config webpack.config.js",
"build:publish": "rimraf *.tgz --glob && yarn build && npm pack && cpx *.tgz ../../pre-install"
},
"exports": {
".": "./dist/index.js",
"./main": "./dist/module.js"
},
"devDependencies": {
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"ts-loader": "^9.5.0",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@janhq/core": "file:../../core",
"ky": "^1.7.2",
"p-queue": "^8.0.1"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/*",
"package.json",
"README.md"
],
"bundleDependencies": []
}
15 changes: 15 additions & 0 deletions extensions/engine-management-extension/src/@types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export {}
declare global {
declare const API_URL: string
declare const CORTEX_ENGINE_VERSION: string
declare const SOCKET_URL: string

interface Core {
api: APIFunctions
events: EventEmitter
}
interface Window {
core?: Core | undefined
electronAPI?: any | undefined
}
}
184 changes: 184 additions & 0 deletions extensions/engine-management-extension/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
EngineManagementExtension,
InferenceEngine,
DefaultEngineVariant,
Engines,
EngineVariant,
EngineReleased,
} from '@janhq/core'
import ky, { HTTPError } from 'ky'
import PQueue from 'p-queue'

/**
* JSONEngineManagementExtension is a EngineManagementExtension implementation that provides
* functionality for managing engines.
*/
export default class JSONEngineManagementExtension extends EngineManagementExtension {
queue = new PQueue({ concurrency: 1 })

/**
* Called when the extension is loaded.
*/
async onLoad() {
this.queue.add(() => this.healthz())
try {
await this.getDefaultEngineVariant(InferenceEngine.cortex_llamacpp)
} catch (error) {
if (error instanceof HTTPError && error.response.status === 400) {
await this.setDefaultEngineVariant(InferenceEngine.cortex_llamacpp, {
variant: 'mac-arm64',
version: `${CORTEX_ENGINE_VERSION}`,
})
} else {
console.error('An unexpected error occurred:', error)
}
}
}

/**
* Called when the extension is unloaded.
*/
onUnload() {}

/**
* @returns A Promise that resolves to an object of list engines.
*/
async getEngines(): Promise<Engines> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines`)
.json<Engines>()
.then((e) => e)
) as Promise<Engines>
}

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to an array of installed engine.
*/
async getInstalledEngines(name: InferenceEngine): Promise<EngineVariant[]> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}`)
.json<EngineVariant[]>()
.then((e) => e)
) as Promise<EngineVariant[]>
}

/**
* @param name - Inference engine name.
* @param version - Version of the engine.
* @param platform - Optional to sort by operating system. macOS, linux, windows.
* @returns A Promise that resolves to an array of latest released engine by version.
*/
async getReleasedEnginesByVersion(
name: InferenceEngine,
version: string,
platform?: string
) {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}/releases/${version}`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
) as Promise<EngineReleased[]>
}

/**
* @param name - Inference engine name.
* @param platform - Optional to sort by operating system. macOS, linux, windows.
* @returns A Promise that resolves to an array of latest released engine by version.
*/
async getLatestReleasedEngine(name: InferenceEngine, platform?: string) {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}/releases/latest`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
) as Promise<EngineReleased[]>
}

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to intall of engine.
*/
async installEngine(
name: InferenceEngine,
engineConfig: { variant: string; version?: string }
) {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
) as Promise<{ messages: string }>
}

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to unintall of engine.
*/
async uninstallEngine(
name: InferenceEngine,
engineConfig: { variant: string; version: string }
) {
return this.queue.add(() =>
ky
.delete(`${API_URL}/v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
) as Promise<{ messages: string }>
}

/**
* @param name - Inference engine name.
* @returns A Promise that resolves to an object of default engine.
*/
async getDefaultEngineVariant(name: InferenceEngine) {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}/default`)
.json<{ messages: string }>()
.then((e) => e)
) as Promise<DefaultEngineVariant>
}

/**
* @body variant - string
* @body version - string
* @returns A Promise that resolves to set default engine.
*/
async setDefaultEngineVariant(
name: InferenceEngine,
engineConfig: { variant: string; version: string }
) {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/engines/${name}/default`, { json: engineConfig })
.then((e) => e)
) as Promise<{ messages: string }>
}

/**
* @returns A Promise that resolves to update engine.
*/
async updateEngine(name: InferenceEngine) {
return this.queue.add(() =>
ky.post(`${API_URL}/v1/engines/${name}/update`).then((e) => e)
) as Promise<{ messages: string }>
}

/**
* Do health check on cortex.cpp
* @returns
*/
healthz(): Promise<void> {
return ky
.get(`${API_URL}/healthz`, {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
.then(() => {})
}
}
Loading
Loading