Skip to content

Commit

Permalink
janhq#357 plugin & app can subscribe and emit events (janhq#358)
Browse files Browse the repository at this point in the history
* feature: event based plugin

* chore: update README.md

* Update yarn script for build plugins (janhq#363)

* Update yarn script for build plugins

* Plugin-core install from npmjs instead of from local

---------

Co-authored-by: Hien To <>

* janhq#360 plugin preferences (janhq#361)

* feature: janhq#360 plugin preferences

* chore: update core-plugin README.md

* chore: create collections on start

* chore: bumb core version

* chore: update README

* chore: notify preferences update

* fix: preference update

---------

Co-authored-by: hiento09 <[email protected]>
  • Loading branch information
louis-jan and hiento09 authored Oct 16, 2023
1 parent 237d94e commit 2725843
Show file tree
Hide file tree
Showing 30 changed files with 4,447 additions and 501 deletions.
8 changes: 3 additions & 5 deletions electron/core/plugins/data-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { core, store, RegisterExtensionPoint, StoreService, DataService } from "@janhq/plugin-core";
import { core, store, RegisterExtensionPoint, StoreService, DataService, PluginService } from "@janhq/plugin-core";

// Provide an async method to manipulate the price provided by the extension point
const PluginName = "data-plugin";
const MODULE_PATH = "data-plugin/dist/cjs/module.js";

/**
Expand All @@ -12,7 +12,6 @@ const MODULE_PATH = "data-plugin/dist/cjs/module.js";
*
*/
function createCollection({ name, schema }: { name: string; schema?: { [key: string]: any } }): Promise<void> {
console.log("renderer: creating collection:", name, schema);
return core.invokePluginFunc(MODULE_PATH, "createCollection", name, schema);
}

Expand Down Expand Up @@ -137,8 +136,7 @@ function onStart() {

// Register all the above functions and objects with the relevant extension points
export function init({ register }: { register: RegisterExtensionPoint }) {
onStart();

register(PluginService.OnStart, PluginName, onStart);
register(StoreService.CreateCollection, createCollection.name, createCollection);
register(StoreService.DeleteCollection, deleteCollection.name, deleteCollection);
register(StoreService.InsertOne, insertOne.name, insertOne);
Expand Down
168 changes: 121 additions & 47 deletions electron/core/plugins/data-plugin/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion electron/core/plugins/data-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"node_modules"
],
"dependencies": {
"@janhq/plugin-core": "file:../../../../plugin-core",
"@janhq/plugin-core": "^0.1.7",
"pouchdb-find": "^8.0.1",
"pouchdb-node": "^8.0.1"
}
Expand Down
104 changes: 89 additions & 15 deletions electron/core/plugins/inference-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,97 @@
const MODULE_PATH = "inference-plugin/dist/module.js";

const initModel = async (product) =>
new Promise(async (resolve) => {
if (window.electronAPI) {
window.electronAPI
.invokePluginFunc(MODULE_PATH, "initModel", product)
.then((res) => resolve(res));
}
});
import { EventName, InferenceService, NewMessageRequest, PluginService, core, events, store } from "@janhq/plugin-core";

const inferenceUrl = () => "http://localhost:3928/llama/chat_completion";
const PluginName = "inference-plugin";
const MODULE_PATH = `${PluginName}/dist/module.js`;
const inferenceUrl = "http://localhost:3928/llama/chat_completion";

const initModel = async (product) => core.invokePluginFunc(MODULE_PATH, "initModel", product);

const stopModel = () => {
window.electronAPI.invokePluginFunc(MODULE_PATH, "killSubprocess");
core.invokePluginFunc(MODULE_PATH, "killSubprocess");
};

async function handleMessageRequest(data: NewMessageRequest) {
// TODO: Common collections should be able to access via core functions instead of store
const messageHistory =
(await store.findMany("messages", { conversationId: data.conversationId }, [{ createdAt: "asc" }])) ?? [];
const recentMessages = messageHistory
.filter((e) => e.message !== "" && (e.user === "user" || e.user === "assistant"))
.slice(-10)
.map((message) => {
return {
content: message.message.trim(),
role: message.user === "user" ? "user" : "assistant",
};
});

const message = {
...data,
message: "",
user: "assistant",
createdAt: new Date().toISOString(),
_id: undefined,
};
// TODO: Common collections should be able to access via core functions instead of store
const id = await store.insertOne("messages", message);

message._id = id;
events.emit(EventName.OnNewMessageResponse, message);

const response = await fetch(inferenceUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream",
"Access-Control-Allow-Origi": "*",
},
body: JSON.stringify({
messages: recentMessages,
stream: true,
model: "gpt-3.5-turbo",
max_tokens: 500,
}),
});
const stream = response.body;

const decoder = new TextDecoder("utf-8");
const reader = stream?.getReader();
let answer = "";

while (true && reader) {
const { done, value } = await reader.read();
if (done) {
console.log("SSE stream closed");
break;
}
const text = decoder.decode(value);
const lines = text.trim().split("\n");
for (const line of lines) {
if (line.startsWith("data: ") && !line.includes("data: [DONE]")) {
const data = JSON.parse(line.replace("data: ", ""));
answer += data.choices[0]?.delta?.content ?? "";
if (answer.startsWith("assistant: ")) {
answer = answer.replace("assistant: ", "");
}
message.message = answer;
events.emit(EventName.OnMessageResponseUpdate, message);
}
}
}
message.message = answer.trim();
// TODO: Common collections should be able to access via core functions instead of store
await store.updateOne("messages", message._id, message);
}

const registerListener = () => {
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
};

