* feat(Model): #1028 made model.json optional Signed-off-by: James <james@jan.ai> --------- Signed-off-by: James <james@jan.ai> Co-authored-by: James <james@jan.ai>
This commit is contained in:
parent
aecef0df5a
commit
26eb1d9a67
@ -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
|
||||
|
||||
@ -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<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
|
||||
*/
|
||||
@ -97,4 +108,5 @@ export {
|
||||
joinPath,
|
||||
openExternalUrl,
|
||||
baseName,
|
||||
fileStat,
|
||||
}
|
||||
|
||||
12
core/src/node/api/routes/fileManager.ts
Normal file
12
core/src/node/api/routes/fileManager.ts
Normal file
@ -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
core/src/types/file/index.ts
Normal file
4
core/src/types/file/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type FileStat = {
|
||||
isDirectory: boolean
|
||||
size: number
|
||||
}
|
||||
@ -4,3 +4,4 @@ export * from './thread'
|
||||
export * from './message'
|
||||
export * from './inference'
|
||||
export * from './monitoring'
|
||||
export * from './file'
|
||||
|
||||
@ -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<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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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<Model[]> {
|
||||
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<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.
|
||||
|
||||
35
models/config/default-model.json
Normal file
35
models/config/default-model.json
Normal file
@ -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"
|
||||
}
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user