Skip to content

Commit

Permalink
#255: download model for web
Browse files Browse the repository at this point in the history
  • Loading branch information
John authored and John committed Oct 18, 2023
1 parent 21bee76 commit ed2db5f
Show file tree
Hide file tree
Showing 16 changed files with 1,064 additions and 202 deletions.
2 changes: 1 addition & 1 deletion electron/core/plugin-manager/execution/facade.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import Plugin from "./Plugin";
import { register } from "./activation-manager";
import plugins from "../../plugins/plugin.json"
import plugins from "../../../../web/public/plugins/plugin.json"

/**
* @typedef {Object.<string, any>} installOptions The {@link https://www.npmjs.com/package/pacote|pacote options}
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"build:publish-darwin": "yarn build:web && yarn workspace jan build:publish-darwin",
"build:publish-win32": "yarn build:web && yarn workspace jan build:publish-win32",
"build:publish-linux": "yarn build:web && yarn workspace jan build:publish-linux",
"buid:web-plugins": "yarn build:plugins && cp \"./electron/core/plugins/data-plugin/dist/index.js\" \"./web/public/plugins/data-plugin\" && cp \"./electron/core/plugins/inference-plugin/dist/index.js\" \"./web/public/plugins/inference-plugin\" && cp \"./electron/core/plugins/model-management-plugin/dist/index.js\" \"./web/public/plugins/model-management-plugin\" && cp \"./electron/core/plugins/monitoring-plugin/dist/bundle.js\" \"./web/public/plugins/monitoring-plugin\"",
"server:dev": "yarn build:web && cpx \"web/out/**\" \"server/renderer/\" && yarn buid:web-plugins && cp -r ./electron/core/plugins ./server/plugins && yarn workspace server dev"
"buid:web-plugins": "yarn build:plugins && yarn build:web && cp \"./plugins/data-plugin/dist/esm/index.js\" \"./web/public/plugins/data-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/public/plugins/inference-plugin\" && cp \"./plugins/model-management-plugin/dist/index.js\" \"./web/public/plugins/model-management-plugin\" && cp \"./plugins/monitoring-plugin/dist/bundle.js\" \"./web/public/plugins/monitoring-plugin\"",
"server:dev": "yarn buid:web-plugins && cpx \"web/out/**\" \"server/renderer/\" && mkdir -p ./server/@janhq && cp -r ./plugins/* ./server/@janhq && yarn workspace server dev"

},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions plugin-core/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export enum EventName {
OnNewMessageRequest = "onNewMessageRequest",
OnNewMessageResponse = "onNewMessageResponse",
OnMessageResponseUpdate = "onMessageResponseUpdate",
OnDownloadUpdate = "OnDownloadUpdate",
OnDownloadSuccess = "OnDownloadSuccess",
OnDownloadError = "OnDownloadError"
}

/**
Expand Down
9 changes: 8 additions & 1 deletion plugins/data-plugin/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const dbs: Record<string, any> = {};
*/
function createCollection(name: string, schema?: { [key: string]: any }): Promise<void> {
return new Promise<void>((resolve) => {
const dbPath = path.join(app.getPath("userData"), "databases");
const dbPath = path.join(appPath(), "databases");
if (!fs.existsSync(dbPath)) fs.mkdirSync(dbPath);
const db = new PouchDB(`${path.join(dbPath, name)}`);
dbs[name] = db;
Expand Down Expand Up @@ -226,6 +226,13 @@ function findMany(
.then((data) => data.docs); // Return documents
}

function appPath() {
if (app) {
return app.getPath("userData");
}
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
}

module.exports = {
createCollection,
deleteCollection,
Expand Down
9 changes: 8 additions & 1 deletion plugins/inference-plugin/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const initModel = (fileName) => {
config.custom_config = {};
}

const modelPath = path.join(app.getPath("userData"), fileName);
const modelPath = path.join(appPath(), fileName);

config.custom_config.llama_model_path = modelPath;

Expand Down Expand Up @@ -112,6 +112,13 @@ function killSubprocess() {
}
}

function appPath() {
if (app) {
return app.getPath("userData");
}
return process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share");
}

module.exports = {
initModel,
killSubprocess,
Expand Down
58 changes: 56 additions & 2 deletions plugins/model-management-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ModelManagementService, PluginService, RegisterExtensionPoint, core, store } from "@janhq/plugin-core";
import { ModelManagementService, PluginService, RegisterExtensionPoint, core, store, EventName, events } from "@janhq/plugin-core";

const PluginName = "@janhq/model-management-plugin";
const MODULE_PATH = "@janhq/model-management-plugin/dist/module.js";
Expand All @@ -7,7 +7,60 @@ const getDownloadedModels = () => core.invokePluginFunc(MODULE_PATH, "getDownloa

const getAvailableModels = () => core.invokePluginFunc(MODULE_PATH, "getAvailableModels");

const downloadModel = (product) => core.downloadFile(product.downloadUrl, product.fileName);
const downloadModel = (product) => {
core.downloadFile(product.downloadUrl, product.fileName);
checkDownloadProgress(product.fileName);
}

async function checkDownloadProgress(fileName: string) {
if (typeof window !== "undefined" && typeof (window as any).electronAPI === "undefined") {
const intervalId = setInterval(() => {
downloadProgress(fileName, intervalId);
}, 3000);
}
}

async function downloadProgress(fileName: string, intervalId: NodeJS.Timeout): Promise<any> {
const response = await fetch("/api/v1/downloadProgress", {
method: 'POST',
body: JSON.stringify({ fileName: fileName }),
headers: { 'Content-Type': 'application/json', 'Authorization': '' }
});

if (!response.ok) {
events.emit(EventName.OnDownloadError, null);
clearInterval(intervalId);
}
else if (response.status >= 400) {
events.emit(EventName.OnDownloadError, null);
clearInterval(intervalId);
}
else {
const text = await response.text();
try {
const json = JSON.parse(text)
if (isEmptyObject(json)) {
if (!fileName) {
clearInterval(intervalId);
}
return;
}
if (json?.success === true) {
events.emit(EventName.OnDownloadSuccess, json);
clearInterval(intervalId);
} else {
events.emit(EventName.OnDownloadUpdate, json);
}
} catch (err) {
events.emit(EventName.OnDownloadError, null);
clearInterval(intervalId);
}
}
}

function isEmptyObject(ojb: any): boolean {
return Object.keys(ojb).length === 0;
}

const deleteModel = (path) => core.deleteFile(path);

Expand Down Expand Up @@ -79,6 +132,7 @@ function getModelById(modelId: string): Promise<any> {

function onStart() {
store.createCollection("models", {});
checkDownloadProgress(null);
}

// Register all the above functions and objects with the relevant extension points
Expand Down
78 changes: 41 additions & 37 deletions server/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express, { Express, Request, Response } from 'express'
import cors from "cors";
import { resolve} from "path";
import { resolve } from "path";
import { unlink, createWriteStream } from "fs";
import * as http from 'http';
const progress = require("request-progress");
Expand All @@ -13,11 +13,16 @@ const port: number = 4000
const dataDir = __dirname;
type DownloadProgress = Record<string, any>;
const downloadProgress: DownloadProgress = {};

const app: Express = express()

app.use(express.static('renderer'))
app.use(cors(options))
app.use(express.json());
app.use((err: Error, req: Request, res: Response, next: Function) => {
console.error(err.stack);
res.status(500).send({ error: err });
});

app.post('/api/v1/invokeFunction', (req: Request, res: Response) => {
const method = req.body["method"];
const args = req.body["args"];
Expand All @@ -27,6 +32,12 @@ app.post('/api/v1/invokeFunction', (req: Request, res: Response) => {
res.json(Object());
break;
case "downloadFile":
const obj = downloadingFile();
if (obj) {
res.status(500)
res.json({ error: obj.fileName + " is being downloaded!" })
return;
}
(async () => {
downloadModel(args.downloadUrl, args.fileName);
})().catch(e => {
Expand All @@ -47,48 +58,35 @@ app.post('/api/v1/invokeFunction', (req: Request, res: Response) => {
}
});

app.post('/api/v1/chatCompletion', (baseRequest: Request, baseReponse: Response) => {
const postData = JSON.stringify(baseRequest.body);
const options: http.RequestOptions = {
path: '/llama/chat_completion',
hostname: 'localhost',
port: 3928,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': postData.length
}
};
const req = http.request(options, function (resp: http.IncomingMessage) {
baseReponse.setHeader('content-disposition', resp.headers['content-disposition'] ?? "attachment; filename=chat_completions.txt");
baseReponse.setHeader('Content-type', resp.headers['content-type'] ?? "text/plain; charset=utf-8");
resp.pipe(baseReponse);
});
req.write(postData);
req.end();
});

app.post('/api/v1/downloadProgress', (req: Request, res: Response) => {
const fileName = req.body["fileName"];
if(fileName && downloadProgress[fileName]){
if (fileName && downloadProgress[fileName]) {
res.json(downloadProgress[fileName])
return;
} else {
const obj = downloadingFile();
if (obj) {
res.json(obj)
return;
}
}
res.json(Object());
});

app.listen(port, () => console.log(`Application is running on port ${port}`));


const invokeFunction = (modulePath: string, method: string, ...args: any): Promise<any> => {
const invokeFunction = async (modulePath: string, method: string, args: any): Promise<any> => {
console.log(modulePath, method, args);
const module = require(/* webpackIgnore: true */ path.join(
dataDir,
"plugins",
"",
modulePath
));
requiredModules[modulePath] = module;
if (typeof module[method] === "function") {
return module[method](...args);
return (module[method](...args))
.catch((err: any) => { console.log(err) })
} else {
return Promise.resolve();
}
Expand All @@ -101,21 +99,23 @@ const downloadModel = (downloadUrl: string, fileName: string) => {
console.log("Download file", fileName, "to", destination);
progress(request(downloadUrl), {})
.on("progress", function (state: any) {
downloadProgress[fileName] = state;
downloadProgress[fileName] = {
...state,
fileName,
};
console.log("downloading file", fileName, state.percent);
})
.on("error", function (err: Error) {
console.log("err downloading file", fileName, err);
delete downloadProgress[fileName];
downloadProgress[fileName] = {
success: false,
fileName: fileName,
};
})
.on("end", function () {
invokeFunction(
"data-plugin/dist/module.js",
"updateFinishedDownloadAt",
fileName,
Date.now()
);
delete downloadProgress[fileName];
downloadProgress[fileName] = {
success: true,
fileName: fileName,
};
})
.pipe(createWriteStream(destination));
}
Expand All @@ -138,3 +138,7 @@ const deleteFile = (filePath: string) => {
return result;
}

const downloadingFile = (): any | undefined => {
const obj = Object.values(downloadProgress).find(obj => obj && typeof obj.success === "undefined")
return obj
}
28 changes: 28 additions & 0 deletions web/app/_helpers/EventHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { addNewMessageAtom, updateMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
import { setDownloadStateAtom, setDownloadStateSuccessAtom } from "@/_helpers/atoms/DownloadState.atom";
import { toChatMessage } from "@/_models/ChatMessage";
import { events, EventName, NewMessageResponse } from "@janhq/plugin-core";
import { useSetAtom } from "jotai";
import { ReactNode, useEffect } from "react";
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { ModelManagementService } from "@janhq/plugin-core";
import { getDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { downloadedModelAtom } from "./atoms/DownloadedModel.atom";

export default function EventHandler({ children }: { children: ReactNode }) {
const addNewMessage = useSetAtom(addNewMessageAtom);
const updateMessage = useSetAtom(updateMessageAtom);
const setDownloadState = useSetAtom(setDownloadStateAtom);
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom);
const setDownloadedModels = useSetAtom(downloadedModelAtom);

function handleNewMessageResponse(message: NewMessageResponse) {
const newResponse = toChatMessage(message);
Expand All @@ -17,17 +25,37 @@ export default function EventHandler({ children }: { children: ReactNode }) {
updateMessage(messageResponse._id, messageResponse.conversationId, messageResponse.message);
}

function handleDownloadUpdate(state: any) {
if (!state) return;
setDownloadState(state);
}

function handleDownloadSuccess(state: any) {
if (state && state.fileName && state.success === true) {
setDownloadStateSuccess(state.fileName);
executeSerial(ModelManagementService.UpdateFinishedDownloadAt, state.fileName).then(() => {
getDownloadedModels().then((models) => {
setDownloadedModels(models);
});
});
}
}

useEffect(() => {
if (window.corePlugin.events) {
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse);
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate);
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate);
events.on(EventName.OnDownloadSuccess, handleDownloadSuccess);
}
}, []);

useEffect(() => {
return () => {
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse);
events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate);
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate);
events.off(EventName.OnDownloadSuccess, handleDownloadSuccess);
};
}, []);
return <> {children}</>;
Expand Down
8 changes: 4 additions & 4 deletions web/app/_hooks/useGetSystemResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export default function useGetSystemResources() {
getSystemResources();

// Fetch interval - every 3s
// const intervalId = setInterval(() => {
// getSystemResources();
// }, 3000);
const intervalId = setInterval(() => {
getSystemResources();
}, 3000);

// clean up
// return () => clearInterval(intervalId);
return () => clearInterval(intervalId);
}, []);

return {
Expand Down
Loading

0 comments on commit ed2db5f

Please sign in to comment.