const onStart = async () => {
registerListener();
};
// Register all the above functions and objects with the relevant extension points
export function init({ register }) {
register("initModel", "initModel", initModel);
register("inferenceUrl", "inferenceUrl", inferenceUrl);
register("stopModel", "stopModel", stopModel);
register(PluginService.OnStart, PluginName, onStart);
register(InferenceService.InitModel, initModel.name, initModel);
register(InferenceService.StopModel, stopModel.name, stopModel);
}
14 changes: 14 additions & 0 deletions electron/core/plugins/inference-plugin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions electron/core/plugins/inference-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@janhq/plugin-core": "^0.1.7",
"kill-port-process": "^3.2.0",
"tcp-port-used": "^1.0.2",
"ts-loader": "^9.5.0"
Expand Down
25 changes: 8 additions & 17 deletions electron/core/plugins/inference-plugin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Language and Environment */
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
/* Modules */
"module": "ES6" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */,
// "paths": {} /* Specify a set of entries that re-map imports to additional lookup locations. */,
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "resolveJsonModule": true, /* Enable importing .json files. */
"target": "es2016",
"module": "ES6",
"moduleResolution": "node",

"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": false /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true
}
}
7 changes: 0 additions & 7 deletions electron/core/plugins/inference-plugin/types/index.d.ts

This file was deleted.

6 changes: 4 additions & 2 deletions electron/core/plugins/model-management-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ModelManagementService, RegisterExtensionPoint, core, store } from "@janhq/plugin-core";
import { ModelManagementService, PluginService, RegisterExtensionPoint, core, store } from "@janhq/plugin-core";

const PluginName = "model-management-plugin";
const MODULE_PATH = "model-management-plugin/dist/module.js";

const getDownloadedModels = () => core.invokePluginFunc(MODULE_PATH, "getDownloadedModels");
Expand Down Expand Up @@ -81,7 +83,7 @@ function onStart() {

// Register all the above functions and objects with the relevant extension points
export function init({ register }: { register: RegisterExtensionPoint }) {
onStart();
register(PluginService.OnStart, PluginName, onStart);

register(ModelManagementService.GetDownloadedModels, getDownloadedModels.name, getDownloadedModels);
register(ModelManagementService.GetAvailableModels, getAvailableModels.name, getAvailableModels);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion electron/core/plugins/model-management-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
],
"dependencies": {
"@huggingface/hub": "^0.8.5",
"@janhq/plugin-core": "file:../../../../plugin-core",
"@janhq/plugin-core": "^0.1.7",
"ts-loader": "^9.5.0"
},
"bundledDependencies": [
Expand Down
115 changes: 115 additions & 0 deletions electron/core/plugins/openai-plugin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
PluginService,
EventName,
NewMessageRequest,
events,
store,
preferences,
RegisterExtensionPoint,
} from "@janhq/plugin-core";
import { Configuration, OpenAIApi } from "azure-openai";

const PluginName = "openai-plugin";

const setRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.setRequestHeader = function newSetRequestHeader(key: string, val: string) {
if (key.toLocaleLowerCase() === "user-agent") {
return;
}
setRequestHeader.apply(this, [key, val]);
};

var openai: OpenAIApi | undefined = undefined;

const setup = async () => {
const apiKey: string = (await preferences.get(PluginName, "apiKey")) ?? "";
const endpoint: string = (await preferences.get(PluginName, "endpoint")) ?? "";
const deploymentName: string = (await preferences.get(PluginName, "deploymentName")) ?? "";
try {
openai = new OpenAIApi(
new Configuration({
azure: {
apiKey, //Your API key goes here
endpoint, //Your endpoint goes here. It is like: "https://endpointname.openai.azure.com/"
deploymentName, //Your deployment name goes here. It is like "chatgpt"
},
})
);
} catch (err) {
openai = undefined;
console.log(err);
}
};

async function onStart() {
setup();
registerListener();
}

async function handleMessageRequest(data: NewMessageRequest) {
if (!openai) {
const message = {
...data,
message: "Your API key is not set. Please set it in the plugin preferences.",
user: "GPT-3",
avatar: "https://static-assets.jan.ai/openai-icon.jpg",
createdAt: new Date().toISOString(),
_id: undefined,
};
const id = await store.insertOne("messages", message);
message._id = id;
events.emit(EventName.OnNewMessageResponse, message);
return;
}

const message = {
...data,
message: "",
user: "GPT-3",
avatar: "https://static-assets.jan.ai/openai-icon.jpg",
createdAt: new Date().toISOString(),
_id: undefined,
};
const id = await store.insertOne("messages", message);

message._id = id;
events.emit(EventName.OnNewMessageResponse, message);
const response = await openai.createChatCompletion({
messages: [{ role: "user", content: data.message }],
model: "gpt-3.5-turbo",
});
message.message = response.data.choices[0].message.content;
events.emit(EventName.OnMessageResponseUpdate, message);
await store.updateOne("messages", message._id, message);
}

const registerListener = () => {
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
};

const onPreferencesUpdate = () => {
setup();
};
// Register all the above functions and objects with the relevant extension points
export function init({ register }: { register: RegisterExtensionPoint }) {
register(PluginService.OnStart, PluginName, onStart);
register(PluginService.OnPreferencesUpdate, PluginName, onPreferencesUpdate);

preferences.registerPreferences<string>(register, PluginName, "apiKey", "API Key", "Azure Project API Key", "");
preferences.registerPreferences<string>(
register,
PluginName,
"endpoint",
"API Endpoint",
"Azure Deployment Endpoint API",
""
);
preferences.registerPreferences<string>(
register,
PluginName,
"deploymentName",
"Deployment Name",
"The deployment name you chose when you deployed the model",
""
);
}
Loading

0 comments on commit 2725843

Please sign in to comment.