Skip to content

Commit

Permalink
feat(Model): janhq#1028 made model.json optional (janhq#1314)
Browse files Browse the repository at this point in the history
* feat(Model): janhq#1028 made model.json optional

Signed-off-by: James <[email protected]>

---------

Signed-off-by: James <[email protected]>
Co-authored-by: James <[email protected]>
  • Loading branch information
namchuai and James authored Jan 4, 2024
1 parent aecef0d commit 26eb1d9
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 14 deletions.
3 changes: 2 additions & 1 deletion core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FileStat } from './types'

/**
* Execute a extension module function in main process
*
Expand Down Expand Up @@ -74,6 +76,15 @@ const openExternalUrl: (url: string) => Promise<any> = (url) =>
*/
const getResourcePath: () => Promise<string> = () => global.core.api?.getResourcePath()

/**
* Gets the file's stats.
*
* @param path - The path to the file.
* @returns {Promise<FileStat>} - A promise that resolves with the file's stats.
*/
const fileStat: (path: string) => Promise<FileStat | undefined> = (path) =>
global.core.api?.fileStat(path)

/**
* Register extension point function type definition
*/
Expand All @@ -97,4 +108,5 @@ export {
joinPath,
openExternalUrl,
baseName,
fileStat,
}
12 changes: 12 additions & 0 deletions core/src/node/api/routes/fileManager.ts
Original file line number Diff line number Diff line change
@@ -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) => {})
}
4 changes: 4 additions & 0 deletions core/src/types/file/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type FileStat = {
isDirectory: boolean
size: number
}
1 change: 1 addition & 0 deletions core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './thread'
export * from './message'
export * from './inference'
export * from './monitoring'
export * from './file'
39 changes: 34 additions & 5 deletions electron/handlers/fileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<FileStat | undefined> => {
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
}
)
}
107 changes: 99 additions & 8 deletions extensions/model-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -199,7 +203,7 @@ export default class JanModelExtension implements ModelExtension {
): Promise<Model[]> {
try {
if (!(await fs.existsSync(JanModelExtension._homeDir))) {
console.debug('model folder not found')
console.error('Model folder not found')
return []
}

Expand All @@ -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) => {
Expand Down Expand Up @@ -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<Model> {
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<Model | undefined> {
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.
Expand Down
35 changes: 35 additions & 0 deletions models/config/default-model.json
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 26eb1d9

Please sign in to comment.