From 26eb1d9a6712bd63b27c149a5251de9e983a915b Mon Sep 17 00:00:00 2001 From: NamH Date: Thu, 4 Jan 2024 16:50:32 +0700 Subject: [PATCH] feat(Model): #1028 made model.json optional (#1314) * feat(Model): #1028 made model.json optional Signed-off-by: James --------- Signed-off-by: James Co-authored-by: James --- core/src/api/index.ts | 3 +- core/src/core.ts | 12 +++ core/src/node/api/routes/fileManager.ts | 12 +++ core/src/types/file/index.ts | 4 + core/src/types/index.ts | 1 + electron/handlers/fileManager.ts | 39 +++++++-- extensions/model-extension/src/index.ts | 107 ++++++++++++++++++++++-- models/config/default-model.json | 35 ++++++++ server/package.json | 1 + 9 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 core/src/node/api/routes/fileManager.ts create mode 100644 core/src/types/file/index.ts create mode 100644 models/config/default-model.json diff --git a/core/src/api/index.ts b/core/src/api/index.ts index be1b06777..75fc3f007 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -53,9 +53,10 @@ export enum FileSystemRoute { writeFileSync = 'writeFileSync', } export enum FileManagerRoute { - synceFile = 'syncFile', + syncFile = 'syncFile', getUserSpace = 'getUserSpace', getResourcePath = 'getResourcePath', + fileStat = 'fileStat', } export type ApiFunction = (...args: any[]) => any diff --git a/core/src/core.ts b/core/src/core.ts index 2cfd43a39..11683bdc5 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -1,3 +1,5 @@ +import { FileStat } from './types' + /** * Execute a extension module function in main process * @@ -74,6 +76,15 @@ const openExternalUrl: (url: string) => Promise = (url) => */ const getResourcePath: () => Promise = () => global.core.api?.getResourcePath() +/** + * Gets the file's stats. + * + * @param path - The path to the file. + * @returns {Promise} - A promise that resolves with the file's stats. + */ +const fileStat: (path: string) => Promise = (path) => + global.core.api?.fileStat(path) + /** * Register extension point function type definition */ @@ -97,4 +108,5 @@ export { joinPath, openExternalUrl, baseName, + fileStat, } diff --git a/core/src/node/api/routes/fileManager.ts b/core/src/node/api/routes/fileManager.ts new file mode 100644 index 000000000..04ab1913b --- /dev/null +++ b/core/src/node/api/routes/fileManager.ts @@ -0,0 +1,12 @@ +import { FileManagerRoute } from '../../../api' +import { HttpServer } from '../../index' + +export const fsRouter = async (app: HttpServer) => { + app.post(`/app/${FileManagerRoute.syncFile}`, async (request: any, reply: any) => {}) + + app.post(`/app/${FileManagerRoute.getUserSpace}`, async (request: any, reply: any) => {}) + + app.post(`/app/${FileManagerRoute.getResourcePath}`, async (request: any, reply: any) => {}) + + app.post(`/app/${FileManagerRoute.fileStat}`, async (request: any, reply: any) => {}) +} diff --git a/core/src/types/file/index.ts b/core/src/types/file/index.ts new file mode 100644 index 000000000..6526cfc6d --- /dev/null +++ b/core/src/types/file/index.ts @@ -0,0 +1,4 @@ +export type FileStat = { + isDirectory: boolean + size: number +} diff --git a/core/src/types/index.ts b/core/src/types/index.ts index a4722ba78..5fb4448f9 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -4,3 +4,4 @@ export * from './thread' export * from './message' export * from './inference' export * from './monitoring' +export * from './file' diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts index 2a78deaf9..f8b8ee6f1 100644 --- a/electron/handlers/fileManager.ts +++ b/electron/handlers/fileManager.ts @@ -4,14 +4,17 @@ import reflect from '@alumna/reflect' import { FileManagerRoute } from '@janhq/core' import { userSpacePath, getResourcePath } from './../utils/path' +import fs from 'fs' +import { join } from 'path' +import { FileStat } from '@janhq/core/.' /** * Handles file system extensions operations. */ export function handleFileMangerIPCs() { - // Handles the 'synceFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path. + // Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path. ipcMain.handle( - FileManagerRoute.synceFile, + FileManagerRoute.syncFile, async (_event, src: string, dest: string) => { return reflect({ src, @@ -31,7 +34,33 @@ export function handleFileMangerIPCs() { ) // Handles the 'getResourcePath' IPC event. This event is triggered to get the resource path. - ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => { - return getResourcePath() - }) + ipcMain.handle(FileManagerRoute.getResourcePath, async (_event) => + getResourcePath() + ) + + // handle fs is directory here + ipcMain.handle( + FileManagerRoute.fileStat, + async (_event, path: string): Promise => { + const normalizedPath = path + .replace(`file://`, '') + .replace(`file:/`, '') + .replace(`file:\\\\`, '') + .replace(`file:\\`, '') + + const fullPath = join(userSpacePath, normalizedPath) + const isExist = fs.existsSync(fullPath) + if (!isExist) return undefined + + const isDirectory = fs.lstatSync(fullPath).isDirectory() + const size = fs.statSync(fullPath).size + + const fileStat: FileStat = { + isDirectory, + size, + } + + return fileStat + } + ) } diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index 8c8972dac..b0fc995a1 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -5,11 +5,12 @@ import { abortDownload, getResourcePath, getUserSpace, + fileStat, InferenceEngine, joinPath, + ModelExtension, + Model, } from '@janhq/core' -import { ModelExtension, Model } from '@janhq/core' -import { baseName } from '@janhq/core/.' /** * A extension for models @@ -21,6 +22,9 @@ export default class JanModelExtension implements ModelExtension { private static readonly _incompletedModelFileName = '.download' private static readonly _offlineInferenceEngine = InferenceEngine.nitro + private static readonly _configDirName = 'config' + private static readonly _defaultModelFileName = 'default-model.json' + /** * Implements type from JanExtension. * @override @@ -199,7 +203,7 @@ export default class JanModelExtension implements ModelExtension { ): Promise { try { if (!(await fs.existsSync(JanModelExtension._homeDir))) { - console.debug('model folder not found') + console.error('Model folder not found') return [] } @@ -220,13 +224,22 @@ export default class JanModelExtension implements ModelExtension { dirName, JanModelExtension._modelMetadataFileName, ]) - let model = await this.readModelMetadata(jsonPath) - model = typeof model === 'object' ? model : JSON.parse(model) - if (selector && !(await selector?.(dirName, model))) { - return + if (await fs.existsSync(jsonPath)) { + // if we have the model.json file, read it + let model = await this.readModelMetadata(jsonPath) + model = typeof model === 'object' ? model : JSON.parse(model) + + if (selector && !(await selector?.(dirName, model))) { + return + } + return model + } else { + // otherwise, we generate our own model file + // TODO: we might have more than one binary file here. This will be addressed with new version of Model file + // which is the PR from Hiro on branch Jan can see + return this.generateModelMetadata(dirName) } - return model }) const results = await Promise.allSettled(readJsonPromises) const modelData = results.map((result) => { @@ -254,6 +267,84 @@ export default class JanModelExtension implements ModelExtension { return fs.readFileSync(path, 'utf-8') } + /** + * Handle the case where we have the model directory but we don't have the corresponding + * model.json file associated with it. + * + * This function will create a model.json file for the model. + * + * @param dirName the director which reside in ~/jan/models but does not have model.json file. + */ + private async generateModelMetadata(dirName: string): Promise { + const files: string[] = await fs.readdirSync( + await joinPath([JanModelExtension._homeDir, dirName]) + ) + + // sort files by name + files.sort() + + // find the first file which is not a directory + let binaryFileName: string | undefined = undefined + let binaryFileSize: number | undefined = undefined + + for (const file of files) { + if (file.endsWith(JanModelExtension._incompletedModelFileName)) continue + if (file.endsWith('.json')) continue + + const path = await joinPath([JanModelExtension._homeDir, dirName, file]) + const fileStats = await fileStat(path) + if (fileStats.isDirectory) continue + binaryFileSize = fileStats.size + binaryFileName = file + break + } + + if (!binaryFileName) { + console.warn(`Unable to find binary file for model ${dirName}`) + return + } + + const defaultModel = await this.getDefaultModel() + if (!defaultModel) { + console.error('Unable to find default model') + return + } + + const model: Model = { + ...defaultModel, + id: dirName, + name: dirName, + created: Date.now(), + description: `${dirName} - user self import model`, + } + + const modelFilePath = await joinPath([ + JanModelExtension._homeDir, + dirName, + JanModelExtension._modelMetadataFileName, + ]) + + await fs.writeFileSync(modelFilePath, JSON.stringify(model, null, 2)) + + return model + } + + private async getDefaultModel(): Promise { + const defaultModelPath = await joinPath([ + JanModelExtension._homeDir, + JanModelExtension._configDirName, + JanModelExtension._defaultModelFileName, + ]) + + if (!(await fs.existsSync(defaultModelPath))) { + return undefined + } + + const model = await this.readModelMetadata(defaultModelPath) + + return typeof model === 'object' ? model : JSON.parse(model) + } + /** * Gets all available models. * @returns A Promise that resolves with an array of all models. diff --git a/models/config/default-model.json b/models/config/default-model.json new file mode 100644 index 000000000..a00a8c024 --- /dev/null +++ b/models/config/default-model.json @@ -0,0 +1,35 @@ +{ + "object": "model", + "version": 1, + "format": "gguf", + "source_url": "N/A", + "id": "N/A", + "name": "N/A", + "created": 0, + "description": "User self import model", + "settings": { + "ctx_len": 4096, + "ngl": 0, + "embedding": false, + "n_parallel": 0, + "cpu_threads": 0, + "prompt_template": "" + }, + "parameters": { + "temperature": 0, + "token_limit": 0, + "top_k": 0, + "top_p": 0, + "stream": false, + "max_tokens": 4096, + "stop": [], + "frequency_penalty": 0, + "presence_penalty": 0 + }, + "metadata": { + "author": "User", + "tags": [], + "size": 0 + }, + "engine": "nitro" +} diff --git a/server/package.json b/server/package.json index 36fdc124f..4db9894be 100644 --- a/server/package.json +++ b/server/package.json @@ -17,6 +17,7 @@ "build": "tsc" }, "dependencies": { + "@alumna/reflect": "^1.1.3", "@fastify/cors": "^8.4.2", "@fastify/static": "^6.12.0", "@fastify/swagger": "^8.13.0",