Merge branch 'main' into download_button_change_on_os

This commit is contained in:
Sriman Vikram V 2023-11-06 12:44:57 +05:30 committed by GitHub
commit f621ae7511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
154 changed files with 2003 additions and 3642 deletions

View File

@ -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: |

View File

@ -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

View File

@ -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"

View File

@ -96,20 +96,23 @@ Note: This instruction is tested on MacOS only.
1. **Clone the Repository:**
```
```bash
git clone https://github.com/janhq/jan
git checkout DESIRED_BRANCH
cd jan
```
```
2. **Install dependencies:**
```
```bash
yarn install
# Build core module
yarn build:core
# Packing base plugins
yarn build:plugins
```
```
3. **Run development and Using Jan Desktop**
@ -127,6 +130,11 @@ Note: This instruction is tested on MacOS only.
git clone https://github.com/janhq/jan
cd jan
yarn install
# Build core module
yarn build:core
# Package base plugins
yarn build:plugins
# Build the app

View File

@ -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";

View File

@ -1,6 +1,6 @@
{
"name": "@janhq/core",
"version": "0.1.9",
"version": "0.1.10",
"description": "Plugin core lib",
"keywords": [
"jan",
@ -17,7 +17,7 @@
},
"exports": {
".": "./lib/index.js",
"./store": "./lib/store.js"
"./plugin": "./lib/plugins/index.js"
},
"files": [
"lib",

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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[];
};
/**

57
core/src/fs.ts Normal file
View File

@ -0,0 +1,57 @@
/**
* 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);
/**
* Removes a directory at the specified path.
* @param {string} path - The path of the directory to remove.
* @returns {Promise<any>} A Promise that resolves when the directory is removed successfully.
*/
const rmdir: (path: string) => Promise<any> = (path) =>
window.coreAPI?.rmdir(path) ?? window.electronAPI?.rmdir(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,
rmdir,
deleteFile,
};

40
core/src/index.ts Normal file
View 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
View 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;
}

View 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
View 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";

View 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
View 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[]>;
}

View 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
View 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;
};

View File

@ -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,
};

View File

@ -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__/*"]
}

118
electron/handlers/fs.ts Normal file
View File

@ -0,0 +1,118 @@
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();
}
}
);
});
});
/**
* Removes a directory in the user data directory.
* @param event - The event object.
* @param path - The path of the directory to remove.
* @returns A promise that resolves when the directory is removed successfully.
*/
ipcMain.handle("rmdir", async (event, path: string): Promise<void> => {
return new Promise((resolve, reject) => {
fs.rmdir(
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);
}
});
});
}
);
}

View File

@ -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.

View File

