feat: class-based plugin manager
chore: add facades refactor: core module export refactor: inference plugin - deprecate function registering (#537) * refactor: revamp inference plugin as class - deprecate function registering * refactor: monitoring plugin - deprecate service registering (#538) refactor: revamp inference plugin as class - deprecate function registering chore: update import refactor: plugin revamp - model management chore: update build steps and remove experimental plugins refactor: remove pluggable electron chore: add sorting for conversations chore: build plugins for testing chore: consistent plugin directory name chore: docs chore: fix CI chore: update conversation prefix
This commit is contained in:
parent
f4772fb8bc
commit
96dba2690d
6
.github/workflows/jan-electron-build.yml
vendored
6
.github/workflows/jan-electron-build.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
||||
- name: Install yarn dependencies
|
||||
run: |
|
||||
yarn install
|
||||
yarn build:pull-plugins
|
||||
yarn build:plugins
|
||||
env:
|
||||
APP_PATH: "."
|
||||
DEVELOPER_ID: ${{ secrets.DEVELOPER_ID }}
|
||||
@ -104,7 +104,7 @@ jobs:
|
||||
run: |
|
||||
yarn config set network-timeout 300000
|
||||
yarn install
|
||||
yarn build:pull-plugins
|
||||
yarn build:plugins
|
||||
|
||||
- name: Build and publish app
|
||||
run: |
|
||||
@ -153,7 +153,7 @@ jobs:
|
||||
run: |
|
||||
yarn config set network-timeout 300000
|
||||
yarn install
|
||||
yarn build:pull-plugins
|
||||
yarn build:plugins
|
||||
|
||||
- name: Build and publish app
|
||||
run: |
|
||||
|
||||
@ -44,9 +44,10 @@ jobs:
|
||||
- name: Linter and test
|
||||
run: |
|
||||
yarn config set network-timeout 300000
|
||||
yarn build:core
|
||||
yarn install
|
||||
yarn lint
|
||||
yarn build:pull-plugins
|
||||
yarn build:plugins
|
||||
yarn build:test
|
||||
yarn test
|
||||
env:
|
||||
@ -75,8 +76,9 @@ jobs:
|
||||
- name: Linter and test
|
||||
run: |
|
||||
yarn config set network-timeout 300000
|
||||
yarn build:core
|
||||
yarn install
|
||||
yarn build:pull-plugins
|
||||
yarn build:plugins
|
||||
yarn build:test-win32
|
||||
yarn test
|
||||
|
||||
@ -103,7 +105,8 @@ jobs:
|
||||
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
|
||||
echo -e "Display ID: $DISPLAY"
|
||||
yarn config set network-timeout 300000
|
||||
yarn build:core
|
||||
yarn install
|
||||
yarn build:pull-plugins
|
||||
yarn build:plugins
|
||||
yarn build:test-linux
|
||||
yarn test
|
||||
18
.github/workflows/jan-plugins.yml
vendored
18
.github/workflows/jan-plugins.yml
vendored
@ -54,6 +54,11 @@ jobs:
|
||||
for dir in $(cat /tmp/change_dir.txt)
|
||||
do
|
||||
echo "$dir"
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract current version
|
||||
current_version=$(jq -r '.version' $dir/package.json)
|
||||
|
||||
@ -80,6 +85,11 @@ jobs:
|
||||
with:
|
||||
node-version: "20.x"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: Build core module
|
||||
run: |
|
||||
cd core
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Publish npm packages
|
||||
run: |
|
||||
@ -87,6 +97,10 @@ jobs:
|
||||
for dir in $(cat /tmp/change_dir.txt)
|
||||
do
|
||||
echo $dir
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
||||
continue
|
||||
fi
|
||||
cd $dir
|
||||
npm install
|
||||
if [[ $dir == 'data-plugin' ]]; then
|
||||
@ -112,6 +126,10 @@ jobs:
|
||||
for dir in $(cat /tmp/change_dir.txt)
|
||||
do
|
||||
echo "$dir"
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Directory $dir does not exist, plugin might be removed, skipping..."
|
||||
continue
|
||||
fi
|
||||
version=$(jq -r '.version' plugins/$dir/package.json)
|
||||
git config --global user.email "service@jan.ai"
|
||||
git config --global user.name "Service Account"
|
||||
|
||||
304
core/index.ts
304
core/index.ts
@ -1,304 +0,0 @@
|
||||
/**
|
||||
* CoreService exports
|
||||
*/
|
||||
|
||||
export type CoreService =
|
||||
| StoreService
|
||||
| DataService
|
||||
| InferenceService
|
||||
| ModelManagementService
|
||||
| SystemMonitoringService
|
||||
| PreferenceService
|
||||
| PluginService;
|
||||
|
||||
/**
|
||||
* Represents the available methods for the StoreService.
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum StoreService {
|
||||
/**
|
||||
* Creates a new collection in the database store.
|
||||
*/
|
||||
CreateCollection = "createCollection",
|
||||
|
||||
/**
|
||||
* Deletes an existing collection from the database store.
|
||||
*/
|
||||
DeleteCollection = "deleteCollection",
|
||||
|
||||
/**
|
||||
* Inserts a new value into an existing collection in the database store.
|
||||
*/
|
||||
InsertOne = "insertOne",
|
||||
|
||||
/**
|
||||
* Updates an existing value in an existing collection in the database store.
|
||||
*/
|
||||
UpdateOne = "updateOne",
|
||||
|
||||
/**
|
||||
* Updates multiple records in a collection in the database store.
|
||||
*/
|
||||
UpdateMany = "updateMany",
|
||||
|
||||
/**
|
||||
* Deletes an existing value from an existing collection in the database store.
|
||||
*/
|
||||
DeleteOne = "deleteOne",
|
||||
|
||||
/**
|
||||
* Delete multiple records in a collection in the database store.
|
||||
*/
|
||||
DeleteMany = "deleteMany",
|
||||
|
||||
/**
|
||||
* Retrieve multiple records from a collection in the data store
|
||||
*/
|
||||
FindMany = "findMany",
|
||||
|
||||
/**
|
||||
* Retrieve a record from a collection in the data store.
|
||||
*/
|
||||
FindOne = "findOne",
|
||||
}
|
||||
|
||||
/**
|
||||
* DataService exports.
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum DataService {
|
||||
/**
|
||||
* Gets a list of conversations.
|
||||
*/
|
||||
GetConversations = "getConversations",
|
||||
|
||||
/**
|
||||
* Creates a new conversation.
|
||||
*/
|
||||
CreateConversation = "createConversation",
|
||||
|
||||
/**
|
||||
* Updates an existing conversation.
|
||||
*/
|
||||
UpdateConversation = "updateConversation",
|
||||
|
||||
/**
|
||||
* Deletes an existing conversation.
|
||||
*/
|
||||
DeleteConversation = "deleteConversation",
|
||||
|
||||
/**
|
||||
* Creates a new message in an existing conversation.
|
||||
*/
|
||||
CreateMessage = "createMessage",
|
||||
|
||||
/**
|
||||
* Updates an existing message in an existing conversation.
|
||||
*/
|
||||
UpdateMessage = "updateMessage",
|
||||
|
||||
/**
|
||||
* Gets a list of messages for an existing conversation.
|
||||
*/
|
||||
GetConversationMessages = "getConversationMessages",
|
||||
|
||||
/**
|
||||
* Gets a conversation matching an ID.
|
||||
*/
|
||||
GetConversationById = "getConversationById",
|
||||
|
||||
/**
|
||||
* Creates a new conversation using the prompt instruction.
|
||||
*/
|
||||
CreateBot = "createBot",
|
||||
|
||||
/**
|
||||
* Gets all created bots.
|
||||
*/
|
||||
GetBots = "getBots",
|
||||
|
||||
/**
|
||||
* Gets a bot matching an ID.
|
||||
*/
|
||||
GetBotById = "getBotById",
|
||||
|
||||
/**
|
||||
* Deletes a bot matching an ID.
|
||||
*/
|
||||
DeleteBot = "deleteBot",
|
||||
|
||||
/**
|
||||
* Updates a bot matching an ID.
|
||||
*/
|
||||
UpdateBot = "updateBot",
|
||||
|
||||
/**
|
||||
* Gets the plugin manifest.
|
||||
*/
|
||||
GetPluginManifest = "getPluginManifest",
|
||||
}
|
||||
|
||||
/**
|
||||
* InferenceService exports.
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum InferenceService {
|
||||
/**
|
||||
* Initializes a model for inference.
|
||||
*/
|
||||
InitModel = "initModel",
|
||||
|
||||
/**
|
||||
* Stops a running inference model.
|
||||
*/
|
||||
StopModel = "stopModel",
|
||||
|
||||
/**
|
||||
* Single inference response.
|
||||
*/
|
||||
InferenceRequest = "inferenceRequest",
|
||||
}
|
||||
|
||||
/**
|
||||
* ModelManagementService exports.
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum ModelManagementService {
|
||||
/**
|
||||
* Deletes a downloaded model.
|
||||
*/
|
||||
DeleteModel = "deleteModel",
|
||||
|
||||
/**
|
||||
* Downloads a model from the server.
|
||||
*/
|
||||
DownloadModel = "downloadModel",
|
||||
|
||||
/**
|
||||
* Gets configued models from the database.
|
||||
*/
|
||||
GetConfiguredModels = "getConfiguredModels",
|
||||
|
||||
/**
|
||||
* Stores a model in the database.
|
||||
*/
|
||||
StoreModel = "storeModel",
|
||||
|
||||
/**
|
||||
* Updates the finished download time for a model in the database.
|
||||
*/
|
||||
UpdateFinishedDownloadAt = "updateFinishedDownloadAt",
|
||||
|
||||
/**
|
||||
* Gets a list of finished download models from the database.
|
||||
*/
|
||||
GetFinishedDownloadModels = "getFinishedDownloadModels",
|
||||
|
||||
/**
|
||||
* Deletes a download model from the database.
|
||||
*/
|
||||
DeleteDownloadModel = "deleteDownloadModel",
|
||||
|
||||
/**
|
||||
* Gets a model by its ID from the database.
|
||||
*/
|
||||
GetModelById = "getModelById",
|
||||
}
|
||||
|
||||
/**
|
||||
* PreferenceService exports.
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum PreferenceService {
|
||||
/**
|
||||
* The experiment component for which preferences are being managed.
|
||||
*/
|
||||
ExperimentComponent = "experimentComponent",
|
||||
|
||||
/**
|
||||
* Gets the plugin preferences.
|
||||
*/
|
||||
PluginPreferences = "pluginPreferences",
|
||||
}
|
||||
|
||||
/**
|
||||
* SystemMonitoringService exports.
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum SystemMonitoringService {
|
||||
/**
|
||||
* Gets information about system resources.
|
||||
*/
|
||||
GetResourcesInfo = "getResourcesInfo",
|
||||
|
||||
/**
|
||||
* Gets the current system load.
|
||||
*/
|
||||
GetCurrentLoad = "getCurrentLoad",
|
||||
}
|
||||
|
||||
/**
|
||||
* PluginService exports.
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum PluginService {
|
||||
/**
|
||||
* The plugin is being started.
|
||||
*/
|
||||
OnStart = "pluginOnStart",
|
||||
|
||||
/**
|
||||
* The plugin is being started.
|
||||
*/
|
||||
OnPreferencesUpdate = "pluginPreferencesUpdate",
|
||||
|
||||
/**
|
||||
* The plugin is being stopped.
|
||||
*/
|
||||
OnStop = "pluginOnStop",
|
||||
|
||||
/**
|
||||
* The plugin is being destroyed.
|
||||
*/
|
||||
OnDestroy = "pluginOnDestroy",
|
||||
}
|
||||
|
||||
/**
|
||||
* Store module exports.
|
||||
* @module
|
||||
*/
|
||||
export { store } from "./store";
|
||||
|
||||
/**
|
||||
* @deprecated This object is deprecated and should not be used.
|
||||
* Use individual functions instead.
|
||||
*/
|
||||
export { core } from "./core";
|
||||
|
||||
/**
|
||||
* Core module exports.
|
||||
* @module
|
||||
*/
|
||||
export {
|
||||
RegisterExtensionPoint,
|
||||
deleteFile,
|
||||
downloadFile,
|
||||
invokePluginFunc,
|
||||
} from "./core";
|
||||
|
||||
/**
|
||||
* Events module exports.
|
||||
* @module
|
||||
*/
|
||||
export {
|
||||
events,
|
||||
EventName,
|
||||
NewMessageRequest,
|
||||
NewMessageResponse,
|
||||
} from "./events";
|
||||
|
||||
/**
|
||||
* Preferences module exports.
|
||||
* @module
|
||||
*/
|
||||
export { preferences } from "./preferences";
|
||||
@ -17,7 +17,7 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./store": "./lib/store.js"
|
||||
"./plugin": "./lib/plugins/index.js"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
import { store } from "./store";
|
||||
|
||||
/**
|
||||
* Returns the value of the specified preference for the specified plugin.
|
||||
*
|
||||
* @param pluginName The name of the plugin.
|
||||
* @param preferenceKey The key of the preference.
|
||||
* @returns A promise that resolves to the value of the preference.
|
||||
*/
|
||||
function get(pluginName: string, preferenceKey: string): Promise<any> {
|
||||
return store
|
||||
.createCollection("preferences", {})
|
||||
.then(() => store.findOne("preferences", `${pluginName}.${preferenceKey}`))
|
||||
.then((doc) => doc?.value ?? "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the specified preference for the specified plugin.
|
||||
*
|
||||
* @param pluginName The name of the plugin.
|
||||
* @param preferenceKey The key of the preference.
|
||||
* @param value The value of the preference.
|
||||
* @returns A promise that resolves when the preference has been set.
|
||||
*/
|
||||
function set(pluginName: string, preferenceKey: string, value: any): Promise<any> {
|
||||
return store
|
||||
.createCollection("preferences", {})
|
||||
.then(() =>
|
||||
store
|
||||
.findOne("preferences", `${pluginName}.${preferenceKey}`)
|
||||
.then((doc) =>
|
||||
doc
|
||||
? store.updateOne("preferences", `${pluginName}.${preferenceKey}`, { value })
|
||||
: store.insertOne("preferences", { _id: `${pluginName}.${preferenceKey}`, value })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all preferences for the specified plugin.
|
||||
*
|
||||
* @param pluginName The name of the plugin.
|
||||
* @returns A promise that resolves when the preferences have been cleared.
|
||||
*/
|
||||
function clear(pluginName: string): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a preference with the specified default value.
|
||||
*
|
||||
* @param register The function to use for registering the preference.
|
||||
* @param pluginName The name of the plugin.
|
||||
* @param preferenceKey The key of the preference.
|
||||
* @param preferenceName The name of the preference.
|
||||
* @param preferenceDescription The description of the preference.
|
||||
* @param defaultValue The default value of the preference.
|
||||
*/
|
||||
function registerPreferences<T>(
|
||||
register: Function,
|
||||
pluginName: string,
|
||||
preferenceKey: string,
|
||||
preferenceName: string,
|
||||
preferenceDescription: string,
|
||||
defaultValue: T
|
||||
) {
|
||||
register("PluginPreferences", `${pluginName}.${preferenceKey}`, () => ({
|
||||
pluginName,
|
||||
preferenceKey,
|
||||
preferenceName,
|
||||
preferenceDescription,
|
||||
defaultValue,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* An object that provides methods for getting, setting, and clearing preferences.
|
||||
*/
|
||||
export const preferences = {
|
||||
get,
|
||||
set,
|
||||
clear,
|
||||
registerPreferences,
|
||||
};
|
||||
@ -7,7 +7,23 @@
|
||||
* @returns Promise<any>
|
||||
*
|
||||
*/
|
||||
const invokePluginFunc: (plugin: string, method: string, ...args: any[]) => Promise<any> = (plugin, method, ...args) =>
|
||||
const executeOnMain: (
|
||||
plugin: string,
|
||||
method: string,
|
||||
...args: any[]
|
||||
) => Promise<any> = (plugin, method, ...args) =>
|
||||
window.coreAPI?.invokePluginFunc(plugin, method, ...args) ??
|
||||
window.electronAPI?.invokePluginFunc(plugin, method, ...args);
|
||||
|
||||
/**
|
||||
* @deprecated This object is deprecated and should not be used.
|
||||
* Use individual functions instead.
|
||||
*/
|
||||
const invokePluginFunc: (
|
||||
plugin: string,
|
||||
method: string,
|
||||
...args: any[]
|
||||
) => Promise<any> = (plugin, method, ...args) =>
|
||||
window.coreAPI?.invokePluginFunc(plugin, method, ...args) ??
|
||||
window.electronAPI?.invokePluginFunc(plugin, method, ...args);
|
||||
|
||||
@ -17,8 +33,12 @@ const invokePluginFunc: (plugin: string, method: string, ...args: any[]) => Prom
|
||||
* @param {string} fileName - The name to use for the downloaded file.
|
||||
* @returns {Promise<any>} A promise that resolves when the file is downloaded.
|
||||
*/
|
||||
const downloadFile: (url: string, fileName: string) => Promise<any> = (url, fileName) =>
|
||||
window.coreAPI?.downloadFile(url, fileName) ?? window.electronAPI?.downloadFile(url, fileName);
|
||||
const downloadFile: (url: string, fileName: string) => Promise<any> = (
|
||||
url,
|
||||
fileName
|
||||
) =>
|
||||
window.coreAPI?.downloadFile(url, fileName) ??
|
||||
window.electronAPI?.downloadFile(url, fileName);
|
||||
|
||||
/**
|
||||
* Deletes a file from the local file system.
|
||||
@ -51,6 +71,7 @@ export type RegisterExtensionPoint = (
|
||||
*/
|
||||
export const core = {
|
||||
invokePluginFunc,
|
||||
executeOnMain,
|
||||
downloadFile,
|
||||
deleteFile,
|
||||
appDataPath,
|
||||
@ -59,4 +80,10 @@ export const core = {
|
||||
/**
|
||||
* Functions exports
|
||||
*/
|
||||
export { invokePluginFunc, downloadFile, deleteFile, appDataPath };
|
||||
export {
|
||||
invokePluginFunc,
|
||||
executeOnMain,
|
||||
downloadFile,
|
||||
deleteFile,
|
||||
appDataPath,
|
||||
};
|
||||
@ -6,12 +6,16 @@ export enum EventName {
|
||||
OnNewMessageRequest = "onNewMessageRequest",
|
||||
OnNewMessageResponse = "onNewMessageResponse",
|
||||
OnMessageResponseUpdate = "onMessageResponseUpdate",
|
||||
OnMessageResponseFinished = "OnMessageResponseFinished",
|
||||
OnMessageResponseFinished = "onMessageResponseFinished",
|
||||
OnDownloadUpdate = "onDownloadUpdate",
|
||||
OnDownloadSuccess = "onDownloadSuccess",
|
||||
OnDownloadError = "onDownloadError",
|
||||
}
|
||||
|
||||
export type MessageHistory = {
|
||||
role: string;
|
||||
content: string;
|
||||
};
|
||||
/**
|
||||
* The `NewMessageRequest` type defines the shape of a new message request object.
|
||||
*/
|
||||
@ -23,6 +27,7 @@ export type NewMessageRequest = {
|
||||
message?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
history?: MessageHistory[];
|
||||
};
|
||||
|
||||
/**
|
||||
49
core/src/fs.ts
Normal file
49
core/src/fs.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Writes data to a file at the specified path.
|
||||
* @param {string} path - The path to the file.
|
||||
* @param {string} data - The data to write to the file.
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is written successfully.
|
||||
*/
|
||||
const writeFile: (path: string, data: string) => Promise<any> = (path, data) =>
|
||||
window.coreAPI?.writeFile(path, data) ??
|
||||
window.electronAPI?.writeFile(path, data);
|
||||
|
||||
/**
|
||||
* Reads the contents of a file at the specified path.
|
||||
* @param {string} path - The path of the file to read.
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the file.
|
||||
*/
|
||||
const readFile: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.readFile(path) ?? window.electronAPI?.readFile(path);
|
||||
|
||||
/**
|
||||
* List the directory files
|
||||
* @param {string} path - The path of the directory to list files.
|
||||
* @returns {Promise<any>} A Promise that resolves with the contents of the directory.
|
||||
*/
|
||||
const listFiles: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.listFiles(path) ?? window.electronAPI?.listFiles(path);
|
||||
|
||||
/**
|
||||
* Creates a directory at the specified path.
|
||||
* @param {string} path - The path of the directory to create.
|
||||
* @returns {Promise<any>} A Promise that resolves when the directory is created successfully.
|
||||
*/
|
||||
const mkdir: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.mkdir(path) ?? window.electronAPI?.mkdir(path);
|
||||
|
||||
/**
|
||||
* Deletes a file from the local file system.
|
||||
* @param {string} path - The path of the file to delete.
|
||||
* @returns {Promise<any>} A Promise that resolves when the file is deleted.
|
||||
*/
|
||||
const deleteFile: (path: string) => Promise<any> = (path) =>
|
||||
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);
|
||||
|
||||
export const fs = {
|
||||
writeFile,
|
||||
readFile,
|
||||
listFiles,
|
||||
mkdir,
|
||||
deleteFile,
|
||||
};
|
||||
40
core/src/index.ts
Normal file
40
core/src/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @deprecated This object is deprecated and should not be used.
|
||||
* Use individual functions instead.
|
||||
*/
|
||||
export { core, deleteFile, invokePluginFunc } from "./core";
|
||||
|
||||
/**
|
||||
* Core module exports.
|
||||
* @module
|
||||
*/
|
||||
export { downloadFile, executeOnMain } from "./core";
|
||||
|
||||
/**
|
||||
* Events module exports.
|
||||
* @module
|
||||
*/
|
||||
export { events } from "./events";
|
||||
|
||||
/**
|
||||
* Events types exports.
|
||||
* @module
|
||||
*/
|
||||
export {
|
||||
EventName,
|
||||
NewMessageRequest,
|
||||
NewMessageResponse,
|
||||
MessageHistory,
|
||||
} from "./events";
|
||||
|
||||
/**
|
||||
* Filesystem module exports.
|
||||
* @module
|
||||
*/
|
||||
export { fs } from "./fs";
|
||||
|
||||
/**
|
||||
* Plugin base module export.
|
||||
* @module
|
||||
*/
|
||||
export { JanPlugin, PluginType } from "./plugin";
|
||||
13
core/src/plugin.ts
Normal file
13
core/src/plugin.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export enum PluginType {
|
||||
Conversational = "conversational",
|
||||
Inference = "inference",
|
||||
Preference = "preference",
|
||||
SystemMonitoring = "systemMonitoring",
|
||||
Model = "model",
|
||||
}
|
||||
|
||||
export abstract class JanPlugin {
|
||||
abstract type(): PluginType;
|
||||
abstract onLoad(): void;
|
||||
abstract onUnload(): void;
|
||||
}
|
||||
32
core/src/plugins/conversational.ts
Normal file
32
core/src/plugins/conversational.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { JanPlugin } from "../plugin";
|
||||
import { Conversation } from "../types/index";
|
||||
|
||||
/**
|
||||
* Abstract class for conversational plugins.
|
||||
* @abstract
|
||||
* @extends JanPlugin
|
||||
*/
|
||||
export abstract class ConversationalPlugin extends JanPlugin {
|
||||
/**
|
||||
* Returns a list of conversations.
|
||||
* @abstract
|
||||
* @returns {Promise<any[]>} A promise that resolves to an array of conversations.
|
||||
*/
|
||||
abstract getConversations(): Promise<any[]>;
|
||||
|
||||
/**
|
||||
* Saves a conversation.
|
||||
* @abstract
|
||||
* @param {Conversation} conversation - The conversation to save.
|
||||
* @returns {Promise<void>} A promise that resolves when the conversation is saved.
|
||||
*/
|
||||
abstract saveConversation(conversation: Conversation): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes a conversation.
|
||||
* @abstract
|
||||
* @param {string} conversationId - The ID of the conversation to delete.
|
||||
* @returns {Promise<void>} A promise that resolves when the conversation is deleted.
|
||||
*/
|
||||
abstract deleteConversation(conversationId: string): Promise<void>;
|
||||
}
|
||||
20
core/src/plugins/index.ts
Normal file
20
core/src/plugins/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Conversational plugin. Persists and retrieves conversations.
|
||||
* @module
|
||||
*/
|
||||
export { ConversationalPlugin } from "./conversational";
|
||||
|
||||
/**
|
||||
* Inference plugin. Start, stop and inference models.
|
||||
*/
|
||||
export { InferencePlugin } from "./inference";
|
||||
|
||||
/**
|
||||
* Monitoring plugin for system monitoring.
|
||||
*/
|
||||
export { MonitoringPlugin } from "./monitoring";
|
||||
|
||||
/**
|
||||
* Model plugin for managing models.
|
||||
*/
|
||||
export { ModelPlugin } from "./model";
|
||||
25
core/src/plugins/inference.ts
Normal file
25
core/src/plugins/inference.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { NewMessageRequest } from "../events";
|
||||
import { JanPlugin } from "../plugin";
|
||||
|
||||
/**
|
||||
* An abstract class representing an Inference Plugin for Jan.
|
||||
*/
|
||||
export abstract class InferencePlugin extends JanPlugin {
|
||||
/**
|
||||
* Initializes the model for the plugin.
|
||||
* @param modelFileName - The name of the file containing the model.
|
||||
*/
|
||||
abstract initModel(modelFileName: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stops the model for the plugin.
|
||||
*/
|
||||
abstract stopModel(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Processes an inference request.
|
||||
* @param data - The data for the inference request.
|
||||
* @returns The result of the inference request.
|
||||
*/
|
||||
abstract inferenceRequest(data: NewMessageRequest): Promise<any>;
|
||||
}
|
||||
44
core/src/plugins/model.ts
Normal file
44
core/src/plugins/model.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Represents a plugin for managing machine learning models.
|
||||
* @abstract
|
||||
*/
|
||||
import { JanPlugin } from "../plugin";
|
||||
import { Model, ModelCatalog } from "../types/index";
|
||||
|
||||
/**
|
||||
* An abstract class representing a plugin for managing machine learning models.
|
||||
*/
|
||||
export abstract class ModelPlugin extends JanPlugin {
|
||||
/**
|
||||
* Downloads a model.
|
||||
* @param model - The model to download.
|
||||
* @returns A Promise that resolves when the model has been downloaded.
|
||||
*/
|
||||
abstract downloadModel(model: Model): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes a model.
|
||||
* @param filePath - The file path of the model to delete.
|
||||
* @returns A Promise that resolves when the model has been deleted.
|
||||
*/
|
||||
abstract deleteModel(filePath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Saves a model.
|
||||
* @param model - The model to save.
|
||||
* @returns A Promise that resolves when the model has been saved.
|
||||
*/
|
||||
abstract saveModel(model: Model): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets a list of downloaded models.
|
||||
* @returns A Promise that resolves with an array of downloaded models.
|
||||
*/
|
||||
abstract getDownloadedModels(): Promise<Model[]>;
|
||||
|
||||
/**
|
||||
* Gets a list of configured models.
|
||||
* @returns A Promise that resolves with an array of configured models.
|
||||
*/
|
||||
abstract getConfiguredModels(): Promise<ModelCatalog[]>;
|
||||
}
|
||||
19
core/src/plugins/monitoring.ts
Normal file
19
core/src/plugins/monitoring.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { JanPlugin } from "../plugin";
|
||||
|
||||
/**
|
||||
* Abstract class for monitoring plugins.
|
||||
* @extends JanPlugin
|
||||
*/
|
||||
export abstract class MonitoringPlugin extends JanPlugin {
|
||||
/**
|
||||
* Returns information about the system resources.
|
||||
* @returns {Promise<any>} A promise that resolves with the system resources information.
|
||||
*/
|
||||
abstract getResourcesInfo(): Promise<any>;
|
||||
|
||||
/**
|
||||
* Returns the current system load.
|
||||
* @returns {Promise<any>} A promise that resolves with the current system load.
|
||||
*/
|
||||
abstract getCurrentLoad(): Promise<any>;
|
||||
}
|
||||
91
core/src/types/index.ts
Normal file
91
core/src/types/index.ts
Normal file
@ -0,0 +1,91 @@
|
||||
export interface Conversation {
|
||||
_id: string;
|
||||
modelId?: string;
|
||||
botId?: string;
|
||||
name: string;
|
||||
message?: string;
|
||||
summary?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
messages: Message[];
|
||||
}
|
||||
export interface Message {
|
||||
message?: string;
|
||||
user?: string;
|
||||
_id: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
/**
|
||||
* Combination of owner and model name.
|
||||
* Being used as file name. MUST be unique.
|
||||
*/
|
||||
_id: string;
|
||||
name: string;
|
||||
quantMethod: string;
|
||||
bits: number;
|
||||
size: number;
|
||||
maxRamRequired: number;
|
||||
usecase: string;
|
||||
downloadLink: string;
|
||||
modelFile?: string;
|
||||
/**
|
||||
* For tracking download info
|
||||
*/
|
||||
startDownloadAt?: number;
|
||||
finishDownloadAt?: number;
|
||||
productId: string;
|
||||
productName: string;
|
||||
shortDescription: string;
|
||||
longDescription: string;
|
||||
avatarUrl: string;
|
||||
author: string;
|
||||
version: string;
|
||||
modelUrl: string;
|
||||
createdAt: number;
|
||||
updatedAt?: number;
|
||||
status: string;
|
||||
releaseDate: number;
|
||||
tags: string[];
|
||||
}
|
||||
export interface ModelCatalog {
|
||||
_id: string;
|
||||
name: string;
|
||||
shortDescription: string;
|
||||
avatarUrl: string;
|
||||
longDescription: string;
|
||||
author: string;
|
||||
version: string;
|
||||
modelUrl: string;
|
||||
createdAt: number;
|
||||
updatedAt?: number;
|
||||
status: string;
|
||||
releaseDate: number;
|
||||
tags: string[];
|
||||
availableVersions: ModelVersion[];
|
||||
}
|
||||
/**
|
||||
* Model type which will be stored in the database
|
||||
*/
|
||||
export type ModelVersion = {
|
||||
/**
|
||||
* Combination of owner and model name.
|
||||
* Being used as file name. Should be unique.
|
||||
*/
|
||||
_id: string;
|
||||
name: string;
|
||||
quantMethod: string;
|
||||
bits: number;
|
||||
size: number;
|
||||
maxRamRequired: number;
|
||||
usecase: string;
|
||||
downloadLink: string;
|
||||
productId: string;
|
||||
/**
|
||||
* For tracking download state
|
||||
*/
|
||||
startDownloadAt?: number;
|
||||
finishDownloadAt?: number;
|
||||
};
|
||||
129
core/store.ts
129
core/store.ts
@ -1,129 +0,0 @@
|
||||
/**
|
||||
* Creates, reads, updates, and deletes data in a data store.
|
||||
* @module
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new collection in the data store.
|
||||
* @param {string} name - The name of the collection to create.
|
||||
* @param { [key: string]: any } schema - schema of the collection to create, include fields and their types
|
||||
* @returns {Promise<void>} A promise that resolves when the collection is created.
|
||||
*/
|
||||
function createCollection(
|
||||
name: string,
|
||||
schema: { [key: string]: any }
|
||||
): Promise<void> {
|
||||
return window.corePlugin?.store?.createCollection(name, schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a collection from the data store.
|
||||
* @param {string} name - The name of the collection to delete.
|
||||
* @returns {Promise<void>} A promise that resolves when the collection is deleted.
|
||||
*/
|
||||
function deleteCollection(name: string): Promise<void> {
|
||||
return window.corePlugin?.store?.deleteCollection(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a value into a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection to insert the value into.
|
||||
* @param {any} value - The value to insert into the collection.
|
||||
* @returns {Promise<any>} A promise that resolves with the inserted value.
|
||||
*/
|
||||
function insertOne(collectionName: string, value: any): Promise<any> {
|
||||
return window.corePlugin?.store?.insertOne(collectionName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a record from a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection containing the record to retrieve.
|
||||
* @param {string} key - The key of the record to retrieve.
|
||||
* @returns {Promise<any>} A promise that resolves when the record is retrieved.
|
||||
*/
|
||||
function findOne(collectionName: string, key: string): Promise<any> {
|
||||
return window.corePlugin?.store?.findOne(collectionName, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all records that match a selector in a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection to retrieve.
|
||||
* @param {{ [key: string]: any }} selector - The selector to use to get records from the collection.
|
||||
* @param {[{ [key: string]: any }]} sort - The sort options to use to retrieve records.
|
||||
* @returns {Promise<any>} A promise that resolves when all records are retrieved.
|
||||
*/
|
||||
function findMany(
|
||||
collectionName: string,
|
||||
selector?: { [key: string]: any },
|
||||
sort?: [{ [key: string]: any }]
|
||||
): Promise<any> {
|
||||
return window.corePlugin?.store?.findMany(collectionName, selector, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the value of a record in a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection containing the record to update.
|
||||
* @param {string} key - The key of the record to update.
|
||||
* @param {any} value - The new value for the record.
|
||||
* @returns {Promise<void>} A promise that resolves when the record is updated.
|
||||
*/
|
||||
function updateOne(
|
||||
collectionName: string,
|
||||
key: string,
|
||||
value: any
|
||||
): Promise<void> {
|
||||
return window.corePlugin?.store?.updateOne(collectionName, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all records that match a selector in a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection containing the records to update.
|
||||
* @param {{ [key: string]: any }} selector - The selector to use to get the records to update.
|
||||
* @param {any} value - The new value for the records.
|
||||
* @returns {Promise<void>} A promise that resolves when the records are updated.
|
||||
*/
|
||||
function updateMany(
|
||||
collectionName: string,
|
||||
value: any,
|
||||
selector?: { [key: string]: any }
|
||||
): Promise<void> {
|
||||
return window.corePlugin?.store?.updateMany(collectionName, selector, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a single record from a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection containing the record to delete.
|
||||
* @param {string} key - The key of the record to delete.
|
||||
* @returns {Promise<void>} A promise that resolves when the record is deleted.
|
||||
*/
|
||||
function deleteOne(collectionName: string, key: string): Promise<void> {
|
||||
return window.corePlugin?.store?.deleteOne(collectionName, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all records with a matching key from a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection to delete the records from.
|
||||
* @param {{ [key: string]: any }} selector - The selector to use to get the records to delete.
|
||||
* @returns {Promise<void>} A promise that resolves when the records are deleted.
|
||||
*/
|
||||
function deleteMany(
|
||||
collectionName: string,
|
||||
selector?: { [key: string]: any }
|
||||
): Promise<void> {
|
||||
return window.corePlugin?.store?.deleteMany(collectionName, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the data store operations as an object.
|
||||
*/
|
||||
export const store = {
|
||||
createCollection,
|
||||
deleteCollection,
|
||||
insertOne,
|
||||
findOne,
|
||||
findMany,
|
||||
updateOne,
|
||||
updateMany,
|
||||
deleteOne,
|
||||
deleteMany,
|
||||
};
|
||||
@ -7,7 +7,9 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
"declaration": true,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"exclude": ["lib", "node_modules", "**/*.test.ts", "**/__mocks__/*"]
|
||||
}
|
||||
|
||||
96
electron/handlers/fs.ts
Normal file
96
electron/handlers/fs.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { app, ipcMain } from "electron";
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
/**
|
||||
* Handles file system operations.
|
||||
*/
|
||||
export function handleFs() {
|
||||
/**
|
||||
* Reads a file from the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to read.
|
||||
* @returns A promise that resolves with the contents of the file.
|
||||
*/
|
||||
ipcMain.handle("readFile", async (event, path: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(join(app.getPath("userData"), path), "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Writes data to a file in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the file to write to.
|
||||
* @param data - The data to write to the file.
|
||||
* @returns A promise that resolves when the file has been written.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"writeFile",
|
||||
async (event, path: string, data: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(
|
||||
join(app.getPath("userData"), path),
|
||||
data,
|
||||
"utf8",
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to create.
|
||||
* @returns A promise that resolves when the directory has been created.
|
||||
*/
|
||||
ipcMain.handle("mkdir", async (event, path: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.mkdir(
|
||||
join(app.getPath("userData"), path),
|
||||
{ recursive: true },
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Lists the files in a directory in the user data directory.
|
||||
* @param event - The event object.
|
||||
* @param path - The path of the directory to list files from.
|
||||
* @returns A promise that resolves with an array of file names.
|
||||
*/
|
||||
ipcMain.handle(
|
||||
"listFiles",
|
||||
async (event, path: string): Promise<string[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(join(app.getPath("userData"), path), (err, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(files);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -12,6 +12,7 @@ import { rmdir, unlink, createWriteStream } from "fs";
|
||||
import { init } from "./core/plugin/index";
|
||||
import { setupMenu } from "./utils/menu";
|
||||
import { dispose } from "./utils/disposable";
|
||||
import { handleFs } from "./handlers/fs";
|
||||
|
||||
const pacote = require("pacote");
|
||||
const request = require("request");
|
||||
@ -127,6 +128,7 @@ function handleAppUpdates() {
|
||||
* Handles various IPC messages from the renderer process.
|
||||
*/
|
||||
function handleIPCs() {
|
||||
handleFs();
|
||||
/**
|
||||
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
|
||||
* This will change the appearance of the app to the light theme.
|
||||
|
||||
@ -1,7 +1,60 @@
|
||||
/**
|
||||
* Exposes a set of APIs to the renderer process via the contextBridge object.
|
||||
* @remarks
|
||||
* This module is used to make Pluggable Electron's facade available to the renderer on window.plugins.
|
||||
* @module preload
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exposes a set of APIs to the renderer process via the contextBridge object.
|
||||
* @remarks
|
||||
* This module is used to make Pluggable Electron's facade available to the renderer on window.plugins.
|
||||
* @function useFacade
|
||||
* @memberof module:preload
|
||||
* @returns {void}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Exposes a set of APIs to the renderer process via the contextBridge object.
|
||||
* @remarks
|
||||
* This module is used to make Pluggable Electron's facade available to the renderer on window.plugins.
|
||||
* @namespace electronAPI
|
||||
* @memberof module:preload
|
||||
* @property {Function} invokePluginFunc - Invokes a plugin function with the given arguments.
|
||||
* @property {Function} setNativeThemeLight - Sets the native theme to light.
|
||||
* @property {Function} setNativeThemeDark - Sets the native theme to dark.
|
||||
* @property {Function} setNativeThemeSystem - Sets the native theme to system.
|
||||
* @property {Function} basePlugins - Returns the base plugins.
|
||||
* @property {Function} pluginPath - Returns the plugin path.
|
||||
* @property {Function} appDataPath - Returns the app data path.
|
||||
* @property {Function} reloadPlugins - Reloads the plugins.
|
||||
* @property {Function} appVersion - Returns the app version.
|
||||
* @property {Function} openExternalUrl - Opens the given URL in the default browser.
|
||||
* @property {Function} relaunch - Relaunches the app.
|
||||
* @property {Function} openAppDirectory - Opens the app directory.
|
||||
* @property {Function} deleteFile - Deletes the file at the given path.
|
||||
* @property {Function} readFile - Reads the file at the given path.
|
||||
* @property {Function} writeFile - Writes the given data to the file at the given path.
|
||||
* @property {Function} listFiles - Lists the files in the directory at the given path.
|
||||
* @property {Function} mkdir - Creates a directory at the given path.
|
||||
* @property {Function} installRemotePlugin - Installs the remote plugin with the given name.
|
||||
* @property {Function} downloadFile - Downloads the file at the given URL to the given path.
|
||||
* @property {Function} pauseDownload - Pauses the download of the file with the given name.
|
||||
* @property {Function} resumeDownload - Resumes the download of the file with the given name.
|
||||
* @property {Function} abortDownload - Aborts the download of the file with the given name.
|
||||
* @property {Function} onFileDownloadUpdate - Registers a callback to be called when a file download is updated.
|
||||
* @property {Function} onFileDownloadError - Registers a callback to be called when a file download encounters an error.
|
||||
* @property {Function} onFileDownloadSuccess - Registers a callback to be called when a file download is completed successfully.
|
||||
* @property {Function} onAppUpdateDownloadUpdate - Registers a callback to be called when an app update download is updated.
|
||||
* @property {Function} onAppUpdateDownloadError - Registers a callback to be called when an app update download encounters an error.
|
||||
* @property {Function} onAppUpdateDownloadSuccess - Registers a callback to be called when an app update download is completed successfully.
|
||||
*/
|
||||
|
||||
// Make Pluggable Electron's facade available to the renderer on window.plugins
|
||||
import { useFacade } from "./core/plugin/facade";
|
||||
|
||||
useFacade();
|
||||
//@ts-ignore
|
||||
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
@ -32,6 +85,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
|
||||
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
||||
|
||||
readFile: (path: string) => ipcRenderer.invoke("readFile", path),
|
||||
|
||||
writeFile: (path: string, data: string) =>
|
||||
ipcRenderer.invoke("writeFile", path, data),
|
||||
|
||||
listFiles: (path: string) => ipcRenderer.invoke("listFiles", path),
|
||||
|
||||
mkdir: (path: string) => ipcRenderer.invoke("mkdir", path),
|
||||
|
||||
installRemotePlugin: (pluginName: string) =>
|
||||
ipcRenderer.invoke("installRemotePlugin", pluginName),
|
||||
|
||||
|
||||
17
package.json
17
package.json
@ -27,9 +27,9 @@
|
||||
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
|
||||
"build:electron": "yarn workspace jan build",
|
||||
"build:electron:test": "yarn workspace jan build:test",
|
||||
"build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/data-plugin @janhq/model-management-plugin @janhq/monitoring-plugin",
|
||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm install && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
|
||||
"build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-management-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/data-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-management-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
|
||||
"build:pull-plugins": "rimraf ./electron/core/pre-install/*.tgz && cd ./electron/core/pre-install && npm pack @janhq/inference-plugin @janhq/monitoring-plugin",
|
||||
"build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install --ignore-scripts && npm run postinstall:dev && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run postinstall && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall && npm run build:publish\"",
|
||||
"build:plugins-web": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm install && npm run build:deps && npm run postinstall\" \"cd ./plugins/inference-plugin && npm install && npm run postinstall\" \"cd ./plugins/model-plugin && npm install && npm run postinstall\" \"cd ./plugins/monitoring-plugin && npm install && npm run postinstall\" && concurrently --kill-others-on-fail \"cd ./plugins/conversational-plugin && npm run build:publish\" \"cd ./plugins/inference-plugin && npm run build:publish\" \"cd ./plugins/model-plugin && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm run build:publish\"",
|
||||
"build": "yarn build:web && yarn build:electron",
|
||||
"build:test": "yarn build:web && yarn build:electron:test",
|
||||
"build:test-darwin": "yarn build:web && yarn workspace jan build:test-darwin",
|
||||
@ -42,15 +42,18 @@
|
||||
"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",
|
||||
"build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/data-plugin\" && cp \"./plugins/data-plugin/dist/esm/index.js\" \"./web/out/plugins/data-plugin\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-management-plugin\" && cp \"./plugins/model-management-plugin/dist/index.js\" \"./web/out/plugins/model-management-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"",
|
||||
"build:web-plugins": "yarn build:web && yarn build:plugins-web && mkdir -p \"./web/out/plugins/conversational-plugin\" && cp \"./plugins/conversational-plugin/dist/index.js\" \"./web/out/plugins/conversational-plugin\" && mkdir -p \"./web/out/plugins/inference-plugin\" && cp \"./plugins/inference-plugin/dist/index.js\" \"./web/out/plugins/inference-plugin\" && mkdir -p \"./web/out/plugins/model-plugin\" && cp \"./plugins/model-plugin/dist/index.js\" \"./web/out/plugins/model-plugin\" && mkdir -p \"./web/out/plugins/monitoring-plugin\" && cp \"./plugins/monitoring-plugin/dist/index.js\" \"./web/out/plugins/monitoring-plugin\"",
|
||||
"server:prod": "yarn workspace server build && yarn build:web-plugins && cpx \"web/out/**\" \"server/build/renderer/\" && mkdir -p ./server/build/@janhq && cp -r ./plugins/* ./server/build/@janhq",
|
||||
"start:server": "yarn server:prod && node server/build/main.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.1",
|
||||
"cpx": "^1.5.0",
|
||||
"wait-on": "^7.0.1",
|
||||
"rimraf": "^3.0.2"
|
||||
"rimraf": "^3.0.2",
|
||||
"wait-on": "^7.0.1"
|
||||
},
|
||||
"version": "0.0.0"
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@janhq/core": "file:core"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
{
|
||||
"name": "@janhq/azure-openai-plugin",
|
||||
"name": "@janhq/conversational-plugin",
|
||||
"version": "1.0.7",
|
||||
"description": "Inference plugin for Azure OpenAI",
|
||||
"icon": "https://static-assets.jan.ai/openai-icon.jpg",
|
||||
"description": "Conversational Plugin - Stores jan app conversations",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/module.js",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"requiredVersion": "^0.3.1",
|
||||
"license": "MIT",
|
||||
@ -13,7 +11,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
},
|
||||
"exports": {
|
||||
@ -27,16 +25,9 @@
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@janhq/core": "^0.1.6",
|
||||
"azure-openai": "^0.9.4",
|
||||
"kill-port-process": "^3.2.0",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"@janhq/core": "file:../../core",
|
||||
"ts-loader": "^9.5.0"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"tcp-port-used",
|
||||
"kill-port-process"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@ -44,5 +35,6 @@
|
||||
"dist/*",
|
||||
"package.json",
|
||||
"README.md"
|
||||
]
|
||||
],
|
||||
"bundleDependencies": []
|
||||
}
|
||||
211
plugins/conversational-plugin/src/index.ts
Normal file
211
plugins/conversational-plugin/src/index.ts
Normal file
@ -0,0 +1,211 @@
|
||||
import { PluginType, fs } from "@janhq/core";
|
||||
import { ConversationalPlugin } from "@janhq/core/lib/plugins";
|
||||
import { Message, Conversation } from "@janhq/core/lib/types";
|
||||
|
||||
/**
|
||||
* JanConversationalPlugin is a ConversationalPlugin implementation that provides
|
||||
* functionality for managing conversations in a Jan bot.
|
||||
*/
|
||||
export default class JanConversationalPlugin implements ConversationalPlugin {
|
||||
/**
|
||||
* Returns the type of the plugin.
|
||||
*/
|
||||
type(): PluginType {
|
||||
return PluginType.Conversational;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is loaded.
|
||||
*/
|
||||
onLoad() {
|
||||
console.debug("JanConversationalPlugin loaded");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is unloaded.
|
||||
*/
|
||||
onUnload() {
|
||||
console.debug("JanConversationalPlugin unloaded");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise that resolves to an array of Conversation objects.
|
||||
*/
|
||||
getConversations(): Promise<Conversation[]> {
|
||||
return this.getConversationDocs().then((conversationIds) =>
|
||||
Promise.all(
|
||||
conversationIds.map((conversationId) =>
|
||||
this.loadConversationFromMarkdownFile(
|
||||
`conversations/${conversationId}`
|
||||
)
|
||||
)
|
||||
).then((conversations) =>
|
||||
conversations.sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a Conversation object to a Markdown file.
|
||||
* @param conversation The Conversation object to save.
|
||||
*/
|
||||
saveConversation(conversation: Conversation): Promise<void> {
|
||||
return this.writeMarkdownToFile(conversation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a conversation with the specified ID.
|
||||
* @param conversationId The ID of the conversation to delete.
|
||||
*/
|
||||
deleteConversation(conversationId: string): Promise<void> {
|
||||
return fs.deleteFile(`conversations/${conversationId}.md`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise that resolves to an array of conversation IDs.
|
||||
* The conversation IDs are the names of the Markdown files in the "conversations" directory.
|
||||
* @private
|
||||
*/
|
||||
private async getConversationDocs(): Promise<string[]> {
|
||||
return fs.listFiles("conversations").then((files: string[]) => {
|
||||
return Promise.all(
|
||||
files.filter((file) => file.startsWith("jan-"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Markdown string and returns a Conversation object.
|
||||
* @param markdown The Markdown string to parse.
|
||||
* @private
|
||||
*/
|
||||
private parseConversationMarkdown(markdown: string): Conversation {
|
||||
const conversation: Conversation = {
|
||||
_id: "",
|
||||
name: "",
|
||||
messages: [],
|
||||
};
|
||||
var currentMessage: Message | undefined = undefined;
|
||||
for (const line of markdown.split("\n")) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine.startsWith("- _id:")) {
|
||||
conversation._id = trimmedLine.replace("- _id:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- modelId:")) {
|
||||
conversation.modelId = trimmedLine.replace("- modelId:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- name:")) {
|
||||
conversation.name = trimmedLine.replace("- name:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- lastMessage:")) {
|
||||
conversation.message = trimmedLine.replace("- lastMessage:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- summary:")) {
|
||||
conversation.summary = trimmedLine.replace("- summary:", "").trim();
|
||||
} else if (
|
||||
trimmedLine.startsWith("- createdAt:") &&
|
||||
currentMessage === undefined
|
||||
) {
|
||||
conversation.createdAt = trimmedLine.replace("- createdAt:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- updatedAt:")) {
|
||||
conversation.updatedAt = trimmedLine.replace("- updatedAt:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- botId:")) {
|
||||
conversation.botId = trimmedLine.replace("- botId:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- user:")) {
|
||||
if (currentMessage)
|
||||
currentMessage.user = trimmedLine.replace("- user:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- createdAt:")) {
|
||||
if (currentMessage)
|
||||
currentMessage.createdAt = trimmedLine
|
||||
.replace("- createdAt:", "")
|
||||
.trim();
|
||||
|
||||
currentMessage.updatedAt = currentMessage.createdAt;
|
||||
} else if (trimmedLine.startsWith("- message:")) {
|
||||
if (currentMessage)
|
||||
currentMessage.message = trimmedLine.replace("- message:", "").trim();
|
||||
} else if (trimmedLine.startsWith("- Message ")) {
|
||||
const messageMatch = trimmedLine.match(/- Message (message-\d+):/);
|
||||
if (messageMatch) {
|
||||
if (currentMessage) {
|
||||
conversation.messages.push(currentMessage);
|
||||
}
|
||||
currentMessage = { _id: messageMatch[1] };
|
||||
}
|
||||
} else if (
|
||||
currentMessage?.message &&
|
||||
!trimmedLine.startsWith("## Messages")
|
||||
) {
|
||||
currentMessage.message = currentMessage.message + "\n" + line.trim();
|
||||
} else if (trimmedLine.startsWith("## Messages")) {
|
||||
currentMessage = undefined;
|
||||
} else {
|
||||
console.log("missing field processing: ", trimmedLine);
|
||||
}
|
||||
}
|
||||
|
||||
return conversation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Conversation object from a Markdown file.
|
||||
* @param filePath The path to the Markdown file.
|
||||
* @private
|
||||
*/
|
||||
private async loadConversationFromMarkdownFile(
|
||||
filePath: string
|
||||
): Promise<Conversation | undefined> {
|
||||
try {
|
||||
const markdown: string = await fs.readFile(filePath);
|
||||
return this.parseConversationMarkdown(markdown);
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Markdown string from a Conversation object.
|
||||
* @param conversation The Conversation object to generate Markdown from.
|
||||
* @private
|
||||
*/
|
||||
private generateMarkdown(conversation: Conversation): string {
|
||||
// Generate the Markdown content based on the Conversation object
|
||||
const conversationMetadata = `
|
||||
- _id: ${conversation._id}
|
||||
- modelId: ${conversation.modelId}
|
||||
- name: ${conversation.name}
|
||||
- lastMessage: ${conversation.message}
|
||||
- summary: ${conversation.summary}
|
||||
- createdAt: ${conversation.createdAt}
|
||||
- updatedAt: ${conversation.updatedAt}
|
||||
- botId: ${conversation.botId}
|
||||
`;
|
||||
|
||||
const messages = conversation.messages.map(
|
||||
(message) => `
|
||||
- Message ${message._id}:
|
||||
- createdAt: ${message.createdAt}
|
||||
- user: ${message.user}
|
||||
- message: ${message.message?.trim()}
|
||||
`
|
||||
);
|
||||
|
||||
return `## Conversation Metadata
|
||||
${conversationMetadata}
|
||||
## Messages
|
||||
${messages.map((msg) => msg.trim()).join("\n")}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a Conversation object to a Markdown file.
|
||||
* @param conversation The Conversation object to write to a Markdown file.
|
||||
* @private
|
||||
*/
|
||||
private async writeMarkdownToFile(conversation: Conversation) {
|
||||
await fs.mkdir("conversations");
|
||||
// Generate the Markdown content
|
||||
const markdownContent = this.generateMarkdown(conversation);
|
||||
// Write the content to a Markdown file
|
||||
await fs.writeFile(`conversations/${conversation._id}.md`, markdownContent);
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,8 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
@ -1,10 +1,9 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
entry: "./index.ts", // Adjust the entry point to match your project's main file
|
||||
entry: "./src/index.ts", // Adjust the entry point to match your project's main file
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
@ -20,14 +19,13 @@ module.exports = {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
library: { type: "module" }, // Specify ESM output format
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
}),
|
||||
],
|
||||
plugins: [new webpack.DefinePlugin({})],
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
},
|
||||
// Do not minify the output, otherwise it breaks the class registration
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
// Add loaders and other configuration as needed for your project
|
||||
};
|
||||
3
plugins/data-plugin/@types/global.d.ts
vendored
3
plugins/data-plugin/@types/global.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
declare const PLUGIN_NAME: string;
|
||||
declare const MODULE_PATH: string;
|
||||
declare const PLUGIN_CATALOG: string;
|
||||
@ -1,6 +0,0 @@
|
||||
## Jan data handler plugin
|
||||
|
||||
- index.ts: Main entry point for the plugin.
|
||||
- module.ts: Defines the plugin module which would be executed by the main node process.
|
||||
- package.json: Plugin & npm module manifest.
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./../dist/cjs",
|
||||
"module": "commonjs"
|
||||
},
|
||||
"files": ["../module.ts"]
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "./../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./../dist/esm",
|
||||
"module": "esnext"
|
||||
},
|
||||
"include": ["@types/*"],
|
||||
"files": ["../@types/global.d.ts", "../index.ts"]
|
||||
}
|
||||
@ -1,345 +0,0 @@
|
||||
import {
|
||||
invokePluginFunc,
|
||||
store,
|
||||
RegisterExtensionPoint,
|
||||
StoreService,
|
||||
DataService,
|
||||
PluginService,
|
||||
} from "@janhq/core";
|
||||
|
||||
/**
|
||||
* Create a collection on data store
|
||||
*
|
||||
* @param name name of the collection to create
|
||||
* @param schema schema of the collection to create, include fields and their types
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function createCollection({
|
||||
name,
|
||||
schema,
|
||||
}: {
|
||||
name: string;
|
||||
schema?: { [key: string]: any };
|
||||
}): Promise<void> {
|
||||
return invokePluginFunc(MODULE_PATH, "createCollection", name, schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection
|
||||
*
|
||||
* @param name name of the collection to delete
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function deleteCollection(name: string): Promise<void> {
|
||||
return invokePluginFunc(MODULE_PATH, "deleteCollection", name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a value to a collection
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param value value to insert
|
||||
* @returns Promise<any>
|
||||
*
|
||||
*/
|
||||
function insertOne({
|
||||
collectionName,
|
||||
value,
|
||||
}: {
|
||||
collectionName: string;
|
||||
value: any;
|
||||
}): Promise<any> {
|
||||
return invokePluginFunc(MODULE_PATH, "insertOne", collectionName, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update value of a collection's record
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param key key of the record to update
|
||||
* @param value value to update
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function updateOne({
|
||||
collectionName,
|
||||
key,
|
||||
value,
|
||||
}: {
|
||||
collectionName: string;
|
||||
key: string;
|
||||
value: any;
|
||||
}): Promise<void> {
|
||||
return invokePluginFunc(MODULE_PATH, "updateOne", collectionName, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all records that match a selector in a collection in the data store.
|
||||
* @param collectionName - The name of the collection containing the records to update.
|
||||
* @param selector - The selector to use to get the records to update.
|
||||
* @param value - The new value for the records.
|
||||
* @returns {Promise<void>} A promise that resolves when the records are updated.
|
||||
*/
|
||||
function updateMany({
|
||||
collectionName,
|
||||
value,
|
||||
selector,
|
||||
}: {
|
||||
collectionName: string;
|
||||
value: any;
|
||||
selector?: { [key: string]: any };
|
||||
}): Promise<void> {
|
||||
return invokePluginFunc(
|
||||
MODULE_PATH,
|
||||
"updateMany",
|
||||
collectionName,
|
||||
value,
|
||||
selector
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection's record
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param key key of the record to delete
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function deleteOne({
|
||||
collectionName,
|
||||
key,
|
||||
}: {
|
||||
collectionName: string;
|
||||
key: string;
|
||||
}): Promise<void> {
|
||||
return invokePluginFunc(MODULE_PATH, "deleteOne", collectionName, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all records with a matching key from a collection in the data store.
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param selector selector to use to get the records to delete.
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
*/
|
||||
function deleteMany({
|
||||
collectionName,
|
||||
selector,
|
||||
}: {
|
||||
collectionName: string;
|
||||
selector?: { [key: string]: any };
|
||||
}): Promise<void> {
|
||||
return invokePluginFunc(MODULE_PATH, "deleteMany", collectionName, selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a record from a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection containing the record to retrieve.
|
||||
* @param {string} key - The key of the record to retrieve.
|
||||
* @returns {Promise<any>} A promise that resolves when the record is retrieved.
|
||||
*/
|
||||
function findOne({
|
||||
collectionName,
|
||||
key,
|
||||
}: {
|
||||
collectionName: string;
|
||||
key: string;
|
||||
}): Promise<any> {
|
||||
return invokePluginFunc(MODULE_PATH, "findOne", collectionName, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets records in a collection in the data store using a selector.
|
||||
* @param {string} collectionName - The name of the collection containing the record to get the value from.
|
||||
* @param {{ [key: string]: any }} selector - The selector to use to get the value from the record.
|
||||
* @param {[{ [key: string]: any }]} sort - The sort options to use to retrieve records.
|
||||
* @returns {Promise<any>} A promise that resolves with the selected value.
|
||||
*/
|
||||
function findMany({
|
||||
collectionName,
|
||||
selector,
|
||||
sort,
|
||||
}: {
|
||||
collectionName: string;
|
||||
selector: { [key: string]: any };
|
||||
sort?: [{ [key: string]: any }];
|
||||
}): Promise<any> {
|
||||
return invokePluginFunc(
|
||||
MODULE_PATH,
|
||||
"findMany",
|
||||
collectionName,
|
||||
selector,
|
||||
sort
|
||||
);
|
||||
}
|
||||
|
||||
function onStart() {
|
||||
createCollection({ name: "conversations", schema: {} });
|
||||
createCollection({ name: "messages", schema: {} });
|
||||
createCollection({ name: "bots", schema: {} });
|
||||
}
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
// prettier-ignore
|
||||
export function init({ register }: { register: RegisterExtensionPoint }) {
|
||||
register(PluginService.OnStart, PLUGIN_NAME, onStart);
|
||||
register(StoreService.CreateCollection, createCollection.name, createCollection);
|
||||
register(StoreService.DeleteCollection, deleteCollection.name, deleteCollection);
|
||||
|
||||
register(StoreService.InsertOne, insertOne.name, insertOne);
|
||||
register(StoreService.UpdateOne, updateOne.name, updateOne);
|
||||
register(StoreService.UpdateMany, updateMany.name, updateMany);
|
||||
register(StoreService.DeleteOne, deleteOne.name, deleteOne);
|
||||
register(StoreService.DeleteMany, deleteMany.name, deleteMany);
|
||||
register(StoreService.FindOne, findOne.name, findOne);
|
||||
register(StoreService.FindMany, findMany.name, findMany);
|
||||
|
||||
// for conversations management
|
||||
register(DataService.GetConversations, getConversations.name, getConversations);
|
||||
register(DataService.GetConversationById,getConversationById.name,getConversationById);
|
||||
register(DataService.CreateConversation, createConversation.name, createConversation);
|
||||
register(DataService.UpdateConversation, updateConversation.name, updateConversation);
|
||||
register(DataService.DeleteConversation, deleteConversation.name, deleteConversation);
|
||||
|
||||
// for messages management
|
||||
register(DataService.UpdateMessage, updateMessage.name, updateMessage);
|
||||
register(DataService.CreateMessage, createMessage.name, createMessage);
|
||||
register(DataService.GetConversationMessages, getConversationMessages.name, getConversationMessages);
|
||||
|
||||
// for bots management
|
||||
register(DataService.CreateBot, createBot.name, createBot);
|
||||
register(DataService.GetBots, getBots.name, getBots);
|
||||
register(DataService.GetBotById, getBotById.name, getBotById);
|
||||
register(DataService.DeleteBot, deleteBot.name, deleteBot);
|
||||
register(DataService.UpdateBot, updateBot.name, updateBot);
|
||||
|
||||
// for plugin manifest
|
||||
register(DataService.GetPluginManifest, getPluginManifest.name, getPluginManifest)
|
||||
}
|
||||
|
||||
function getConversations(): Promise<any> {
|
||||
return store.findMany("conversations", {}, [{ updatedAt: "desc" }]);
|
||||
}
|
||||
|
||||
function getConversationById(id: string): Promise<any> {
|
||||
return store.findOne("conversations", id);
|
||||
}
|
||||
|
||||
function createConversation(conversation: any): Promise<number | undefined> {
|
||||
return store.insertOne("conversations", conversation);
|
||||
}
|
||||
|
||||
function updateConversation(conversation: any): Promise<void> {
|
||||
return store.updateOne("conversations", conversation._id, conversation);
|
||||
}
|
||||
|
||||
function createMessage(message: any): Promise<number | undefined> {
|
||||
return store.insertOne("messages", message);
|
||||
}
|
||||
|
||||
function updateMessage(message: any): Promise<void> {
|
||||
return store.updateOne("messages", message._id, message);
|
||||
}
|
||||
|
||||
function deleteConversation(id: any) {
|
||||
return store
|
||||
.deleteOne("conversations", id)
|
||||
.then(() => store.deleteMany("messages", { conversationId: id }));
|
||||
}
|
||||
|
||||
function getConversationMessages(conversationId: any) {
|
||||
return store.findMany("messages", { conversationId }, [
|
||||
{ createdAt: "desc" },
|
||||
]);
|
||||
}
|
||||
|
||||
function createBot(bot: any): Promise<void> {
|
||||
console.debug("Creating bot", JSON.stringify(bot, null, 2));
|
||||
return store
|
||||
.insertOne("bots", bot)
|
||||
.then(() => {
|
||||
console.debug("Bot created", JSON.stringify(bot, null, 2));
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error creating bot", err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
function getBots(): Promise<any> {
|
||||
console.debug("Getting bots");
|
||||
return store
|
||||
.findMany("bots", { name: { $gt: null } })
|
||||
.then((bots) => {
|
||||
console.debug("Bots retrieved", JSON.stringify(bots, null, 2));
|
||||
return Promise.resolve(bots);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error getting bots", err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteBot(id: string): Promise<any> {
|
||||
console.debug("Deleting bot", id);
|
||||
return store
|
||||
.deleteOne("bots", id)
|
||||
.then(() => {
|
||||
console.debug("Bot deleted", id);
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error deleting bot", err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
function updateBot(bot: any): Promise<void> {
|
||||
console.debug("Updating bot", JSON.stringify(bot, null, 2));
|
||||
return store
|
||||
.updateOne("bots", bot._id, bot)
|
||||
.then(() => {
|
||||
console.debug("Bot updated");
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error updating bot", err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
function getBotById(botId: string): Promise<any> {
|
||||
console.debug("Getting bot", botId);
|
||||
return store
|
||||
.findOne("bots", botId)
|
||||
.then((bot) => {
|
||||
console.debug("Bot retrieved", JSON.stringify(bot, null, 2));
|
||||
return Promise.resolve(bot);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error getting bot", err);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the plugin manifest by importing the remote model catalog and clearing the cache to get the latest version.
|
||||
* A timestamp is added to the URL to prevent caching.
|
||||
* @returns A Promise that resolves with the plugin manifest.
|
||||
*/
|
||||
function getPluginManifest(): Promise<any> {
|
||||
// Clear cache to get the latest model catalog
|
||||
delete require.cache[
|
||||
require.resolve(/* webpackIgnore: true */ PLUGIN_CATALOG)
|
||||
];
|
||||
// Import the remote model catalog
|
||||
// Add a timestamp to the URL to prevent caching
|
||||
return import(
|
||||
/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`
|
||||
).then((module) => module.default);
|
||||
}
|
||||
@ -1,246 +0,0 @@
|
||||
var PouchDB = require("pouchdb-node");
|
||||
PouchDB.plugin(require("pouchdb-find"));
|
||||
var path = require("path");
|
||||
var { app } = require("electron");
|
||||
var fs = require("fs");
|
||||
|
||||
const dbs: Record<string, any> = {};
|
||||
|
||||
/**
|
||||
* Create a collection on data store
|
||||
*
|
||||
* @param name name of the collection to create
|
||||
* @param schema schema of the collection to create, include fields and their types
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function createCollection(name: string, schema?: { [key: string]: any }): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
const dbPath = path.join(appPath(), "databases");
|
||||
if (!fs.existsSync(dbPath)) fs.mkdirSync(dbPath);
|
||||
const db = new PouchDB(`${path.join(dbPath, name)}`);
|
||||
dbs[name] = db;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection
|
||||
*
|
||||
* @param name name of the collection to delete
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function deleteCollection(name: string): Promise<void> {
|
||||
// Do nothing with Unstructured Database
|
||||
return dbs[name].destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a value to a collection
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param value value to insert
|
||||
* @returns Promise<any>
|
||||
*
|
||||
*/
|
||||
function insertOne(collectionName: string, value: any): Promise<any> {
|
||||
if (!value._id) return dbs[collectionName].post(value).then((doc) => doc.id);
|
||||
return dbs[collectionName].put(value).then((doc) => doc.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update value of a collection's record
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param key key of the record to update
|
||||
* @param value value to update
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function updateOne(collectionName: string, key: string, value: any): Promise<void> {
|
||||
console.debug(`updateOne ${collectionName}: ${key} - ${JSON.stringify(value)}`);
|
||||
return dbs[collectionName].get(key).then((doc) => {
|
||||
return dbs[collectionName].put({
|
||||
_id: key,
|
||||
_rev: doc._rev,
|
||||
...value,
|
||||
},
|
||||
{ force: true });
|
||||
}).then((res: any) => {
|
||||
console.info(`updateOne ${collectionName} result: ${JSON.stringify(res)}`);
|
||||
}).catch((err: any) => {
|
||||
console.error(`updateOne ${collectionName} error: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update value of a collection's records
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param selector selector of records to update
|
||||
* @param value value to update
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function updateMany(collectionName: string, value: any, selector?: { [key: string]: any }): Promise<any> {
|
||||
// Creates keys from selector for indexing
|
||||
const keys = selector ? Object.keys(selector) : [];
|
||||
|
||||
// At a basic level, there are two steps to running a query: createIndex()
|
||||
// (to define which fields to index) and find() (to query the index).
|
||||
return (
|
||||
keys.length > 0
|
||||
? dbs[collectionName].createIndex({
|
||||
// There is selector so we need to create index
|
||||
index: { fields: keys },
|
||||
})
|
||||
: Promise.resolve()
|
||||
) // No selector, so no need to create index
|
||||
.then(() =>
|
||||
dbs[collectionName].find({
|
||||
// Find documents using Mango queries
|
||||
selector,
|
||||
})
|
||||
)
|
||||
.then((data) => {
|
||||
const docs = data.docs.map((doc) => {
|
||||
// Update doc with new value
|
||||
return (doc = {
|
||||
...doc,
|
||||
...value,
|
||||
});
|
||||
});
|
||||
return dbs[collectionName].bulkDocs(docs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection's record
|
||||
*
|
||||
* @param collectionName name of the collection
|
||||
* @param key key of the record to delete
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function deleteOne(collectionName: string, key: string): Promise<void> {
|
||||
return findOne(collectionName, key).then((doc) => dbs[collectionName].remove(doc));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection records by selector
|
||||
*
|
||||
* @param {string} collectionName name of the collection
|
||||
* @param {{ [key: string]: any }} selector selector for retrieving records.
|
||||
* @returns Promise<void>
|
||||
*
|
||||
*/
|
||||
function deleteMany(collectionName: string, selector?: { [key: string]: any }): Promise<void> {
|
||||
// Creates keys from selector for indexing
|
||||
const keys = selector ? Object.keys(selector) : [];
|
||||
|
||||
// At a basic level, there are two steps to running a query: createIndex()
|
||||
// (to define which fields to index) and find() (to query the index).
|
||||
return (
|
||||
keys.length > 0
|
||||
? dbs[collectionName].createIndex({
|
||||
// There is selector so we need to create index
|
||||
index: { fields: keys },
|
||||
})
|
||||
: Promise.resolve()
|
||||
) // No selector, so no need to create index
|
||||
.then(() =>
|
||||
dbs[collectionName].find({
|
||||
// Find documents using Mango queries
|
||||
selector,
|
||||
})
|
||||
)
|
||||
.then((data) => {
|
||||
return Promise.all(
|
||||
// Remove documents
|
||||
data.docs.map((doc) => {
|
||||
return dbs[collectionName].remove(doc);
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a record from a collection in the data store.
|
||||
* @param {string} collectionName - The name of the collection containing the record to retrieve.
|
||||
* @param {string} key - The key of the record to retrieve.
|
||||
* @returns {Promise<any>} A promise that resolves when the record is retrieved.
|
||||
*/
|
||||
function findOne(collectionName: string, key: string): Promise<any> {
|
||||
return dbs[collectionName].get(key).catch(() => undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets records in a collection in the data store using a selector.
|
||||
* @param {string} collectionName - The name of the collection containing records to retrieve.
|
||||
* @param {{ [key: string]: any }} selector - The selector to use to retrieve records.
|
||||
* @param {[{ [key: string]: any }]} sort - The sort options to use to retrieve records.
|
||||
* @returns {Promise<any>} A promise that resolves with the selected records.
|
||||
*/
|
||||
function findMany(
|
||||
collectionName: string,
|
||||
selector?: { [key: string]: any },
|
||||
sort?: [{ [key: string]: any }]
|
||||
): Promise<any> {
|
||||
const keys = selector ? Object.keys(selector) : [];
|
||||
const sortKeys = sort ? sort.flatMap((e) => (e ? Object.keys(e) : undefined)) : [];
|
||||
|
||||
// Note that we are specifying that the field must be greater than or equal to null
|
||||
// which is a workaround for the fact that the Mango query language requires us to have a selector.
|
||||
// In CouchDB collation order, null is the "lowest" value, and so this will return all documents regardless of their field value.
|
||||
sortKeys.forEach((key) => {
|
||||
if (!keys.includes(key)) {
|
||||
selector = { ...selector, [key]: { $gt: null } };
|
||||
}
|
||||
});
|
||||
|
||||
// There is no selector & sort, so we can just use allDocs() to get all the documents.
|
||||
if (sortKeys.concat(keys).length === 0) {
|
||||
return dbs[collectionName]
|
||||
.allDocs({
|
||||
include_docs: true,
|
||||
endkey: "_design",
|
||||
inclusive_end: false,
|
||||
})
|
||||
.then((data) => data.rows.map((row) => row.doc));
|
||||
}
|
||||
// At a basic level, there are two steps to running a query: createIndex()
|
||||
// (to define which fields to index) and find() (to query the index).
|
||||
return dbs[collectionName]
|
||||
.createIndex({
|
||||
// Create index for selector & sort
|
||||
index: { fields: sortKeys.concat(keys) },
|
||||
})
|
||||
.then(() => {
|
||||
// Find documents using Mango queries
|
||||
return dbs[collectionName].find({
|
||||
selector,
|
||||
sort,
|
||||
});
|
||||
})
|
||||
.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,
|
||||
insertOne,
|
||||
findOne,
|
||||
findMany,
|
||||
updateOne,
|
||||
updateMany,
|
||||
deleteOne,
|
||||
deleteMany,
|
||||
};
|
||||
@ -1,53 +0,0 @@
|
||||
{
|
||||
"name": "@janhq/data-plugin",
|
||||
"version": "1.0.19",
|
||||
"description": "The Data Connector provides easy access to a data API using the PouchDB engine. It offers accessible data management capabilities.",
|
||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
||||
"main": "dist/esm/index.js",
|
||||
"module": "dist/cjs/module.js",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "AGPL-3.0",
|
||||
"supportCloudNative": true,
|
||||
"url": "/plugins/data-plugin/index.js",
|
||||
"activationPoints": [
|
||||
"init"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b ./config/tsconfig.esm.json && tsc -b ./config/tsconfig.cjs.json && webpack --config webpack.config.js",
|
||||
"build:deps": "electron-rebuild -f -w leveldown@5.6.0 --arch=arm64 -v 26.2.1 && node-gyp -C ./node_modules/leveldown clean && mkdir -p ./node_modules/leveldown/prebuilds/darwin-arm64 && cp ./node_modules/leveldown/bin/darwin-arm64-116/leveldown.node ./node_modules/leveldown/prebuilds/darwin-arm64/node.napi.node",
|
||||
"postinstall": "rimraf *.tgz --glob && npm run build",
|
||||
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
|
||||
},
|
||||
"exports": {
|
||||
"import": "./dist/esm/index.js",
|
||||
"require": "./dist/cjs/module.js",
|
||||
"default": "./dist/esm/index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"node-pre-gyp": "^0.17.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"files": [
|
||||
"dist/**",
|
||||
"package.json",
|
||||
"node_modules"
|
||||
],
|
||||
"dependencies": {
|
||||
"@janhq/core": "^0.1.7",
|
||||
"electron": "26.2.1",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"node-gyp": "^9.4.1",
|
||||
"pouchdb-find": "^8.0.1",
|
||||
"pouchdb-node": "^8.0.1"
|
||||
},
|
||||
"bundleDependencies": [
|
||||
"pouchdb-node",
|
||||
"pouchdb-find"
|
||||
]
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
entry: "./index.ts", // Adjust the entry point to match your project's main file
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
PLUGIN_CATALOG: JSON.stringify(
|
||||
"https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js"
|
||||
),
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
filename: "esm/index.js", // Adjust the output file name as needed
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
library: { type: "module" }, // Specify ESM output format
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
// Add loaders and other configuration as needed for your project
|
||||
};
|
||||
3
plugins/inference-plugin/@types/global.d.ts
vendored
3
plugins/inference-plugin/@types/global.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
declare const PLUGIN_NAME: string;
|
||||
declare const MODULE_PATH: string;
|
||||
declare const INFERENCE_URL: string;
|
||||
@ -1,206 +0,0 @@
|
||||
import {
|
||||
EventName,
|
||||
InferenceService,
|
||||
NewMessageRequest,
|
||||
PluginService,
|
||||
events,
|
||||
store,
|
||||
invokePluginFunc,
|
||||
} from "@janhq/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
const initModel = async (product) =>
|
||||
invokePluginFunc(MODULE_PATH, "initModel", product);
|
||||
|
||||
const stopModel = () => {
|
||||
invokePluginFunc(MODULE_PATH, "killSubprocess");
|
||||
};
|
||||
|
||||
function requestInference(
|
||||
recentMessages: any[],
|
||||
bot?: any
|
||||
): Observable<string> {
|
||||
return new Observable((subscriber) => {
|
||||
const requestBody = JSON.stringify({
|
||||
messages: recentMessages,
|
||||
stream: true,
|
||||
model: "gpt-3.5-turbo",
|
||||
max_tokens: bot?.maxTokens ?? 2048,
|
||||
frequency_penalty: bot?.frequencyPenalty ?? 0,
|
||||
presence_penalty: bot?.presencePenalty ?? 0,
|
||||
temperature: bot?.customTemperature ?? 0,
|
||||
});
|
||||
console.debug(`Request body: ${requestBody}`);
|
||||
fetch(INFERENCE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
body: requestBody,
|
||||
})
|
||||
.then(async (response) => {
|
||||
const stream = response.body;
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const reader = stream?.getReader();
|
||||
let content = "";
|
||||
|
||||
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: ", ""));
|
||||
content += data.choices[0]?.delta?.content ?? "";
|
||||
if (content.startsWith("assistant: ")) {
|
||||
content = content.replace("assistant: ", "");
|
||||
}
|
||||
subscriber.next(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
subscriber.complete();
|
||||
})
|
||||
.catch((err) => subscriber.error(err));
|
||||
});
|
||||
}
|
||||
|
||||
async function retrieveLastTenMessages(conversationId: string, bot?: any) {
|
||||
// TODO: Common collections should be able to access via core functions instead of store
|
||||
const messageHistory =
|
||||
(await store.findMany("messages", { conversationId }, [
|
||||
{ createdAt: "asc" },
|
||||
])) ?? [];
|
||||
|
||||
let recentMessages = messageHistory
|
||||
.filter(
|
||||
(e) => e.message !== "" && (e.user === "user" || e.user === "assistant")
|
||||
)
|
||||
.slice(-9)
|
||||
.map((message) => ({
|
||||
content: message.message.trim(),
|
||||
role: message.user === "user" ? "user" : "assistant",
|
||||
}));
|
||||
|
||||
if (bot && bot.systemPrompt) {
|
||||
// append bot's system prompt
|
||||
recentMessages = [
|
||||
{
|
||||
content: `[INST] ${bot.systemPrompt}`,
|
||||
role: "system",
|
||||
},
|
||||
...recentMessages,
|
||||
];
|
||||
}
|
||||
|
||||
console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`);
|
||||
|
||||
return recentMessages;
|
||||
}
|
||||
|
||||
async function handleMessageRequest(data: NewMessageRequest) {
|
||||
const conversation = await store.findOne(
|
||||
"conversations",
|
||||
data.conversationId
|
||||
);
|
||||
let bot = undefined;
|
||||
if (conversation.botId != null) {
|
||||
bot = await store.findOne("bots", conversation.botId);
|
||||
}
|
||||
|
||||
const recentMessages = await retrieveLastTenMessages(
|
||||
data.conversationId,
|
||||
bot
|
||||
);
|
||||
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);
|
||||
|
||||
requestInference(recentMessages, bot).subscribe({
|
||||
next: (content) => {
|
||||
message.message = content;
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
},
|
||||
complete: async () => {
|
||||
message.message = message.message.trim();
|
||||
// TODO: Common collections should be able to access via core functions instead of store
|
||||
await store.updateOne("messages", message._id, message);
|
||||
events.emit("OnMessageResponseFinished", message);
|
||||
// events.emit(EventName.OnMessageResponseFinished, message);
|
||||
},
|
||||
error: async (err) => {
|
||||
message.message =
|
||||
message.message.trim() + "\n" + "Error occurred: " + err.message;
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
// TODO: Common collections should be able to access via core functions instead of store
|
||||
await store.updateOne("messages", message._id, message);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function inferenceRequest(data: NewMessageRequest): Promise<any> {
|
||||
const message = {
|
||||
...data,
|
||||
message: "",
|
||||
user: "assistant",
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const recentMessages = await retrieveLastTenMessages(data.conversationId);
|
||||
requestInference([
|
||||
...recentMessages,
|
||||
{ role: "user", content: data.message },
|
||||
]).subscribe({
|
||||
next: (content) => {
|
||||
message.message = content;
|
||||
},
|
||||
complete: async () => {
|
||||
resolve(message);
|
||||
},
|
||||
error: async (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const registerListener = () => {
|
||||
events.on(EventName.OnNewMessageRequest, handleMessageRequest);
|
||||
};
|
||||
|
||||
const killSubprocess = () => {
|
||||
invokePluginFunc(MODULE_PATH, "killSubprocess");
|
||||
};
|
||||
|
||||
const onStart = async () => {
|
||||
// Try killing any existing subprocesses related to Nitro
|
||||
killSubprocess();
|
||||
|
||||
registerListener();
|
||||
};
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register(PluginService.OnStart, PLUGIN_NAME, onStart);
|
||||
register(InferenceService.InitModel, initModel.name, initModel);
|
||||
register(InferenceService.StopModel, stopModel.name, stopModel);
|
||||
register(
|
||||
InferenceService.InferenceRequest,
|
||||
inferenceRequest.name,
|
||||
inferenceRequest
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
"name": "@janhq/inference-plugin",
|
||||
"version": "1.0.21",
|
||||
"description": "Inference Plugin, powered by @janhq/nitro, bring a high-performance Llama model inference in pure C++.",
|
||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/command-line.svg",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/module.js",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
@ -35,7 +34,7 @@
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@janhq/core": "^0.1.6",
|
||||
"@janhq/core": "file:../../core",
|
||||
"download-cli": "^1.1.1",
|
||||
"kill-port": "^2.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
2
plugins/inference-plugin/src/@types/global.d.ts
vendored
Normal file
2
plugins/inference-plugin/src/@types/global.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare const MODULE: string;
|
||||
declare const INFERENCE_URL: string;
|
||||
52
plugins/inference-plugin/src/helpers/sse.ts
Normal file
52
plugins/inference-plugin/src/helpers/sse.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Observable } from "rxjs";
|
||||
/**
|
||||
* Sends a request to the inference server to generate a response based on the recent messages.
|
||||
* @param recentMessages - An array of recent messages to use as context for the inference.
|
||||
* @returns An Observable that emits the generated response as a string.
|
||||
*/
|
||||
export function requestInference(recentMessages: any[]): Observable<string> {
|
||||
return new Observable((subscriber) => {
|
||||
const requestBody = JSON.stringify({
|
||||
messages: recentMessages,
|
||||
stream: true,
|
||||
model: "gpt-3.5-turbo",
|
||||
max_tokens: 2048,
|
||||
});
|
||||
fetch(INFERENCE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "text/event-stream",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
body: requestBody,
|
||||
})
|
||||
.then(async (response) => {
|
||||
const stream = response.body;
|
||||
const decoder = new TextDecoder("utf-8");
|
||||
const reader = stream?.getReader();
|
||||
let content = "";
|
||||
|
||||
while (true && reader) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
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: ", ""));
|
||||
content += data.choices[0]?.delta?.content ?? "";
|
||||
if (content.startsWith("assistant: ")) {
|
||||
content = content.replace("assistant: ", "");
|
||||
}
|
||||
subscriber.next(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
subscriber.complete();
|
||||
})
|
||||
.catch((err) => subscriber.error(err));
|
||||
});
|
||||
}
|
||||
140
plugins/inference-plugin/src/index.ts
Normal file
140
plugins/inference-plugin/src/index.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @file This file exports a class that implements the InferencePlugin interface from the @janhq/core package.
|
||||
* The class provides methods for initializing and stopping a model, and for making inference requests.
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
* @version 1.0.0
|
||||
* @module inference-plugin/src/index
|
||||
*/
|
||||
|
||||
import {
|
||||
EventName,
|
||||
MessageHistory,
|
||||
NewMessageRequest,
|
||||
PluginType,
|
||||
events,
|
||||
executeOnMain,
|
||||
} from "@janhq/core";
|
||||
import { InferencePlugin } from "@janhq/core/lib/plugins";
|
||||
import { requestInference } from "./helpers/sse";
|
||||
|
||||
/**
|
||||
* A class that implements the InferencePlugin interface from the @janhq/core package.
|
||||
* The class provides methods for initializing and stopping a model, and for making inference requests.
|
||||
* It also subscribes to events emitted by the @janhq/core package and handles new message requests.
|
||||
*/
|
||||
export default class JanInferencePlugin implements InferencePlugin {
|
||||
/**
|
||||
* Returns the type of the plugin.
|
||||
* @returns {PluginType} The type of the plugin.
|
||||
*/
|
||||
type(): PluginType {
|
||||
return PluginType.Inference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to events emitted by the @janhq/core package.
|
||||
*/
|
||||
onLoad(): void {
|
||||
events.on(EventName.OnNewMessageRequest, this.handleMessageRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the model inference.
|
||||
*/
|
||||
onUnload(): void {
|
||||
this.stopModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the model with the specified file name.
|
||||
* @param {string} modelFileName - The name of the model file.
|
||||
* @returns {Promise<void>} A promise that resolves when the model is initialized.
|
||||
*/
|
||||
initModel(modelFileName: string): Promise<void> {
|
||||
return executeOnMain(MODULE, "initModel", modelFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the model.
|
||||
* @returns {Promise<void>} A promise that resolves when the model is stopped.
|
||||
*/
|
||||
stopModel(): Promise<void> {
|
||||
return executeOnMain(MODULE, "killSubprocess");
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a single response inference request.
|
||||
* @param {NewMessageRequest} data - The data for the inference request.
|
||||
* @returns {Promise<any>} A promise that resolves with the inference response.
|
||||
*/
|
||||
async inferenceRequest(data: NewMessageRequest): Promise<any> {
|
||||
const message = {
|
||||
...data,
|
||||
message: "",
|
||||
user: "assistant",
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const prompts: [MessageHistory] = [
|
||||
{
|
||||
role: "user",
|
||||
content: data.message,
|
||||
},
|
||||
];
|
||||
const recentMessages = await (data.history ?? prompts);
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
requestInference([
|
||||
...recentMessages,
|
||||
{ role: "user", content: data.message },
|
||||
]).subscribe({
|
||||
next: (content) => {
|
||||
message.message = content;
|
||||
},
|
||||
complete: async () => {
|
||||
resolve(message);
|
||||
},
|
||||
error: async (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a new message request by making an inference request and emitting events.
|
||||
* @param {NewMessageRequest} data - The data for the new message request.
|
||||
*/
|
||||
private async handleMessageRequest(data: NewMessageRequest) {
|
||||
const prompts: [MessageHistory] = [
|
||||
{
|
||||
role: "user",
|
||||
content: data.message,
|
||||
},
|
||||
];
|
||||
const recentMessages = await (data.history ?? prompts);
|
||||
const message = {
|
||||
...data,
|
||||
message: "",
|
||||
user: "assistant",
|
||||
createdAt: new Date().toISOString(),
|
||||
_id: `message-${Date.now()}`,
|
||||
};
|
||||
events.emit(EventName.OnNewMessageResponse, message);
|
||||
|
||||
requestInference(recentMessages).subscribe({
|
||||
next: (content) => {
|
||||
message.message = content;
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
},
|
||||
complete: async () => {
|
||||
message.message = message.message.trim();
|
||||
events.emit(EventName.OnMessageResponseFinished, message);
|
||||
},
|
||||
error: async (err) => {
|
||||
message.message =
|
||||
message.message.trim() + "\n" + "Error occurred: " + err.message;
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,8 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
entry: "./index.ts", // Adjust the entry point to match your project's main file
|
||||
entry: "./src/index.ts", // Adjust the entry point to match your project's main file
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
@ -17,8 +17,7 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
INFERENCE_URL: JSON.stringify(process.env.INFERENCE_URL || "http://127.0.0.1:3928/inferences/llamacpp/chat_completion"),
|
||||
}),
|
||||
],
|
||||
|
||||
@ -1,178 +0,0 @@
|
||||
import {
|
||||
ModelManagementService,
|
||||
PluginService,
|
||||
RegisterExtensionPoint,
|
||||
downloadFile,
|
||||
deleteFile,
|
||||
store,
|
||||
EventName,
|
||||
events
|
||||
} from "@janhq/core";
|
||||
import { parseToModel } from "./helper";
|
||||
|
||||
const downloadModel = (product) => {
|
||||
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(() => {
|
||||
fetchDownloadProgress(fileName, intervalId);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDownloadProgress(fileName: string, intervalId: NodeJS.Timeout): Promise<string> {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
const json = await response.json();
|
||||
if (isEmptyObject(json)) {
|
||||
if (!fileName && intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
return Promise.resolve("");
|
||||
}
|
||||
if (json.success === true) {
|
||||
events.emit(EventName.OnDownloadSuccess, json);
|
||||
clearInterval(intervalId);
|
||||
return Promise.resolve("");
|
||||
} else {
|
||||
events.emit(EventName.OnDownloadUpdate, json);
|
||||
return Promise.resolve(json.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
function isEmptyObject(ojb: any): boolean {
|
||||
return Object.keys(ojb).length === 0;
|
||||
}
|
||||
|
||||
const deleteModel = (path) => deleteFile(path);
|
||||
|
||||
/**
|
||||
* Retrieves a list of configured models from the model catalog URL.
|
||||
* @returns A Promise that resolves to an array of configured models.
|
||||
*/
|
||||
async function getConfiguredModels(): Promise<any> {
|
||||
// Add a timestamp to the URL to prevent caching
|
||||
return import(
|
||||
/* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}`
|
||||
).then((module) =>
|
||||
module.default.map((e) => {
|
||||
return parseToModel(e);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a model in the database when user start downloading it
|
||||
*
|
||||
* @param model Product
|
||||
*/
|
||||
function storeModel(model: any) {
|
||||
return store.findOne("models", model._id).then((doc) => {
|
||||
if (doc) {
|
||||
return store.updateOne("models", model._id, model);
|
||||
} else {
|
||||
return store.insertOne("models", model);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the finished download time of a model
|
||||
*
|
||||
* @param model Product
|
||||
*/
|
||||
function updateFinishedDownloadAt(_id: string): Promise<any> {
|
||||
return store.updateMany(
|
||||
"models",
|
||||
{ _id },
|
||||
{ time: Date.now(), finishDownloadAt: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all finished models from the database.
|
||||
*
|
||||
* @returns A promise that resolves with an array of finished models.
|
||||
*/
|
||||
function getFinishedDownloadModels(): Promise<any> {
|
||||
return store.findMany("models", { finishDownloadAt: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a model from the database.
|
||||
*
|
||||
* @param modelId The ID of the model to delete.
|
||||
* @returns A promise that resolves when the model is deleted.
|
||||
*/
|
||||
function deleteDownloadModel(modelId: string): Promise<any> {
|
||||
return store.deleteOne("models", modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a model from the database by ID.
|
||||
*
|
||||
* @param modelId The ID of the model to retrieve.
|
||||
* @returns A promise that resolves with the model.
|
||||
*/
|
||||
function getModelById(modelId: string): Promise<any> {
|
||||
return store.findOne("models", modelId);
|
||||
}
|
||||
|
||||
function onStart() {
|
||||
store.createCollection("models", {});
|
||||
if (!(window as any)?.electronAPI) {
|
||||
fetchDownloadProgress(null, null).then((fileName: string) => fileName && checkDownloadProgress(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }: { register: RegisterExtensionPoint }) {
|
||||
register(PluginService.OnStart, PLUGIN_NAME, onStart);
|
||||
|
||||
register(
|
||||
ModelManagementService.DownloadModel,
|
||||
downloadModel.name,
|
||||
downloadModel
|
||||
);
|
||||
register(ModelManagementService.DeleteModel, deleteModel.name, deleteModel);
|
||||
register(
|
||||
ModelManagementService.GetConfiguredModels,
|
||||
getConfiguredModels.name,
|
||||
getConfiguredModels
|
||||
);
|
||||
|
||||
register(ModelManagementService.StoreModel, storeModel.name, storeModel);
|
||||
register(
|
||||
ModelManagementService.UpdateFinishedDownloadAt,
|
||||
updateFinishedDownloadAt.name,
|
||||
updateFinishedDownloadAt
|
||||
);
|
||||
|
||||
register(
|
||||
ModelManagementService.DeleteDownloadModel,
|
||||
deleteDownloadModel.name,
|
||||
deleteDownloadModel
|
||||
);
|
||||
register(
|
||||
ModelManagementService.GetModelById,
|
||||
getModelById.name,
|
||||
getModelById
|
||||
);
|
||||
register(
|
||||
ModelManagementService.GetFinishedDownloadModels,
|
||||
getFinishedDownloadModels.name,
|
||||
getFinishedDownloadModels
|
||||
);
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
module.exports = {};
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@janhq/model-management-plugin",
|
||||
"name": "@janhq/model-plugin",
|
||||
"version": "1.0.13",
|
||||
"description": "Model Management Plugin provides model exploration and seamless downloads",
|
||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/queue-list.svg",
|
||||
@ -8,7 +8,7 @@
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "AGPL-3.0",
|
||||
"supportCloudNative": true,
|
||||
"url": "/plugins/model-management-plugin/index.js",
|
||||
"url": "/plugins/model-plugin/index.js",
|
||||
"activationPoints": [
|
||||
"init"
|
||||
],
|
||||
@ -29,7 +29,7 @@
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"@janhq/core": "^0.1.6",
|
||||
"@janhq/core": "file:../../core",
|
||||
"ts-loader": "^9.5.0"
|
||||
}
|
||||
}
|
||||
48
plugins/model-plugin/src/helpers/cloudNative.ts
Normal file
48
plugins/model-plugin/src/helpers/cloudNative.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { EventName, events } from "@janhq/core";
|
||||
|
||||
export async function pollDownloadProgress(fileName: string) {
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
typeof (window as any).electronAPI === "undefined"
|
||||
) {
|
||||
const intervalId = setInterval(() => {
|
||||
notifyProgress(fileName, intervalId);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
export async function notifyProgress(
|
||||
fileName: string,
|
||||
intervalId: NodeJS.Timeout
|
||||
): Promise<string> {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
const json = await response.json();
|
||||
if (isEmptyObject(json)) {
|
||||
if (!fileName && intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
return Promise.resolve("");
|
||||
}
|
||||
if (json.success === true) {
|
||||
events.emit(EventName.OnDownloadSuccess, json);
|
||||
clearInterval(intervalId);
|
||||
return Promise.resolve("");
|
||||
} else {
|
||||
events.emit(EventName.OnDownloadUpdate, json);
|
||||
return Promise.resolve(json.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
function isEmptyObject(ojb: any): boolean {
|
||||
return Object.keys(ojb).length === 0;
|
||||
}
|
||||
105
plugins/model-plugin/src/index.ts
Normal file
105
plugins/model-plugin/src/index.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { PluginType, fs, downloadFile } from "@janhq/core";
|
||||
import { ModelPlugin } from "@janhq/core/lib/plugins";
|
||||
import { Model, ModelCatalog } from "@janhq/core/lib/types";
|
||||
import { pollDownloadProgress } from "./helpers/cloudNative";
|
||||
import { parseToModel } from "./helpers/modelParser";
|
||||
|
||||
/**
|
||||
* A plugin for managing machine learning models.
|
||||
*/
|
||||
export default class JanModelPlugin implements ModelPlugin {
|
||||
/**
|
||||
* Implements type from JanPlugin.
|
||||
* @override
|
||||
* @returns The type of the plugin.
|
||||
*/
|
||||
type(): PluginType {
|
||||
return PluginType.Model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is loaded.
|
||||
* @override
|
||||
*/
|
||||
onLoad(): void {
|
||||
/** Cloud Native
|
||||
* TODO: Fetch all downloading progresses?
|
||||
**/
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is unloaded.
|
||||
* @override
|
||||
*/
|
||||
onUnload(): void {}
|
||||
|
||||
/**
|
||||
* Downloads a machine learning model.
|
||||
* @param model - The model to download.
|
||||
* @returns A Promise that resolves when the model is downloaded.
|
||||
*/
|
||||
async downloadModel(model: Model): Promise<void> {
|
||||
await fs.mkdir("models");
|
||||
downloadFile(model.downloadLink, `models/${model._id}`);
|
||||
/** Cloud Native
|
||||
* MARK: Poll Downloading Progress
|
||||
**/
|
||||
pollDownloadProgress(model._id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a machine learning model.
|
||||
* @param filePath - The path to the model file to delete.
|
||||
* @returns A Promise that resolves when the model is deleted.
|
||||
*/
|
||||
deleteModel(filePath: string): Promise<void> {
|
||||
return fs
|
||||
.deleteFile(`models/${filePath}`)
|
||||
.then(() => fs.deleteFile(`models/m-${filePath}.json`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a machine learning model.
|
||||
* @param model - The model to save.
|
||||
* @returns A Promise that resolves when the model is saved.
|
||||
*/
|
||||
async saveModel(model: Model): Promise<void> {
|
||||
await fs.writeFile(`models/m-${model._id}.json`, JSON.stringify(model));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all downloaded models.
|
||||
* @returns A Promise that resolves with an array of all models.
|
||||
*/
|
||||
getDownloadedModels(): Promise<Model[]> {
|
||||
return fs
|
||||
.listFiles("models")
|
||||
.then((files: string[]) => {
|
||||
return Promise.all(
|
||||
files
|
||||
.filter((file) => /^m-.*\.json$/.test(file))
|
||||
.map(async (file) => {
|
||||
const model: Model = JSON.parse(
|
||||
await fs.readFile(`models/${file}`)
|
||||
);
|
||||
return model;
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((e) => fs.mkdir("models").then(() => []));
|
||||
}
|
||||
/**
|
||||
* Gets all available models.
|
||||
* @returns A Promise that resolves with an array of all models.
|
||||
*/
|
||||
getConfiguredModels(): Promise<ModelCatalog[]> {
|
||||
// Add a timestamp to the URL to prevent caching
|
||||
return import(
|
||||
/* webpackIgnore: true */ MODEL_CATALOG_URL + `?t=${Date.now()}`
|
||||
).then((module) =>
|
||||
module.default.map((e) => {
|
||||
return parseToModel(e);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,8 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
@ -4,7 +4,7 @@ const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
entry: "./index.ts", // Adjust the entry point to match your project's main file
|
||||
entry: "./src/index.ts", // Adjust the entry point to match your project's main file
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
2
plugins/monitoring-plugin/@types/global.d.ts
vendored
2
plugins/monitoring-plugin/@types/global.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
declare const PLUGIN_NAME: string;
|
||||
declare const MODULE_PATH: string;
|
||||
@ -1,12 +0,0 @@
|
||||
import { core, SystemMonitoringService } from "@janhq/core";
|
||||
|
||||
// Provide an async method to manipulate the price provided by the extension point
|
||||
const getResourcesInfo = () => core.invokePluginFunc(MODULE_PATH, "getResourcesInfo");
|
||||
|
||||
const getCurrentLoad = () => core.invokePluginFunc(MODULE_PATH, "getCurrentLoad");
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register(SystemMonitoringService.GetResourcesInfo, getResourcesInfo.name, getResourcesInfo);
|
||||
register(SystemMonitoringService.GetCurrentLoad, getCurrentLoad.name, getCurrentLoad);
|
||||
}
|
||||
@ -23,16 +23,16 @@
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@janhq/core": "^0.1.6",
|
||||
"@janhq/core": "file:../../core",
|
||||
"systeminformation": "^5.21.8",
|
||||
"ts-loader": "^9.5.0"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"systeminformation"
|
||||
],
|
||||
"files": [
|
||||
"dist/*",
|
||||
"package.json",
|
||||
"README.md"
|
||||
],
|
||||
"bundleDependencies": [
|
||||
"systeminformation"
|
||||
]
|
||||
}
|
||||
|
||||
1
plugins/monitoring-plugin/src/@types/global.d.ts
vendored
Normal file
1
plugins/monitoring-plugin/src/@types/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare const MODULE: string;
|
||||
43
plugins/monitoring-plugin/src/index.ts
Normal file
43
plugins/monitoring-plugin/src/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { PluginType } from "@janhq/core";
|
||||
import { MonitoringPlugin } from "@janhq/core/lib/plugins";
|
||||
import { executeOnMain } from "@janhq/core";
|
||||
|
||||
/**
|
||||
* JanMonitoringPlugin is a plugin that provides system monitoring functionality.
|
||||
* It implements the MonitoringPlugin interface from the @janhq/core package.
|
||||
*/
|
||||
export default class JanMonitoringPlugin implements MonitoringPlugin {
|
||||
/**
|
||||
* Returns the type of the plugin.
|
||||
* @returns The PluginType.SystemMonitoring value.
|
||||
*/
|
||||
type(): PluginType {
|
||||
return PluginType.SystemMonitoring;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the plugin is loaded.
|
||||
*/
|
||||
onLoad(): void {}
|
||||
|
||||
/**
|
||||
* Called when the plugin is unloaded.
|
||||
*/
|
||||
onUnload(): void {}
|
||||
|
||||
/**
|
||||
* Returns information about the system resources.
|
||||
* @returns A Promise that resolves to an object containing information about the system resources.
|
||||
*/
|
||||
getResourcesInfo(): Promise<any> {
|
||||
return executeOnMain(MODULE, "getResourcesInfo");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns information about the current system load.
|
||||
* @returns A Promise that resolves to an object containing information about the current system load.
|
||||
*/
|
||||
getCurrentLoad(): Promise<any> {
|
||||
return executeOnMain(MODULE, "getCurrentLoad");
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,8 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
entry: "./index.ts", // Adjust the entry point to match your project's main file
|
||||
entry: "./src/index.ts", // Adjust the entry point to match your project's main file
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
@ -22,8 +22,7 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
|
||||
2
plugins/openai-plugin/@types/global.d.ts
vendored
2
plugins/openai-plugin/@types/global.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
declare const PLUGIN_NAME: string;
|
||||
declare const MODULE_PATH: string;
|
||||
@ -1,114 +0,0 @@
|
||||
import {
|
||||
PluginService,
|
||||
EventName,
|
||||
NewMessageRequest,
|
||||
events,
|
||||
store,
|
||||
preferences,
|
||||
RegisterExtensionPoint,
|
||||
} from "@janhq/core";
|
||||
import { Configuration, OpenAIApi } from "azure-openai";
|
||||
|
||||
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(PLUGIN_NAME, "apiKey")) ?? "";
|
||||
const endpoint: string = (await preferences.get(PLUGIN_NAME, "endpoint")) ?? "";
|
||||
const deploymentName: string = (await preferences.get(PLUGIN_NAME, "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);
|
||||
};
|
||||
|
||||
// Preference update - reconfigure OpenAI
|
||||
const onPreferencesUpdate = () => {
|
||||
setup();
|
||||
};
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }: { register: RegisterExtensionPoint }) {
|
||||
register(PluginService.OnStart, PLUGIN_NAME, onStart);
|
||||
register(PluginService.OnPreferencesUpdate, PLUGIN_NAME, onPreferencesUpdate);
|
||||
|
||||
preferences.registerPreferences<string>(register, PLUGIN_NAME, "apiKey", "API Key", "Azure Project API Key", "");
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"endpoint",
|
||||
"API Endpoint",
|
||||
"Azure Deployment Endpoint API",
|
||||
""
|
||||
);
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"deploymentName",
|
||||
"Deployment Name",
|
||||
"The deployment name you chose when you deployed the model",
|
||||
""
|
||||
);
|
||||
}
|
||||
2
plugins/retrieval-plugin/@types/global.d.ts
vendored
2
plugins/retrieval-plugin/@types/global.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
declare const PLUGIN_NAME: string;
|
||||
declare const MODULE_PATH: string;
|
||||
@ -1,75 +0,0 @@
|
||||
# Create a Jan Plugin using Typescript
|
||||
|
||||
Use this template to bootstrap the creation of a TypeScript Jan plugin. 🚀
|
||||
|
||||
## Create Your Own Plugin
|
||||
|
||||
To create your own plugin, you can use this repository as a template! Just follow the below instructions:
|
||||
|
||||
1. Click the Use this template button at the top of the repository
|
||||
2. Select Create a new repository
|
||||
3. Select an owner and name for your new repository
|
||||
4. Click Create repository
|
||||
5. Clone your new repository
|
||||
|
||||
## Initial Setup
|
||||
|
||||
After you've cloned the repository to your local machine or codespace, you'll need to perform some initial setup steps before you can develop your plugin.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> You'll need to have a reasonably modern version of
|
||||
> [Node.js](https://nodejs.org) handy. If you are using a version manager like
|
||||
> [`nodenv`](https://github.com/nodenv/nodenv) or
|
||||
> [`nvm`](https://github.com/nvm-sh/nvm), you can run `nodenv install` in the
|
||||
> root of your repository to install the version specified in
|
||||
> [`package.json`](./package.json). Otherwise, 20.x or later should work!
|
||||
|
||||
1. :hammer_and_wrench: Install the dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
1. :building_construction: Package the TypeScript for distribution
|
||||
|
||||
```bash
|
||||
npm run bundle
|
||||
```
|
||||
|
||||
1. :white_check_mark: Check your artifact
|
||||
|
||||
There will be a tgz file in your plugin directory now
|
||||
|
||||
## Update the Plugin Metadata
|
||||
|
||||
The [`package.json`](package.json) file defines metadata about your plugin, such as
|
||||
plugin name, main entry, description and version.
|
||||
|
||||
When you copy this repository, update `package.json` with the name, description for your plugin.
|
||||
|
||||
## Update the Plugin Code
|
||||
|
||||
The [`src/`](./src/) directory is the heart of your plugin! This contains the
|
||||
source code that will be run when your plugin extension functions are invoked. You can replace the
|
||||
contents of this directory with your own code.
|
||||
|
||||
There are a few things to keep in mind when writing your plugin code:
|
||||
|
||||
- Most Jan Plugin Extension functions are processed asynchronously.
|
||||
In `index.ts`, you will see that the extension function will return a `Promise<any>`.
|
||||
|
||||
```typescript
|
||||
import { core } from "@janhq/core";
|
||||
|
||||
function onStart(): Promise<any> {
|
||||
return core.invokePluginFunc(MODULE_PATH, "run", 0);
|
||||
}
|
||||
```
|
||||
|
||||
For more information about the Jan Plugin Core module, see the
|
||||
[documentation](https://github.com/janhq/jan/blob/main/core/README.md).
|
||||
|
||||
So, what are you waiting for? Go ahead and start customizing your plugin!
|
||||
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "retrieval-plugin",
|
||||
"version": "1.0.3",
|
||||
"description": "Retrieval plugin for Jan app (experimental)",
|
||||
"icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/circle-stack.svg",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/module.js",
|
||||
"requiredVersion": "^0.3.1",
|
||||
"author": "Jan <service@jan.ai>",
|
||||
"license": "MIT",
|
||||
"activationPoints": [
|
||||
"init"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -b . && webpack --config webpack.config.js",
|
||||
"bundle": "npm pack"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@janhq/core": "^0.1.1",
|
||||
"faiss-node": "^0.5.1",
|
||||
"install": "^0.13.0",
|
||||
"langchain": "^0.0.169",
|
||||
"npm": "^10.2.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"ts-loader": "^9.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
"package.json",
|
||||
"README.md"
|
||||
],
|
||||
"bundleDependencies": [
|
||||
"pdf-parse",
|
||||
"langchain",
|
||||
"faiss-node"
|
||||
]
|
||||
}
|
||||
@ -1,172 +0,0 @@
|
||||
/**
|
||||
* The entrypoint for the plugin.
|
||||
*/
|
||||
|
||||
import {
|
||||
EventName,
|
||||
NewMessageRequest,
|
||||
PluginService,
|
||||
RegisterExtensionPoint,
|
||||
invokePluginFunc,
|
||||
events,
|
||||
preferences,
|
||||
store,
|
||||
} from "@janhq/core";
|
||||
|
||||
/**
|
||||
* Register event listener.
|
||||
*/
|
||||
const registerListener = () => {
|
||||
events.on(EventName.OnNewMessageRequest, inferenceRequest);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invokes the `ingest` function from the `module.js` file using the `invokePluginFunc` method.
|
||||
* "ingest" is the name of the function to invoke.
|
||||
* @returns {Promise<any>} A promise that resolves with the result of the `run` function.
|
||||
*/
|
||||
function onStart(): Promise<void> {
|
||||
registerListener();
|
||||
ingest();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the document ingestion directory path from the `preferences` module and invokes the `ingest` function
|
||||
* from the specified module with the directory path and additional options.
|
||||
* The additional options are retrieved from the `preferences` module using the `PLUGIN_NAME` constant.
|
||||
*/
|
||||
async function ingest() {
|
||||
const path = await preferences.get(PLUGIN_NAME, "ingestDocumentDirectoryPath");
|
||||
|
||||
// TODO: Hiro - Add support for custom embeddings
|
||||
const customizedEmbedding = undefined;
|
||||
|
||||
if (path && path.length > 0) {
|
||||
const openAPIKey = await preferences.get(PLUGIN_NAME, "openAIApiKey");
|
||||
const azureOpenAIBasePath = await preferences.get(PLUGIN_NAME, "azureOpenAIBasePath");
|
||||
const azureOpenAIApiInstanceName = await preferences.get(PLUGIN_NAME, "azureOpenAIApiInstanceName");
|
||||
invokePluginFunc(MODULE_PATH, "ingest", path, customizedEmbedding, {
|
||||
openAIApiKey: openAPIKey?.length > 0 ? openAPIKey : undefined,
|
||||
azureOpenAIApiKey: await preferences.get(PLUGIN_NAME, "azureOpenAIApiKey"),
|
||||
azureOpenAIApiVersion: await preferences.get(PLUGIN_NAME, "azureOpenAIApiVersion"),
|
||||
azureOpenAIApiInstanceName: azureOpenAIApiInstanceName?.length > 0 ? azureOpenAIApiInstanceName : undefined,
|
||||
azureOpenAIApiDeploymentName: await preferences.get(PLUGIN_NAME, "azureOpenAIApiDeploymentNameRag"),
|
||||
azureOpenAIBasePath: azureOpenAIBasePath?.length > 0 ? azureOpenAIBasePath : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the document ingestion directory path from the `preferences` module and invokes the `ingest` function
|
||||
* from the specified module with the directory path and additional options.
|
||||
* The additional options are retrieved from the `preferences` module using the `PLUGIN_NAME` constant.
|
||||
*/
|
||||
async function inferenceRequest(data: NewMessageRequest): Promise<any> {
|
||||
// TODO: Hiro - Add support for custom embeddings
|
||||
const customLLM = undefined;
|
||||
const message = {
|
||||
...data,
|
||||
message: "",
|
||||
user: "RAG",
|
||||
createdAt: new Date().toISOString(),
|
||||
_id: undefined,
|
||||
};
|
||||
const id = await store.insertOne("messages", message);
|
||||
message._id = id;
|
||||
events.emit(EventName.OnNewMessageResponse, message);
|
||||
|
||||
const openAPIKey = await preferences.get(PLUGIN_NAME, "openAIApiKey");
|
||||
const azureOpenAIBasePath = await preferences.get(PLUGIN_NAME, "azureOpenAIBasePath");
|
||||
const azureOpenAIApiInstanceName = await preferences.get(PLUGIN_NAME, "azureOpenAIApiInstanceName");
|
||||
invokePluginFunc(MODULE_PATH, "chatWithDocs", data.message, customLLM, {
|
||||
openAIApiKey: openAPIKey?.length > 0 ? openAPIKey : undefined,
|
||||
azureOpenAIApiKey: await preferences.get(PLUGIN_NAME, "azureOpenAIApiKey"),
|
||||
azureOpenAIApiVersion: await preferences.get(PLUGIN_NAME, "azureOpenAIApiVersion"),
|
||||
azureOpenAIApiInstanceName: azureOpenAIApiInstanceName?.length > 0 ? azureOpenAIApiInstanceName : undefined,
|
||||
azureOpenAIApiDeploymentName: await preferences.get(PLUGIN_NAME, "azureOpenAIApiDeploymentNameChat"),
|
||||
azureOpenAIBasePath: azureOpenAIBasePath?.length > 0 ? azureOpenAIBasePath : undefined,
|
||||
modelName: "gpt-3.5-turbo-16k",
|
||||
temperature: 0.2,
|
||||
}).then(async (text) => {
|
||||
console.log("RAG Response:", text);
|
||||
message.message = text;
|
||||
|
||||
events.emit(EventName.OnMessageResponseUpdate, message);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Initializes the plugin by registering the extension functions with the given register function.
|
||||
* @param {Function} options.register - The function to use for registering the extension functions
|
||||
*/
|
||||
export function init({ register }: { register: RegisterExtensionPoint }) {
|
||||
register(PluginService.OnStart, PLUGIN_NAME, onStart);
|
||||
register(PluginService.OnPreferencesUpdate, PLUGIN_NAME, ingest);
|
||||
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"ingestDocumentDirectoryPath",
|
||||
"Document Ingest Directory Path",
|
||||
"The URL of the directory containing the documents to ingest",
|
||||
undefined
|
||||
);
|
||||
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"openAIApiKey",
|
||||
"Open API Key",
|
||||
"OpenAI API Key",
|
||||
undefined
|
||||
);
|
||||
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"azureOpenAIApiKey",
|
||||
"Azure API Key",
|
||||
"Azure Project API Key",
|
||||
undefined
|
||||
);
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"azureOpenAIApiVersion",
|
||||
"Azure API Version",
|
||||
"Azure Project API Version",
|
||||
undefined
|
||||
);
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"azureOpenAIApiInstanceName",
|
||||
"Azure Instance Name",
|
||||
"Azure Project Instance Name",
|
||||
undefined
|
||||
);
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"azureOpenAIApiDeploymentNameChat",
|
||||
"Azure Chat Model Deployment Name",
|
||||
"Azure Project Chat Model Deployment Name (e.g. gpt-3.5-turbo-16k)",
|
||||
undefined
|
||||
);
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"azureOpenAIApiDeploymentNameRag",
|
||||
"Azure Text Embedding Model Deployment Name",
|
||||
"Azure Project Text Embedding Model Deployment Name (e.g. text-embedding-ada-002)",
|
||||
undefined
|
||||
);
|
||||
preferences.registerPreferences<string>(
|
||||
register,
|
||||
PLUGIN_NAME,
|
||||
"azureOpenAIBasePath",
|
||||
"Azure Base Path",
|
||||
"Azure Project Base Path",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
const path = require("path");
|
||||
const { app } = require("electron");
|
||||
const { DirectoryLoader } = require("langchain/document_loaders/fs/directory");
|
||||
const { OpenAIEmbeddings } = require("langchain/embeddings/openai");
|
||||
const { PDFLoader } = require("langchain/document_loaders/fs/pdf");
|
||||
const { CharacterTextSplitter } = require("langchain/text_splitter");
|
||||
const { FaissStore } = require("langchain/vectorstores/faiss");
|
||||
const { ChatOpenAI } = require("langchain/chat_models/openai");
|
||||
const { RetrievalQAChain } = require("langchain/chains");
|
||||
|
||||
var db: any | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Ingests documents from the specified directory
|
||||
* If an `embedding` object is not provided, uses OpenAIEmbeddings.
|
||||
* The resulting embeddings are stored in the database using Faiss.
|
||||
* @param docDir - The directory containing the documents to ingest.
|
||||
* @param embedding - An optional object used to generate embeddings for the documents.
|
||||
* @param config - An optional configuration object used to create a new `OpenAIEmbeddings` object.
|
||||
*/
|
||||
async function ingest(docDir: string, embedding?: any, config?: any) {
|
||||
const loader = new DirectoryLoader(docDir, {
|
||||
".pdf": (path) => new PDFLoader(path),
|
||||
});
|
||||
const docs = await loader.load();
|
||||
const textSplitter = new CharacterTextSplitter();
|
||||
const docsQA = await textSplitter.splitDocuments(docs);
|
||||
const embeddings = embedding ?? new OpenAIEmbeddings({ ...config });
|
||||
db = await FaissStore.fromDocuments(await docsQA, embeddings);
|
||||
console.log("Documents are ingested");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an answer to a given question using the specified `llm` or a new `ChatOpenAI`.
|
||||
* The function uses the `RetrievalQAChain` class to retrieve the most relevant document from the database and generate an answer.
|
||||
* @param question - The question to generate an answer for.
|
||||
* @param llm - An optional object used to generate the answer.
|
||||
* @param config - An optional configuration object used to create a new `ChatOpenAI` object, can be ignored if llm is specified.
|
||||
* @returns A Promise that resolves with the generated answer.
|
||||
*/
|
||||
async function chatWithDocs(question: string, llm?: any, config?: any): Promise<any> {
|
||||
const llm_question_answer =
|
||||
llm ??
|
||||
new ChatOpenAI({
|
||||
temperature: 0.2,
|
||||
...config,
|
||||
});
|
||||
const qa = RetrievalQAChain.fromLLM(llm_question_answer, db.asRetriever(), {
|
||||
verbose: true,
|
||||
});
|
||||
const answer = await qa.run(question);
|
||||
return answer;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ingest,
|
||||
chatWithDocs,
|
||||
};
|
||||
@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"module": "ES6",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": false,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const packageJson = require("./package.json");
|
||||
|
||||
module.exports = {
|
||||
experiments: { outputModule: true },
|
||||
entry: "./src/index.ts",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
PLUGIN_NAME: JSON.stringify(packageJson.name),
|
||||
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
filename: "index.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
library: { type: "module" },
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".js"],
|
||||
},
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
};
|
||||
@ -1,10 +1,10 @@
|
||||
import { useAtomValue } from 'jotai'
|
||||
import React from 'react'
|
||||
import ModelTable from '../ModelTable'
|
||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
||||
|
||||
const ActiveModelTable: React.FC = () => {
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
|
||||
if (!activeModel) return null
|
||||
|
||||
|
||||
@ -3,12 +3,13 @@ import ModelDownloadButton from '../ModelDownloadButton'
|
||||
import ModelDownloadingButton from '../ModelDownloadingButton'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
model: AssistantModel
|
||||
model: Model
|
||||
isRecommend: boolean
|
||||
required?: string
|
||||
onDownloadClick?: (model: AssistantModel) => void
|
||||
onDownloadClick?: (model: Model) => void
|
||||
}
|
||||
|
||||
const AvailableModelCard: React.FC<Props> = ({
|
||||
@ -53,7 +54,7 @@ const AvailableModelCard: React.FC<Props> = ({
|
||||
description={model.shortDescription}
|
||||
isRecommend={isRecommend}
|
||||
name={model.name}
|
||||
type={model.type}
|
||||
type={'LLM'}
|
||||
/>
|
||||
{downloadButton}
|
||||
</div>
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
|
||||
import React from 'react'
|
||||
import ChatItem from '../ChatItem'
|
||||
import useChatMessages from '@hooks/useChatMessages'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { getCurrentChatMessagesAtom } from '@helpers/atoms/ChatMessage.atom'
|
||||
|
||||
const ChatBody: React.FC = () => {
|
||||
const { messages } = useChatMessages()
|
||||
const messages = useAtomValue(getCurrentChatMessagesAtom)
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-1 flex-col-reverse overflow-y-auto [&>*:nth-child(odd)]:bg-background">
|
||||
|
||||
@ -2,9 +2,10 @@ import React from 'react'
|
||||
import Image from 'next/image'
|
||||
import useCreateConversation from '@hooks/useCreateConversation'
|
||||
import { PlayIcon } from '@heroicons/react/24/outline'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
model: AssistantModel
|
||||
model: Model
|
||||
}
|
||||
|
||||
const ConversationalCard: React.FC<Props> = ({ model }) => {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
import ConversationalCard from '../ConversationalCard'
|
||||
import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
type Props = {
|
||||
models: AssistantModel[]
|
||||
models: Model[]
|
||||
}
|
||||
|
||||
const ConversationalList: React.FC<Props> = ({ models }) => (
|
||||
|
||||
@ -12,15 +12,11 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import DraggableProgressBar from '../DraggableProgressBar'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { activeBotAtom } from '@helpers/atoms/Bot.atom'
|
||||
import {
|
||||
rightSideBarExpandStateAtom,
|
||||
} from '@helpers/atoms/SideBarExpand.atom'
|
||||
import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
|
||||
import {
|
||||
MainViewState,
|
||||
setMainViewStateAtom,
|
||||
} from '@helpers/atoms/MainView.atom'
|
||||
import { DataService } from '@janhq/core'
|
||||
import { executeSerial } from '@services/pluginService'
|
||||
|
||||
const CreateBotContainer: React.FC = () => {
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
@ -30,7 +26,7 @@ const CreateBotContainer: React.FC = () => {
|
||||
|
||||
const createBot = async (bot: Bot) => {
|
||||
try {
|
||||
await executeSerial(DataService.CreateBot, bot)
|
||||
// await executeSerial(DataService.CreateBot, bot)
|
||||
} catch (err) {
|
||||
alert(err)
|
||||
console.error(err)
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
import DownloadModelContent from '../DownloadModelContent'
|
||||
|
||||
type Props = {
|
||||
model: AssistantModel
|
||||
model: Model
|
||||
isRecommend: boolean
|
||||
required?: string
|
||||
transferred?: number
|
||||
onDeleteClick?: (model: AssistantModel) => void
|
||||
onDeleteClick?: (model: Model) => void
|
||||
}
|
||||
|
||||
const DownloadedModelCard: React.FC<Props> = ({
|
||||
@ -22,7 +23,7 @@ const DownloadedModelCard: React.FC<Props> = ({
|
||||
description={model.shortDescription}
|
||||
isRecommend={isRecommend}
|
||||
name={model.name}
|
||||
type={model.type}
|
||||
type={'LLM'}
|
||||
/>
|
||||
<div className="flex flex-col justify-center">
|
||||
<button onClick={() => onDeleteClick?.(model)}>Delete</button>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import SearchBar from '../SearchBar'
|
||||
import ModelTable from '../ModelTable'
|
||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import ExploreModelItemHeader from '../ExploreModelItemHeader'
|
||||
import { Button } from '@uikit'
|
||||
import ModelVersionList from '../ModelVersionList'
|
||||
import { Fragment, forwardRef, useEffect, useState } from 'react'
|
||||
import { forwardRef, useEffect, useState } from 'react'
|
||||
import SimpleTag from '../SimpleTag'
|
||||
import {
|
||||
MiscellanousTag,
|
||||
@ -18,9 +18,10 @@ import {
|
||||
import { displayDate } from '@utils/datetime'
|
||||
import useGetMostSuitableModelVersion from '@hooks/useGetMostSuitableModelVersion'
|
||||
import { toGigabytes } from '@utils/converter'
|
||||
import { ModelCatalog } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
model: Product
|
||||
model: ModelCatalog
|
||||
}
|
||||
|
||||
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
|
||||
|
||||
@ -13,10 +13,11 @@ import {
|
||||
} from '@helpers/atoms/MainView.atom'
|
||||
import ConfirmationModal from '../ConfirmationModal'
|
||||
import { showingCancelDownloadModalAtom } from '@helpers/atoms/Modal.atom'
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
suitableModel: ModelVersion
|
||||
exploreModel: Product
|
||||
exploreModel: ModelCatalog
|
||||
}
|
||||
|
||||
const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
|
||||
@ -10,9 +10,9 @@ import {
|
||||
} from '@helpers/atoms/MainView.atom'
|
||||
import { displayDate } from '@utils/datetime'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import useStartStopModel from '@hooks/useStartStopModel'
|
||||
import useGetModelById from '@hooks/useGetModelById'
|
||||
import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom'
|
||||
|
||||
type Props = {
|
||||
conversation: Conversation
|
||||
@ -29,12 +29,12 @@ const HistoryItem: React.FC<Props> = ({
|
||||
}) => {
|
||||
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
|
||||
const isSelected = activeConvoId === conversation._id
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const { startModel } = useStartStopModel()
|
||||
const { getModelById } = useGetModelById()
|
||||
|
||||
const setMainViewState = useSetAtom(setMainViewStateAtom)
|
||||
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
|
||||
const models = useAtomValue(downloadedModelAtom)
|
||||
|
||||
const onClick = async () => {
|
||||
if (conversation.modelId == null) {
|
||||
@ -42,7 +42,7 @@ const HistoryItem: React.FC<Props> = ({
|
||||
return
|
||||
}
|
||||
|
||||
const model = await getModelById(conversation.modelId)
|
||||
const model = models.find((e) => e._id === conversation.modelId)
|
||||
if (model != null) {
|
||||
if (activeModel == null) {
|
||||
// if there's no active model, we simply load conversation's model
|
||||
|
||||
@ -7,7 +7,7 @@ import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import SecondaryButton from '../SecondaryButton'
|
||||
import { PlusIcon } from '@heroicons/react/24/outline'
|
||||
import useCreateConversation from '@hooks/useCreateConversation'
|
||||
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||
import {
|
||||
currentConvoStateAtom,
|
||||
getActiveConvoIdAtom,
|
||||
@ -18,7 +18,7 @@ import { userConversationsAtom } from '@helpers/atoms/Conversation.atom'
|
||||
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
|
||||
|
||||
const InputToolbar: React.FC = () => {
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const currentConvoState = useAtomValue(currentConvoStateAtom)
|
||||
const { inputState, currentConvo } = useGetInputState()
|
||||
const { requestCreateConvo } = useCreateConversation()
|
||||
|
||||
@ -11,7 +11,7 @@ import { MagnifyingGlassIcon, PlusIcon } from '@heroicons/react/24/outline'
|
||||
import useCreateConversation from '@hooks/useCreateConversation'
|
||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
||||
import { Button } from '@uikit'
|
||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { showingModalNoActiveModel } from '@helpers/atoms/Modal.atom'
|
||||
import {
|
||||
FeatureToggleContext,
|
||||
@ -20,7 +20,7 @@ import {
|
||||
const LeftHeaderAction: React.FC = () => {
|
||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const { requestCreateConvo } = useCreateConversation()
|
||||
const setShowModalNoActiveModel = useSetAtom(showingModalNoActiveModel)
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
|
||||
@ -4,16 +4,17 @@ import { useAtomValue } from 'jotai'
|
||||
import ModelActionButton, { ModelActionType } from '../ModelActionButton'
|
||||
import useStartStopModel from '@hooks/useStartStopModel'
|
||||
import useDeleteModel from '@hooks/useDeleteModel'
|
||||
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||
import { toGigabytes } from '@utils/converter'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
model: AssistantModel
|
||||
model: Model
|
||||
}
|
||||
|
||||
const ModelRow: React.FC<Props> = ({ model }) => {
|
||||
const { startModel, stopModel } = useStartStopModel()
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const { deleteModel } = useDeleteModel()
|
||||
const { loading, model: currentModelState } = useAtomValue(stateModel)
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
import { selectedModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
@ -19,7 +20,7 @@ const SelectModels: React.FC = () => {
|
||||
}
|
||||
}, [downloadedModels])
|
||||
|
||||
const onModelSelected = (model: AssistantModel) => {
|
||||
const onModelSelected = (model: Model) => {
|
||||
setSelectedModel(model)
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import ModelRow from '../ModelRow'
|
||||
import ModelTableHeader from '../ModelTableHeader'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
models: AssistantModel[]
|
||||
models: Model[]
|
||||
}
|
||||
|
||||
const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS']
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
|
||||
import Image from 'next/image'
|
||||
import useDownloadModel from '@hooks/useDownloadModel'
|
||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
||||
import { atom, useAtomValue } from 'jotai'
|
||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
||||
import SimpleTag from '../SimpleTag'
|
||||
import { RamRequired, UsecaseTag } from '../SimpleTag/TagType'
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
model: Product
|
||||
model: ModelCatalog
|
||||
modelVersion: ModelVersion
|
||||
isRecommended: boolean
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import React from 'react'
|
||||
import ModelVersionItem from '../ModelVersionItem'
|
||||
import { ModelCatalog, ModelVersion } from '@janhq/core/lib/types'
|
||||
|
||||
type Props = {
|
||||
model: Product
|
||||
model: ModelCatalog
|
||||
versions: ModelVersion[]
|
||||
recommendedVersion: string
|
||||
}
|
||||
|
||||
@ -6,11 +6,11 @@ import useGetAppVersion from '@hooks/useGetAppVersion'
|
||||
import useGetSystemResources from '@hooks/useGetSystemResources'
|
||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
||||
import { formatDownloadPercentage } from '@utils/converter'
|
||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
||||
|
||||
const MonitorBar: React.FC = () => {
|
||||
const progress = useAtomValue(appDownloadProgress)
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const { version } = useGetAppVersion()
|
||||
const { ram, cpu } = useGetSystemResources()
|
||||
const modelDownloadStates = useAtomValue(modelDownloadStateAtom)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { plugins, extensionPoints } from '@plugin'
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
ChartPieIcon,
|
||||
CommandLineIcon,
|
||||
@ -9,10 +8,9 @@ import {
|
||||
|
||||
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
|
||||
import classNames from 'classnames'
|
||||
import { DataService, PluginService, preferences } from '@janhq/core'
|
||||
import { execute } from '@plugin/extension-manager'
|
||||
import LoadingIndicator from './LoadingIndicator'
|
||||
import { executeSerial } from '@services/pluginService'
|
||||
import { FeatureToggleContext } from '@helpers/FeatureToggleWrapper'
|
||||
import { pluginManager } from '@plugin/PluginManager'
|
||||
|
||||
export const Preferences = () => {
|
||||
const [search, setSearch] = useState<string>('')
|
||||
@ -25,14 +23,22 @@ export const Preferences = () => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const experimentRef = useRef(null)
|
||||
const preferenceRef = useRef(null)
|
||||
|
||||
const { experimentalFeatureEnabed } = useContext(FeatureToggleContext)
|
||||
/**
|
||||
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
|
||||
*/
|
||||
useEffect(() => {
|
||||
executeSerial(DataService.GetPluginManifest).then((data: any) => {
|
||||
setPluginCatalog(data)
|
||||
})
|
||||
if (!window.electronAPI) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get plugin manifest
|
||||
import(/* webpackIgnore: true */ PLUGIN_CATALOG + `?t=${Date.now()}`).then(
|
||||
(data) => {
|
||||
if (Array.isArray(data.default) && experimentalFeatureEnabed)
|
||||
setPluginCatalog(data.default)
|
||||
}
|
||||
)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
@ -44,39 +50,8 @@ export const Preferences = () => {
|
||||
*/
|
||||
useEffect(() => {
|
||||
const getActivePlugins = async () => {
|
||||
const plgs = await plugins.getActive()
|
||||
const plgs = await pluginManager.getActive()
|
||||
setActivePlugins(plgs)
|
||||
|
||||
if (extensionPoints.get('experimentComponent')) {
|
||||
const components = await Promise.all(
|
||||
extensionPoints.execute('experimentComponent', {})
|
||||
)
|
||||
if (components.length > 0) {
|
||||
setIsTestAvailable(true)
|
||||
}
|
||||
components.forEach((e) => {
|
||||
if (experimentRef.current) {
|
||||
// @ts-ignore
|
||||
experimentRef.current.appendChild(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (extensionPoints.get('PluginPreferences')) {
|
||||
const data = await Promise.all(
|
||||
extensionPoints.execute('PluginPreferences', {})
|
||||
)
|
||||
setPreferenceItems(Array.isArray(data) ? data : [])
|
||||
Promise.all(
|
||||
(Array.isArray(data) ? data : []).map((e) =>
|
||||
preferences
|
||||
.get(e.pluginName, e.preferenceKey)
|
||||
.then((k) => ({ key: e.preferenceKey, value: k }))
|
||||
)
|
||||
).then((data) => {
|
||||
setPreferenceValues(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
getActivePlugins()
|
||||
}, [])
|
||||
@ -93,7 +68,7 @@ export const Preferences = () => {
|
||||
|
||||
// Send the filename of the to be installed plugin
|
||||
// to the main process for installation
|
||||
const installed = await plugins.install([pluginFile])
|
||||
const installed = await pluginManager.install([pluginFile])
|
||||
if (installed) window.coreAPI?.relaunch()
|
||||
}
|
||||
|
||||
@ -105,7 +80,7 @@ export const Preferences = () => {
|
||||
const uninstall = async (name: string) => {
|
||||
// Send the filename of the to be uninstalled plugin
|
||||
// to the main process for removal
|
||||
const res = await plugins.uninstall([name])
|
||||
const res = await pluginManager.uninstall([name])
|
||||
if (res) window.coreAPI?.relaunch()
|
||||
}
|
||||
|
||||
@ -131,7 +106,7 @@ export const Preferences = () => {
|
||||
const downloadTarball = async (pluginName: string) => {
|
||||
setIsLoading(true)
|
||||
const pluginPath = await window.coreAPI?.installRemotePlugin(pluginName)
|
||||
const installed = await plugins.install([pluginPath])
|
||||
const installed = await pluginManager.install([pluginPath])
|
||||
setIsLoading(false)
|
||||
if (installed) window.coreAPI.relaunch()
|
||||
}
|
||||
@ -144,11 +119,6 @@ export const Preferences = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
if (extensionPoints.get(PluginService.OnPreferencesUpdate))
|
||||
timeout = setTimeout(
|
||||
() => execute(PluginService.OnPreferencesUpdate, {}),
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -408,11 +378,7 @@ export const Preferences = () => {
|
||||
(v) => v.key === e.preferenceKey
|
||||
)[0]?.value
|
||||
}
|
||||
onChange={(event) => {
|
||||
preferences
|
||||
.set(e.pluginName, e.preferenceKey, event.target.value)
|
||||
.then(() => notifyPreferenceUpdate())
|
||||
}}
|
||||
onChange={(event) => {}}
|
||||
></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
MainViewState,
|
||||
setMainViewStateAtom,
|
||||
} from '@helpers/atoms/MainView.atom'
|
||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
|
||||
import { Button } from '@uikit'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
@ -18,7 +18,7 @@ enum ActionButton {
|
||||
|
||||
const SidebarEmptyHistory: React.FC = () => {
|
||||
const { downloadedModels } = useGetDownloadedModels()
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const setMainView = useSetAtom(setMainViewStateAtom)
|
||||
const { requestCreateConvo } = useCreateConversation()
|
||||
const [action, setAction] = useState(ActionButton.DownloadModel)
|
||||
|
||||
@ -3,16 +3,17 @@ import { Dialog, Transition } from '@headlessui/react'
|
||||
import { ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { switchingModelConfirmationModalPropsAtom } from '@helpers/atoms/Modal.atom'
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom } from '@helpers/atoms/Model.atom'
|
||||
import useStartStopModel from '@hooks/useStartStopModel'
|
||||
import { Model } from '@janhq/core/lib/types'
|
||||
|
||||
export type SwitchingModelConfirmationModalProps = {
|
||||
replacingModel: AssistantModel
|
||||
replacingModel: Model
|
||||
}
|
||||
|
||||
const SwitchingModelConfirmationModal: React.FC = () => {
|
||||
const [props, setProps] = useAtom(switchingModelConfirmationModalPropsAtom)
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const { startModel } = useStartStopModel()
|
||||
|
||||
const onConfirmSwitchModelClick = () => {
|
||||
|
||||
@ -4,13 +4,13 @@ import useGetSystemResources from '@hooks/useGetSystemResources'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
|
||||
import { formatDownloadPercentage } from '@utils/converter'
|
||||
import { activeAssistantModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||
import { activeModelAtom, stateModel } from '@helpers/atoms/Model.atom'
|
||||
import useGetAppVersion from '@hooks/useGetAppVersion'
|
||||
import ProgressBar from '@/_components/ProgressBar'
|
||||
import { appDownloadProgress } from '@helpers/JotaiWrapper'
|
||||
|
||||
const BottomBar = () => {
|
||||
const activeModel = useAtomValue(activeAssistantModelAtom)
|
||||
const activeModel = useAtomValue(activeModelAtom)
|
||||
const stateModelStartStop = useAtomValue(stateModel)
|
||||
const { ram, cpu } = useGetSystemResources()
|
||||
const modelDownloadStates = useAtomValue(modelDownloadStateAtom)
|
||||
|
||||
@ -1,21 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { PluginService } from '@janhq/core'
|
||||
import { ThemeWrapper } from '@helpers/ThemeWrapper'
|
||||
import JotaiWrapper from '@helpers/JotaiWrapper'
|
||||
import { ModalWrapper } from '@helpers/ModalWrapper'
|
||||
import { useEffect, useState } from 'react'
|
||||
import CompactLogo from '@containers/Logo/CompactLogo'
|
||||
import { setup, plugins, activationPoints, extensionPoints } from '@plugin'
|
||||
import EventListenerWrapper from '@helpers/EventListenerWrapper'
|
||||
import { setupCoreServices } from '@services/coreService'
|
||||
import {
|
||||
executeSerial,
|
||||
isCorePluginInstalled,
|
||||
setupBasePlugins,
|
||||
} from '@services/pluginService'
|
||||
import { FeatureToggleWrapper } from '@helpers/FeatureToggleWrapper'
|
||||
import { pluginManager } from '../../plugin/PluginManager'
|
||||
|
||||
const Providers = (props: PropsWithChildren) => {
|
||||
const [setupCore, setSetupCore] = useState(false)
|
||||
@ -24,26 +22,16 @@ const Providers = (props: PropsWithChildren) => {
|
||||
const { children } = props
|
||||
|
||||
async function setupPE() {
|
||||
// Enable activation point management
|
||||
setup({
|
||||
importer: (plugin: string) =>
|
||||
import(/* webpackIgnore: true */ plugin).catch((err) => {
|
||||
console.log(err)
|
||||
}),
|
||||
})
|
||||
|
||||
// Register all active plugins with their activation points
|
||||
await plugins.registerActive()
|
||||
await pluginManager.registerActive()
|
||||
|
||||
setTimeout(async () => {
|
||||
// Trigger activation points
|
||||
await activationPoints.trigger('init')
|
||||
if (!isCorePluginInstalled()) {
|
||||
setupBasePlugins()
|
||||
return
|
||||
}
|
||||
if (extensionPoints.get(PluginService.OnStart)) {
|
||||
await executeSerial(PluginService.OnStart)
|
||||
}
|
||||
|
||||
pluginManager.load()
|
||||
setActivated(true)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
@ -1,50 +1,59 @@
|
||||
import { addNewMessageAtom, updateMessageAtom } from './atoms/ChatMessage.atom'
|
||||
import {
|
||||
addNewMessageAtom,
|
||||
chatMessages,
|
||||
updateMessageAtom,
|
||||
} from './atoms/ChatMessage.atom'
|
||||
import { toChatMessage } from '@models/ChatMessage'
|
||||
import { events, EventName, NewMessageResponse, DataService } from '@janhq/core'
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { events, EventName, NewMessageResponse, PluginType } from '@janhq/core'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { ReactNode, useEffect, useRef } from 'react'
|
||||
import useGetBots from '@hooks/useGetBots'
|
||||
import useGetUserConversations from '@hooks/useGetUserConversations'
|
||||
import {
|
||||
updateConversationAtom,
|
||||
updateConversationWaitingForResponseAtom,
|
||||
userConversationsAtom,
|
||||
} from './atoms/Conversation.atom'
|
||||
import { executeSerial } from '@plugin/extension-manager'
|
||||
import { debounce } from 'lodash'
|
||||
import {
|
||||
setDownloadStateAtom,
|
||||
setDownloadStateSuccessAtom,
|
||||
} from './atoms/DownloadState.atom'
|
||||
import { downloadedModelAtom } from './atoms/DownloadedModel.atom'
|
||||
import { ModelManagementService } from '@janhq/core'
|
||||
import { getDownloadedModels } from '../hooks/useGetDownloadedModels'
|
||||
import { pluginManager } from '../plugin/PluginManager'
|
||||
import { Message } from '@janhq/core/lib/types'
|
||||
import { ConversationalPlugin, ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { downloadingModelsAtom } from './atoms/Model.atom'
|
||||
|
||||
let currentConversation: Conversation | undefined = undefined
|
||||
|
||||
const debouncedUpdateConversation = debounce(
|
||||
async (updatedConv: Conversation) => {
|
||||
await executeSerial(DataService.UpdateConversation, updatedConv)
|
||||
},
|
||||
1000
|
||||
)
|
||||
|
||||
export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
const addNewMessage = useSetAtom(addNewMessageAtom)
|
||||
const updateMessage = useSetAtom(updateMessageAtom)
|
||||
const updateConversation = useSetAtom(updateConversationAtom)
|
||||
const { getBotById } = useGetBots()
|
||||
const { getConversationById } = useGetUserConversations()
|
||||
|
||||
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
|
||||
const setDownloadState = useSetAtom(setDownloadStateAtom)
|
||||
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
|
||||
const setDownloadedModels = useSetAtom(downloadedModelAtom)
|
||||
const models = useAtomValue(downloadingModelsAtom)
|
||||
const messages = useAtomValue(chatMessages)
|
||||
const conversations = useAtomValue(userConversationsAtom)
|
||||
const messagesRef = useRef(messages)
|
||||
const convoRef = useRef(conversations)
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages
|
||||
convoRef.current = conversations
|
||||
}, [messages, conversations])
|
||||
|
||||
async function handleNewMessageResponse(message: NewMessageResponse) {
|
||||
if (message.conversationId) {
|
||||
const convo = await getConversationById(message.conversationId)
|
||||
const convo = convoRef.current.find(
|
||||
(e) => e._id == message.conversationId
|
||||
)
|
||||
if (!convo) return
|
||||
const botId = convo?.botId
|
||||
console.debug('botId', botId)
|
||||
if (botId) {
|
||||
const bot = await getBotById(botId)
|
||||
const newResponse = toChatMessage(message, bot)
|
||||
@ -75,27 +84,53 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
!currentConversation ||
|
||||
currentConversation._id !== messageResponse.conversationId
|
||||
) {
|
||||
currentConversation = await getConversationById(
|
||||
messageResponse.conversationId
|
||||
)
|
||||
if (convoRef.current && messageResponse.conversationId)
|
||||
currentConversation = convoRef.current.find(
|
||||
(e) => e._id == messageResponse.conversationId
|
||||
)
|
||||
}
|
||||
|
||||
const updatedConv: Conversation = {
|
||||
...currentConversation,
|
||||
lastMessage: messageResponse.message,
|
||||
}
|
||||
if (currentConversation) {
|
||||
const updatedConv: Conversation = {
|
||||
...currentConversation,
|
||||
lastMessage: messageResponse.message,
|
||||
}
|
||||
|
||||
updateConversation(updatedConv)
|
||||
debouncedUpdateConversation(updatedConv)
|
||||
updateConversation(updatedConv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMessageResponseFinished(
|
||||
messageResponse: NewMessageResponse
|
||||
) {
|
||||
if (!messageResponse.conversationId) return
|
||||
console.debug('handleMessageResponseFinished', messageResponse)
|
||||
if (!messageResponse.conversationId || !convoRef.current) return
|
||||
updateConvWaiting(messageResponse.conversationId, false)
|
||||
|
||||
const convo = convoRef.current.find(
|
||||
(e) => e._id == messageResponse.conversationId
|
||||
)
|
||||
if (convo) {
|
||||
const messagesData = (messagesRef.current ?? [])[convo._id].map<Message>(
|
||||
(e: ChatMessage) => {
|
||||
return {
|
||||
_id: e.id,
|
||||
message: e.text,
|
||||
user: e.senderUid,
|
||||
updatedAt: new Date(e.createdAt).toISOString(),
|
||||
createdAt: new Date(e.createdAt).toISOString(),
|
||||
}
|
||||
}
|
||||
)
|
||||
pluginManager
|
||||
.get<ConversationalPlugin>(PluginType.Conversational)
|
||||
?.saveConversation({
|
||||
...convo,
|
||||
_id: convo._id ?? '',
|
||||
name: convo.name ?? '',
|
||||
messages: messagesData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownloadUpdate(state: any) {
|
||||
@ -106,14 +141,16 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
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)
|
||||
})
|
||||
})
|
||||
const model = models.find((e) => e._id === state.fileName)
|
||||
if (model)
|
||||
pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.saveModel(model)
|
||||
.then(() => {
|
||||
getDownloadedModels().then((models) => {
|
||||
setDownloadedModels(models)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,8 +159,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
events.on(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
||||
events.on(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
||||
events.on(
|
||||
'OnMessageResponseFinished',
|
||||
// EventName.OnMessageResponseFinished,
|
||||
EventName.OnMessageResponseFinished,
|
||||
handleMessageResponseFinished
|
||||
)
|
||||
events.on(EventName.OnDownloadUpdate, handleDownloadUpdate)
|
||||
@ -136,8 +172,7 @@ export default function EventHandler({ children }: { children: ReactNode }) {
|
||||
events.off(EventName.OnNewMessageResponse, handleNewMessageResponse)
|
||||
events.off(EventName.OnMessageResponseUpdate, handleMessageResponseUpdate)
|
||||
events.off(
|
||||
'OnMessageResponseFinished',
|
||||
// EventName.OnMessageResponseFinished,
|
||||
EventName.OnMessageResponseFinished,
|
||||
handleMessageResponseFinished
|
||||
)
|
||||
events.off(EventName.OnDownloadUpdate, handleDownloadUpdate)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useSetAtom } from 'jotai'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { ReactNode, useEffect, useRef } from 'react'
|
||||
import { appDownloadProgress } from './JotaiWrapper'
|
||||
import { ModelManagementService } from '@janhq/core'
|
||||
import { PluginType } from '@janhq/core'
|
||||
import {
|
||||
setDownloadStateAtom,
|
||||
setDownloadStateSuccessAtom,
|
||||
@ -11,7 +11,9 @@ import {
|
||||
import { getDownloadedModels } from '../hooks/useGetDownloadedModels'
|
||||
import { downloadedModelAtom } from './atoms/DownloadedModel.atom'
|
||||
import EventHandler from './EventHandler'
|
||||
import { executeSerial } from '@services/pluginService'
|
||||
import { pluginManager } from '@plugin/PluginManager'
|
||||
import { ModelPlugin } from '@janhq/core/lib/plugins'
|
||||
import { downloadingModelsAtom } from './atoms/Model.atom'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
@ -22,6 +24,11 @@ export default function EventListenerWrapper({ children }: Props) {
|
||||
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom)
|
||||
const setProgress = useSetAtom(appDownloadProgress)
|
||||
const setDownloadedModels = useSetAtom(downloadedModelAtom)
|
||||
const models = useAtomValue(downloadingModelsAtom)
|
||||
const modelsRef = useRef(models)
|
||||
useEffect(() => {
|
||||
modelsRef.current = models
|
||||
}, [models])
|
||||
|
||||
useEffect(() => {
|
||||
if (window && window.electronAPI) {
|
||||
@ -42,16 +49,19 @@ export default function EventListenerWrapper({ children }: Props) {
|
||||
window.electronAPI.onFileDownloadSuccess(
|
||||
(_event: string, callback: any) => {
|
||||
if (callback && callback.fileName) {
|
||||
setDownloadStateSuccess(callback.fileName)
|
||||
const fileName = callback.fileName.replace('models/', '')
|
||||
setDownloadStateSuccess(fileName)
|
||||
|
||||
executeSerial(
|
||||
ModelManagementService.UpdateFinishedDownloadAt,
|
||||
callback.fileName
|
||||
).then(() => {
|
||||
getDownloadedModels().then((models) => {
|
||||
setDownloadedModels(models)
|
||||
})
|
||||
})
|
||||
const model = modelsRef.current.find((e) => e._id === fileName)
|
||||
if (model)
|
||||
pluginManager
|
||||
.get<ModelPlugin>(PluginType.Model)
|
||||
?.saveModel(model)
|
||||
.then(() => {
|
||||
getDownloadedModels().then((models) => {
|
||||
setDownloadedModels(models)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -4,7 +4,7 @@ import { getActiveConvoIdAtom } from './Conversation.atom'
|
||||
/**
|
||||
* Stores all chat messages for all conversations
|
||||
*/
|
||||
const chatMessages = atom<Record<string, ChatMessage[]>>({})
|
||||
export const chatMessages = atom<Record<string, ChatMessage[]>>({})
|
||||
|
||||
/**
|
||||
* Return the chat messages for the current active conversation
|
||||
@ -12,7 +12,8 @@ const chatMessages = atom<Record<string, ChatMessage[]>>({})
|
||||
export const getCurrentChatMessagesAtom = atom<ChatMessage[]>((get) => {
|
||||
const activeConversationId = get(getActiveConvoIdAtom)
|
||||
if (!activeConversationId) return []
|
||||
return get(chatMessages)[activeConversationId] ?? []
|
||||
const messages = get(chatMessages)[activeConversationId]
|
||||
return messages ?? []
|
||||
})
|
||||
|
||||
export const setCurrentChatMessagesAtom = atom(
|
||||
@ -29,6 +30,17 @@ export const setCurrentChatMessagesAtom = atom(
|
||||
}
|
||||
)
|
||||
|
||||
export const setConvoMessagesAtom = atom(
|
||||
null,
|
||||
(get, set, messages: ChatMessage[], convoId: string) => {
|
||||
const newData: Record<string, ChatMessage[]> = {
|
||||
...get(chatMessages),
|
||||
}
|
||||
newData[convoId] = messages
|
||||
set(chatMessages, newData)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Used for pagination. Add old messages to the current conversation
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user