@ -1,7 +1,61 @@
/**
* 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} rmdir - Removes a directory at the given path recursively.
* @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 +86,17 @@ 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),
rmdir: (path: string) => ipcRenderer.invoke("rmdir", path),
installRemotePlugin: (pluginName: string) =>
ipcRenderer.invoke("installRemotePlugin", pluginName),

View File

@ -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"
}
}

View File

@ -0,0 +1,39 @@
{
"name": "@janhq/conversational-json",
"version": "1.0.0",
"description": "Conversational Plugin - Stores jan app conversations as JSON",
"main": "dist/index.js",
"author": "Jan <service@jan.ai>",
"license": "MIT",
"activationPoints": [
"init"
],
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"postinstall": "rimraf *.tgz --glob && npm run build",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
},
"exports": {
".": "./dist/index.js",
"./main": "./dist/module.js"
},
"devDependencies": {
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@janhq/core": "file:../../core",
"ts-loader": "^9.5.0"
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/*",
"package.json",
"README.md"
],
"bundleDependencies": []
}

View File

@ -0,0 +1,87 @@
import { PluginType, fs } from "@janhq/core";
import { ConversationalPlugin } from "@janhq/core/lib/plugins";
import { Conversation } from "@janhq/core/lib/types";
/**
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
* functionality for managing conversations.
*/
export default class JSONConversationalPlugin implements ConversationalPlugin {
/**
* Returns the type of the plugin.
*/
type(): PluginType {
return PluginType.Conversational;
}
/**
* Called when the plugin is loaded.
*/
onLoad() {
fs.mkdir("conversations")
console.debug("JSONConversationalPlugin loaded")
}
/**
* Called when the plugin is unloaded.
*/
onUnload() {
console.debug("JSONConversationalPlugin 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) =>
fs
.readFile(`conversations/${conversationId}/${conversationId}.json`)
.then((data) => {
return JSON.parse(data) as Conversation;
})
)
).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 fs
.mkdir(`conversations/${conversation._id}`)
.then(() =>
fs.writeFile(
`conversations/${conversation._id}/${conversation._id}.json`,
JSON.stringify(conversation)
)
);
}
/**
* Deletes a conversation with the specified ID.
* @param conversationId The ID of the conversation to delete.
*/
deleteConversation(conversationId: string): Promise<void> {
return fs.rmdir(`conversations/${conversationId}`);
}
/**
* 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-")));
});
}
}

View File

@ -7,6 +7,8 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true
}
"skipLibCheck": true,
"rootDir": "./src"
},
"include": ["./src"]
}

View File

@ -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
};

View File

@ -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": []
}

View File

@ -0,0 +1,214 @@
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.
*/
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");
fs.mkdir("conversations");
}
/**
* 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}/${conversationId}.md`
)
)
).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.rmdir(`conversations/${conversationId}`);
}
/**
* 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 (m-\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;
}
}
if (currentMessage) {
conversation.messages.push(currentMessage);
}
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) {
// Generate the Markdown content
const markdownContent = this.generateMarkdown(conversation);
await fs.mkdir(`conversations/${conversation._id}`);
// Write the content to a Markdown file
await fs.writeFile(
`conversations/${conversation._id}/${conversation._id}.md`,
markdownContent
);
}
}

View File

@ -4,10 +4,11 @@
"module": "ES6",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true
}
"skipLibCheck": true,
"rootDir": "./src"
},
"include": ["./src"]
}

View File

@ -1,10 +1,9 @@
const path = require("path");
const webpack = require("webpack");
const packageJson = require("./package.json");
module.exports = {
experiments: { outputModule: true },
entry: "./src/index.ts",
entry: "./src/index.ts", // Adjust the entry point to match your project's main file
mode: "production",
module: {
rules: [
@ -15,21 +14,18 @@ module.exports = {
},
],
},
plugins: [
new webpack.DefinePlugin({
PLUGIN_NAME: JSON.stringify(packageJson.name),
MODULE_PATH: JSON.stringify(`${packageJson.name}/${packageJson.module}`),
}),
],
output: {
filename: "index.js",
filename: "index.js", // Adjust the output file name as needed
path: path.resolve(__dirname, "dist"),
library: { type: "module" },
library: { type: "module" }, // Specify ESM output format
},
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
};

View File

@ -1,3 +0,0 @@
declare const PLUGIN_NAME: string;
declare const MODULE_PATH: string;
declare const PLUGIN_CATALOG: string;

View File

@ -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.

View File

@ -1,8 +0,0 @@
{
"extends": "./../tsconfig.json",
"compilerOptions": {
"outDir": "./../dist/cjs",
"module": "commonjs"
},
"files": ["../module.ts"]
}

View File

@ -1,9 +0,0 @@
{
"extends": "./../tsconfig.json",
"compilerOptions": {
"outDir": "./../dist/esm",
"module": "esnext"
},
"include": ["@types/*"],
"files": ["../@types/global.d.ts", "../index.ts"]
}

View File

@ -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);
}

View File

@ -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,
};

View File

@ -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"
]
}

View File

@ -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
};

View File

@ -1,3 +0,0 @@
declare const PLUGIN_NAME: string;
declare const MODULE_PATH: string;
declare const INFERENCE_URL: string;

View File

@ -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
);
}

View File

@ -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",

View File

@ -0,0 +1,2 @@
declare const MODULE: string;
declare const INFERENCE_URL: string;

View File

@ -0,0 +1,3 @@
export const generateMessageId = () => {
return `m-${Date.now()}`
}

View 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));
});
}

View File

@ -0,0 +1,141 @@
/**
* @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";
import { generateMessageId } from "./helpers/message";
/**
* 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: generateMessageId(),
};
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);
},
});
}
}

View File

@ -8,6 +8,8 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true
}
"skipLibCheck": true,
"rootDir": "./src"
},
"include": ["./src"]
}

View File

@ -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"),
}),
],

View File

@ -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
);
}

View File

@ -1 +0,0 @@
module.exports = {};

View File

@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "es2016",
"module": "esnext",
"moduleResolution": "node",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true
}
}

View File

@ -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"
}
}

View 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;
}

View 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);
})
);
}
}

View File

@ -7,6 +7,8 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true
}
"skipLibCheck": true,
"rootDir": "./src"
},
"include": ["./src"]
}

View File

@ -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: [

View File

@ -1,2 +0,0 @@
declare const PLUGIN_NAME: string;
declare const MODULE_PATH: string;

View File

@ -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);
}

View File

@ -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"
]
}

View File

@ -0,0 +1 @@
declare const MODULE: string;

View 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");
}
}

View File

@ -7,6 +7,8 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": false,
"skipLibCheck": true
}
"skipLibCheck": true,
"rootDir": "./src"
},
"include": ["./src"]
}

View File

@ -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: {

View File

@ -1,2 +0,0 @@
declare const PLUGIN_NAME: string;
declare const MODULE_PATH: string;

View File

@ -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",
""
);
}

View File

@ -1,2 +0,0 @@
declare const PLUGIN_NAME: string;
declare const MODULE_PATH: string;

View File

@ -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!

View File

@ -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"
]
}

View File

@ -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
);
}

View File

@ -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,
};

View File

@ -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

View File

@ -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>

View File

@ -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">

View File

@ -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 }) => {

View File

@ -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 }) => (

View File

@ -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)

View File

@ -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>

View File

@ -1,5 +1,4 @@
import React from 'react'
import SearchBar from '../SearchBar'
import ModelTable from '../ModelTable'
import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'

View File

@ -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) => {

View File

@ -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> = ({

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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']

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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>

View File

@ -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)

View File

@ -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 = () => {

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More