diff --git a/.github/workflows/jan-electron-build.yml b/.github/workflows/jan-electron-build.yml index 4d5835f1e4..7d3f7adcb0 100644 --- a/.github/workflows/jan-electron-build.yml +++ b/.github/workflows/jan-electron-build.yml @@ -121,22 +121,10 @@ jobs: env: VERSION_TAG: ${{ steps.tag.outputs.tag }} - - name: Build uikit + - name: Build app + shell: cmd run: | - cd uikit - yarn config set network-timeout 300000 - yarn install - yarn build - - - name: Install yarn dependencies - shell: powershell - run: | - yarn config set network-timeout 300000 - yarn build:core - yarn install - $env:NITRO_VERSION = Get-Content .\plugins\inference-plugin\nitro\version.txt; echo $env:NITRO_VERSION - yarn build:plugins - yarn build + make build - name: Windows Code Sign with AzureSignTool run: | diff --git a/Makefile b/Makefile index af416304cc..3a3d58b833 100644 --- a/Makefile +++ b/Makefile @@ -15,14 +15,11 @@ endif # Installs yarn dependencies and builds core and plugins install-and-build: build-uikit ifeq ($(OS),Windows_NT) - powershell -Command "yarn config set network-timeout 300000; \ - $$env:NITRO_VERSION = Get-Content .\\plugins\\inference-plugin\\nitro\\version.txt; \ - Write-Output \"Nitro version: $$env:NITRO_VERSION\"; yarn build:core; yarn install; yarn build:plugins" -else + yarn config set network-timeout 300000 +endif yarn build:core yarn install yarn build:plugins -endif dev: install-and-build yarn dev @@ -47,8 +44,21 @@ build: install-and-build clean: ifeq ($(OS),Windows_NT) powershell -Command "Get-ChildItem -Path . -Include node_modules, .next, dist -Recurse -Directory | Remove-Item -Recurse -Force" + rmdir /s /q "%USERPROFILE%\AppData\Roaming\jan" + rmdir /s /q "%USERPROFILE%\AppData\Roaming\jan-electron" + rmdir /s /q "%USERPROFILE%\AppData\Local\jan*" +else ifeq ($(shell uname -s),Linux) + find . -name "node_modules" -type d -prune -exec rm -rf '{}' + + find . -name ".next" -type d -exec rm -rf '{}' + + find . -name "dist" -type d -exec rm -rf '{}' + + rm -rf "~/.config/jan" + rm -rf "~/.config/jan-electron" + rm -rf "~/.cache/jan*" else find . -name "node_modules" -type d -prune -exec rm -rf '{}' + find . -name ".next" -type d -exec rm -rf '{}' + find . -name "dist" -type d -exec rm -rf '{}' + + rm -rf ~/Library/Application\ Support/jan + rm -rf ~/Library/Application\ Support/jan-electron + rm -rf ~/Library/Caches/jan* endif diff --git a/core/src/core.ts b/core/src/core.ts index 4f221f1721..1c4ca1a0a9 100644 --- a/core/src/core.ts +++ b/core/src/core.ts @@ -12,20 +12,7 @@ const executeOnMain: ( method: string, ...args: any[] ) => Promise = (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 = (plugin, method, ...args) => - window.coreAPI?.invokePluginFunc(plugin, method, ...args) ?? - window.electronAPI?.invokePluginFunc(plugin, method, ...args); + window.coreAPI?.invokePluginFunc(plugin, method, ...args) /** * Downloads a file from a URL and saves it to the local file system. @@ -36,16 +23,15 @@ const invokePluginFunc: ( const downloadFile: (url: string, fileName: string) => Promise = ( url, fileName -) => - window.coreAPI?.downloadFile(url, fileName) ?? - window.electronAPI?.downloadFile(url, fileName); +) => window.coreAPI?.downloadFile(url, fileName); /** - * @deprecated This object is deprecated and should not be used. - * Use fs module instead. + * Aborts the download of a specific file. + * @param {string} fileName - The name of the file whose download is to be aborted. + * @returns {Promise} A promise that resolves when the download has been aborted. */ -const deleteFile: (path: string) => Promise = (path) => - window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path); +const abortDownload: (fileName: string) => Promise = (fileName) => + window.coreAPI?.abortDownload(fileName); /** * Retrieves the path to the app data directory using the `coreAPI` object. @@ -58,11 +44,18 @@ const appDataPath: () => Promise = () => window.coreAPI?.appDataPath(); * Gets the user space path. * @returns {Promise} A Promise that resolves with the user space path. */ -const getUserSpace = (): Promise => - window.coreAPI?.getUserSpace() ?? window.electronAPI?.getUserSpace(); +const getUserSpace = (): Promise => window.coreAPI?.getUserSpace(); -/** Register extension point function type definition - * +/** + * Opens the file explorer at a specific path. + * @param {string} path - The path to open in the file explorer. + * @returns {Promise} A promise that resolves when the file explorer is opened. + */ +const openFileExplorer: (path: string) => Promise = (path) => + window.coreAPI?.openFileExplorer(path); + +/** + * Register extension point function type definition */ export type RegisterExtensionPoint = ( extensionName: string, @@ -71,27 +64,14 @@ export type RegisterExtensionPoint = ( priority?: number ) => void; -/** - * @deprecated This object is deprecated and should not be used. - * Use individual functions instead. - */ -export const core = { - invokePluginFunc, - executeOnMain, - downloadFile, - deleteFile, - appDataPath, - getUserSpace, -}; - /** * Functions exports */ export { - invokePluginFunc, executeOnMain, downloadFile, - deleteFile, + abortDownload, appDataPath, getUserSpace, + openFileExplorer, }; diff --git a/core/src/fs.ts b/core/src/fs.ts index 2c94a2ce82..3e65518017 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -5,8 +5,7 @@ * @returns {Promise} A Promise that resolves when the file is written successfully. */ const writeFile: (path: string, data: string) => Promise = (path, data) => - window.coreAPI?.writeFile(path, data) ?? - window.electronAPI?.writeFile(path, data); + window.coreAPI?.writeFile(path, data); /** * Checks whether the path is a directory. @@ -14,7 +13,7 @@ const writeFile: (path: string, data: string) => Promise = (path, data) => * @returns {boolean} A boolean indicating whether the path is a directory. */ const isDirectory = (path: string): Promise => - window.coreAPI?.isDirectory(path) ?? window.electronAPI?.isDirectory(path); + window.coreAPI?.isDirectory(path); /** * Reads the contents of a file at the specified path. @@ -22,7 +21,7 @@ const isDirectory = (path: string): Promise => * @returns {Promise} A Promise that resolves with the contents of the file. */ const readFile: (path: string) => Promise = (path) => - window.coreAPI?.readFile(path) ?? window.electronAPI?.readFile(path); + window.coreAPI?.readFile(path); /** * List the directory files @@ -30,7 +29,7 @@ const readFile: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves with the contents of the directory. */ const listFiles: (path: string) => Promise = (path) => - window.coreAPI?.listFiles(path) ?? window.electronAPI?.listFiles(path); + window.coreAPI?.listFiles(path); /** * Creates a directory at the specified path. @@ -38,7 +37,7 @@ const listFiles: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves when the directory is created successfully. */ const mkdir: (path: string) => Promise = (path) => - window.coreAPI?.mkdir(path) ?? window.electronAPI?.mkdir(path); + window.coreAPI?.mkdir(path); /** * Removes a directory at the specified path. @@ -46,14 +45,30 @@ const mkdir: (path: string) => Promise = (path) => * @returns {Promise} A Promise that resolves when the directory is removed successfully. */ const rmdir: (path: string) => Promise = (path) => - window.coreAPI?.rmdir(path) ?? window.electronAPI?.rmdir(path); + window.coreAPI?.rmdir(path); /** * Deletes a file from the local file system. * @param {string} path - The path of the file to delete. * @returns {Promise} A Promise that resolves when the file is deleted. */ const deleteFile: (path: string) => Promise = (path) => - window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path); + window.coreAPI?.deleteFile(path); + +/** + * Appends data to a file at the specified path. + * @param path path to the file + * @param data data to append + */ +const appendFile: (path: string, data: string) => Promise = (path, data) => + window.coreAPI?.appendFile(path, data); + +/** + * Reads a file line by line. + * @param {string} path - The path of the file to read. + * @returns {Promise} A promise that resolves to the lines of the file. + */ +const readLineByLine: (path: string) => Promise = (path) => + window.coreAPI?.readLineByLine(path); export const fs = { isDirectory, @@ -63,4 +78,6 @@ export const fs = { mkdir, rmdir, deleteFile, + appendFile, + readLineByLine, }; diff --git a/core/src/index.ts b/core/src/index.ts index 9e3954c4bf..8d398f8b5f 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,34 +1,26 @@ -/** - * @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, appDataPath, getUserSpace } from "./core"; +export * from "./core"; /** - * Events module exports. + * Events events exports. * @module */ -export { events } from "./events"; +export * from "./events"; /** * Events types exports. * @module */ -export * from "./events"; - export * from "./types/index"; /** * Filesystem module exports. * @module */ -export { fs } from "./fs"; +export * from "./fs"; /** * Plugin base module export. diff --git a/core/src/plugin.ts b/core/src/plugin.ts index 96bfcbe94f..046c8bf5eb 100644 --- a/core/src/plugin.ts +++ b/core/src/plugin.ts @@ -4,6 +4,7 @@ export enum PluginType { Preference = "preference", SystemMonitoring = "systemMonitoring", Model = "model", + Assistant = "assistant", } export abstract class JanPlugin { diff --git a/core/src/plugins/assistant.ts b/core/src/plugins/assistant.ts new file mode 100644 index 0000000000..296c6c73af --- /dev/null +++ b/core/src/plugins/assistant.ts @@ -0,0 +1,28 @@ +import { Assistant } from "../index"; +import { JanPlugin } from "../plugin"; + +/** + * Abstract class for assistant plugins. + * @extends JanPlugin + */ +export abstract class AssistantPlugin extends JanPlugin { + /** + * Creates a new assistant. + * @param {Assistant} assistant - The assistant object to be created. + * @returns {Promise} A promise that resolves when the assistant has been created. + */ + abstract createAssistant(assistant: Assistant): Promise; + + /** + * Deletes an existing assistant. + * @param {Assistant} assistant - The assistant object to be deleted. + * @returns {Promise} A promise that resolves when the assistant has been deleted. + */ + abstract deleteAssistant(assistant: Assistant): Promise; + + /** + * Retrieves all existing assistants. + * @returns {Promise} A promise that resolves to an array of all assistants. + */ + abstract getAssistants(): Promise; +} diff --git a/core/src/plugins/conversational.ts b/core/src/plugins/conversational.ts index ebeb773331..dc87fdf9b4 100644 --- a/core/src/plugins/conversational.ts +++ b/core/src/plugins/conversational.ts @@ -1,32 +1,57 @@ -import { Thread } from "../index"; +import { Thread, ThreadMessage } from "../index"; import { JanPlugin } from "../plugin"; /** - * Abstract class for conversational plugins. + * Abstract class for Thread plugins. * @abstract * @extends JanPlugin */ export abstract class ConversationalPlugin extends JanPlugin { /** - * Returns a list of conversations. + * Returns a list of thread. * @abstract - * @returns {Promise} A promise that resolves to an array of conversations. + * @returns {Promise} A promise that resolves to an array of threads. */ - abstract getConversations(): Promise; + abstract getThreads(): Promise; /** - * Saves a conversation. + * Saves a thread. * @abstract - * @param {Thread} conversation - The conversation to save. - * @returns {Promise} A promise that resolves when the conversation is saved. + * @param {Thread} thread - The thread to save. + * @returns {Promise} A promise that resolves when the thread is saved. */ - abstract saveConversation(conversation: Thread): Promise; + abstract saveThread(thread: Thread): Promise; /** - * Deletes a conversation. + * Deletes a thread. * @abstract - * @param {string} conversationId - The ID of the conversation to delete. - * @returns {Promise} A promise that resolves when the conversation is deleted. + * @param {string} threadId - The ID of the thread to delete. + * @returns {Promise} A promise that resolves when the thread is deleted. */ - abstract deleteConversation(conversationId: string): Promise; + abstract deleteThread(threadId: string): Promise; + + /** + * Adds a new message to the thread. + * @param {ThreadMessage} message - The message to be added. + * @returns {Promise} A promise that resolves when the message has been added. + */ + abstract addNewMessage(message: ThreadMessage): Promise; + + /** + * Writes an array of messages to a specific thread. + * @param {string} threadId - The ID of the thread to write the messages to. + * @param {ThreadMessage[]} messages - The array of messages to be written. + * @returns {Promise} A promise that resolves when the messages have been written. + */ + abstract writeMessages( + threadId: string, + messages: ThreadMessage[] + ): Promise; + + /** + * Retrieves all messages from a specific thread. + * @param {string} threadId - The ID of the thread to retrieve the messages from. + * @returns {Promise} A promise that resolves to an array of messages from the thread. + */ + abstract getAllMessages(threadId: string): Promise; } diff --git a/core/src/plugins/index.ts b/core/src/plugins/index.ts index 5072819d78..4ca712db3a 100644 --- a/core/src/plugins/index.ts +++ b/core/src/plugins/index.ts @@ -14,7 +14,12 @@ export { InferencePlugin } from "./inference"; */ export { MonitoringPlugin } from "./monitoring"; +/** + * Assistant plugin for managing assistants. + */ +export { AssistantPlugin } from "./assistant"; + /** * Model plugin for managing models. */ -export { ModelPlugin } from "./model"; +export { ModelPlugin } from "./model"; \ No newline at end of file diff --git a/core/src/plugins/inference.ts b/core/src/plugins/inference.ts index 6fc93ed37e..8cbf2717e1 100644 --- a/core/src/plugins/inference.ts +++ b/core/src/plugins/inference.ts @@ -1,4 +1,4 @@ -import { MessageRequest, ThreadMessage } from "../index"; +import { MessageRequest, ModelSettingParams, ThreadMessage } from "../index"; import { JanPlugin } from "../plugin"; /** @@ -7,9 +7,9 @@ import { JanPlugin } from "../plugin"; export abstract class InferencePlugin extends JanPlugin { /** * Initializes the model for the plugin. - * @param modelFileName - The name of the file containing the model. + * @param modelId - The ID of the model to initialize. */ - abstract initModel(modelFileName: string): Promise; + abstract initModel(modelId: string, settings?: ModelSettingParams): Promise; /** * Stops the model for the plugin. diff --git a/core/src/plugins/model.ts b/core/src/plugins/model.ts index 03947d648d..53d3d45651 100644 --- a/core/src/plugins/model.ts +++ b/core/src/plugins/model.ts @@ -16,12 +16,19 @@ export abstract class ModelPlugin extends JanPlugin { */ abstract downloadModel(model: Model): Promise; + /** + * Cancels the download of a specific model. + * @param {string} modelId - The ID of the model to cancel the download for. + * @returns {Promise} A promise that resolves when the download has been cancelled. + */ + abstract cancelModelDownload(modelId: string): Promise; + /** * Deletes a model. - * @param filePath - The file path of the model to delete. + * @param modelId - The ID of the model to delete. * @returns A Promise that resolves when the model has been deleted. */ - abstract deleteModel(filePath: string): Promise; + abstract deleteModel(modelId: string): Promise; /** * Saves a model. diff --git a/core/src/types/index.ts b/core/src/types/index.ts index dd227081a7..15e83772f9 100644 --- a/core/src/types/index.ts +++ b/core/src/types/index.ts @@ -5,7 +5,6 @@ /** * The role of the author of this message. - * @data_transfer_object */ export enum ChatCompletionRole { System = "system", @@ -30,10 +29,20 @@ export type ChatCompletionMessage = { */ export type MessageRequest = { id?: string; + /** The thread id of the message request. **/ - threadId?: string; + threadId: string; + + /** + * The assistant id of the message request. + */ + assistantId?: string; + /** Messages for constructing a chat completion request **/ messages?: ChatCompletionMessage[]; + + /** Runtime parameters for constructing a chat completion request **/ + parameters?: ModelRuntimeParam; }; /** @@ -57,17 +66,50 @@ export enum MessageStatus { */ export type ThreadMessage = { /** Unique identifier for the message, generated by default using the ULID method. **/ - id?: string; + id: string; + /** Object name **/ + object: string; /** Thread id, default is a ulid. **/ - threadId?: string; + thread_id: string; /** The role of the author of this message. **/ - role?: ChatCompletionRole; + assistant_id?: string; + // TODO: comment + role: ChatCompletionRole; /** The content of this message. **/ - content?: string; + content: ThreadContent[]; /** The status of this message. **/ status: MessageStatus; - /** The timestamp indicating when this message was created, represented in ISO 8601 format. **/ - createdAt?: string; + /** The timestamp indicating when this message was created. Represented in Unix time. **/ + created: number; + /** The timestamp indicating when this message was updated. Represented in Unix time. **/ + updated: number; + /** The additional metadata of this message. **/ + metadata?: Record; +}; + +/** + * The content type of the message. + */ +export enum ContentType { + Text = "text", + Image = "image", +} +/** + * The `ThreadContent` type defines the shape of a message's content object + * @data_transfer_object + */ +export type ThreadContent = { + type: ContentType; + text: ContentValue; +}; + +/** + * The `ContentValue` type defines the shape of a content value object + * @data_transfer_object + */ +export type ContentValue = { + value: string; + annotations: string[]; }; /** @@ -77,60 +119,166 @@ export type ThreadMessage = { export interface Thread { /** Unique identifier for the thread, generated by default using the ULID method. **/ id: string; - /** The summary of this thread. **/ - summary?: string; - /** The messages of this thread. **/ - messages: ThreadMessage[]; + /** Object name **/ + object: string; + /** The title of this thread. **/ + title: string; + /** Assistants in this thread. **/ + assistants: ThreadAssistantInfo[]; + // if the thread has been init will full assistant info + isFinishInit: boolean; /** The timestamp indicating when this thread was created, represented in ISO 8601 format. **/ - createdAt?: string; + created: number; /** The timestamp indicating when this thread was updated, represented in ISO 8601 format. **/ - updatedAt?: string; - - /** - * @deprecated This field is deprecated and should not be used. - * Read from model file instead. - */ - modelId?: string; + updated: number; + /** The additional metadata of this thread. **/ + metadata?: Record; } +/** + * Represents the information about an assistant in a thread. + * @stored + */ +export type ThreadAssistantInfo = { + assistant_id: string; + assistant_name: string; + model: ModelInfo; +}; + +/** + * Represents the information about a model. + * @stored + */ +export type ModelInfo = { + id: string; + settings: ModelSettingParams; + parameters: ModelRuntimeParam; +}; + +/** + * Represents the state of a thread. + * @stored + */ +export type ThreadState = { + hasMore: boolean; + waitingForResponse: boolean; + error?: Error; + lastMessage?: string; +}; + /** * Model type defines the shape of a model object. * @stored */ export interface Model { - /** Combination of owner and model name.*/ + /** + * The type of the object. + * Default: "model" + */ + object: string; + + /** + * The version of the model. + */ + version: string; + + /** + * The model download source. It can be an external url or a local filepath. + */ + source_url: string; + + /** + * The model identifier, which can be referenced in the API endpoints. + */ id: string; - /** The name of the model.*/ + + /** + * Human-readable name that is used for UI. + */ name: string; - /** Quantization method name.*/ - quantizationName: string; - /** The the number of bits represents a number.*/ - bits: number; - /** The size of the model file in bytes.*/ + + /** + * The organization that owns the model (you!) + * Default: "you" + */ + owned_by: string; + + /** + * The Unix timestamp (in seconds) for when the model was created + */ + created: number; + + /** + * Default: "A cool model from Huggingface" + */ + description: string; + + /** + * The model state. + * Default: "to_download" + * Enum: "to_download" "downloading" "ready" "running" + */ + state: ModelState; + + /** + * The model settings. + */ + settings: ModelSettingParams; + + /** + * The model runtime parameters. + */ + parameters: ModelRuntimeParam; + + /** + * Metadata of the model. + */ + metadata: ModelMetadata; +} + +/** + * The Model transition states. + */ +export enum ModelState { + ToDownload = "to_download", + Downloading = "downloading", + Ready = "ready", + Running = "running", +} + +/** + * The available model settings. + */ +export type ModelSettingParams = { + ctx_len: number; + ngl: number; + embedding: boolean; + n_parallel: number; +}; + +/** + * The available model runtime parameters. + */ +export type ModelRuntimeParam = { + temperature: number; + token_limit: number; + top_k: number; + top_p: number; + stream: boolean; +}; + +/** + * The metadata of the model. + */ +export type ModelMetadata = { + engine: string; + quantization: string; size: number; - /** The maximum RAM required to run the model in bytes.*/ + binaries: string[]; maxRamRequired: number; - /** The use case of the model.*/ - usecase: string; - /** The download link of the model.*/ - downloadLink: string; - /** The short description of the model.*/ - shortDescription: string; - /** The long description of the model.*/ - longDescription: string; - /** The avatar url of the model.*/ - avatarUrl: string; - /** The author name of the model.*/ author: string; - /** The version of the model.*/ - version: string; - /** The origin url of the model repo.*/ - modelUrl: string; - /** The timestamp indicating when this model was released.*/ - releaseDate: number; - /** The tags attached to the model description */ - tags: string[]; -} + avatarUrl: string; +}; /** * Model type of the presentation object which will be presented to the user @@ -157,27 +305,37 @@ export interface ModelCatalog { releaseDate: number; /** The tags attached to the model description **/ tags: string[]; - /** The available versions of this model to download. */ - availableVersions: ModelVersion[]; + availableVersions: Model[]; } + /** - * Model type which will be present a version of ModelCatalog - * @data_transfer_object + * Assistant type defines the shape of an assistant object. + * @stored */ -export type ModelVersion = { - /** The name of this model version.*/ +export type Assistant = { + /** Represents the avatar of the user. */ + avatar: string; + /** Represents the location of the thread. */ + thread_location: string | undefined; + /** Represents the unique identifier of the object. */ + id: string; + /** Represents the object. */ + object: string; + /** Represents the creation timestamp of the object. */ + created_at: number; + /** Represents the name of the object. */ name: string; - /** The quantization method name.*/ - quantizationName: string; - /** The the number of bits represents a number.*/ - bits: number; - /** The size of the model file in bytes.*/ - size: number; - /** The maximum RAM required to run the model in bytes.*/ - maxRamRequired: number; - /** The use case of the model.*/ - usecase: string; - /** The download link of the model.*/ - downloadLink: string; + /** Represents the description of the object. */ + description: string; + /** Represents the model of the object. */ + model: string; + /** Represents the instructions for the object. */ + instructions: string; + /** Represents the tools associated with the object. */ + tools: any; + /** Represents the file identifiers associated with the object. */ + file_ids: string[]; + /** Represents the metadata of the object. */ + metadata?: Record; }; diff --git a/docs/docs/specs/architecture.md b/docs/docs/specs/architecture.md index 0e3a1407c6..39b7fa833a 100644 --- a/docs/docs/specs/architecture.md +++ b/docs/docs/specs/architecture.md @@ -1,5 +1,6 @@ --- title: Architecture +slug: /specs --- :::warning @@ -10,35 +11,28 @@ This page is still under construction, and should be read as a scratchpad ## Overview -- Jan built a modular infrastructure on top of Electron, in order to support extensions and AI functionality. -- Jan is largely built on top of its own modules. +- Jan has a modular architecture and is largely built on top of its own modules. - Jan uses a local [file-based approach](/specs/file-based) for data persistence. +- Jan currently supports an Electron-based [Desktop UI](https://github.com/janhq/jan) and a C++ inference engine called [Nitro](https://nitro.jan.ai/docs/). -## Modules - -Modules are low level, system services. It is similar to OS kernel modules, in that `modules` provide abstractions to device level, basic functionality like the filesystem, device system, databases, AI inference engines, etc. - -## Pluggable Modules +## Extensions -Jan exports modules that mirror OpenAI’s, exposing similar APIs and objects: +Jan has an Extensions API inspired by VSCode. In fact, most of Jan's core services are built as extensions. -- Modules are modular, atomic implementations of a single OpenAI-compatible endpoint -- Modules can be swapped out for alternate implementations - - The default `messages` module persists messages in thread-specific `.json` - - `messages-postgresql` uses Postgres for production-grade cloud-native environments +Jan supports the following OpenAI compatible extensions: -| Jan Module | Description | API Docs | -| ---------- | ------------- | ---------------------------- | -| Chat | Inference | [/chat](/api/chat) | -| Models | Models | [/model](/api/model) | -| Assistants | Apps | [/assistant](/api/assistant) | -| Threads | Conversations | [/thread](/api/thread) | -| Messages | Messages | [/message](/api/message) | +| Jan Module | Description | API Docs | +| ---------- | ------------- | --------------------------------------------- | +| Chat | Inference | [/chats](/api-reference/#tag/Chat-Completion) | +| Models | Models | [/models](/api-reference/#tag/Models) | +| Assistants | Apps | [/assistants](/api-reference/#tag/Assistants) | +| Threads | Conversations | [/threads](/api-reference/#tag/Threads) | +| Messages | Messages | [/messages](/api-reference/#tag/Messages) | -## Extensions +## Modules -Extensions are feature level services that include both UI and logic implementation. +Modules are low level, system services. It is similar to OS kernel modules. Modules provide abstractions to basic, device level functionality like working with the filesystem, device system, databases, AI inference engines, etc. - +Jan follows the [dependency inversion principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle) such that `modules` expose the interfaces that `extensions` can then implement. diff --git a/docs/docs/specs/assistants.md b/docs/docs/specs/engineering/assistants.md similarity index 94% rename from docs/docs/specs/assistants.md rename to docs/docs/specs/engineering/assistants.md index d584b66b08..ea0ec09551 100644 --- a/docs/docs/specs/assistants.md +++ b/docs/docs/specs/engineering/assistants.md @@ -1,5 +1,6 @@ --- title: "Assistants" +slug: /specs/assistants --- :::caution @@ -14,7 +15,7 @@ In Jan, assistants are `primary` entities with the following capabilities: - Assistants can use `models`, `tools`, handle and emit `events`, and invoke `custom code`. - Users can create custom assistants with saved `model` settings and parameters. -- An [OpenAI Assistants API](https://platform.openai.com/docs/api-reference/assistants) compatible endpoint at `localhost:3000/v1/assistants`. +- An [OpenAI Assistants API](https://platform.openai.com/docs/api-reference/assistants) compatible endpoint at `localhost:1337/v1/assistants`. - Jan ships with a default assistant called "Jan" that lets you use all models. ## Folder Structure @@ -49,6 +50,7 @@ In Jan, assistants are `primary` entities with the following capabilities: "models": [ // Defaults to "*" all models { ...model_0 } ], + "instructions": "Be concise", // A system prompt for the assistant "events": [], // Defaults to "*" "metadata": {}, // Defaults to {} // "tools": [], // Coming soon diff --git a/docs/docs/specs/chats.md b/docs/docs/specs/engineering/chats.md similarity index 92% rename from docs/docs/specs/chats.md rename to docs/docs/specs/engineering/chats.md index c119cc45dc..7bb96faf00 100644 --- a/docs/docs/specs/chats.md +++ b/docs/docs/specs/engineering/chats.md @@ -1,5 +1,6 @@ --- title: Chats +slug: /specs/chats --- :::caution @@ -13,7 +14,7 @@ This is currently under development. In Jan, `chats` are LLM responses in the form of OpenAI compatible `chat completion objects`. - Models take a list of messages and return a model-generated response as output. -- An [OpenAI Chat API](https://platform.openai.com/docs/api-reference/chat) compatible endpoint at `localhost:3000/v1/chats`. +- An [OpenAI Chat API](https://platform.openai.com/docs/api-reference/chat) compatible endpoint at `localhost:1337/v1/chats`. ## Folder Structure diff --git a/docs/docs/specs/files.md b/docs/docs/specs/engineering/files.md similarity index 98% rename from docs/docs/specs/files.md rename to docs/docs/specs/engineering/files.md index 1aea59cfab..0becbf6d6a 100644 --- a/docs/docs/specs/files.md +++ b/docs/docs/specs/engineering/files.md @@ -1,5 +1,6 @@ --- title: "Files" +slug: /specs/files --- :::warning diff --git a/docs/docs/specs/engineering/fine-tuning.md b/docs/docs/specs/engineering/fine-tuning.md new file mode 100644 index 0000000000..f2d4153d29 --- /dev/null +++ b/docs/docs/specs/engineering/fine-tuning.md @@ -0,0 +1,6 @@ +--- +title: "Fine-tuning" +slug: /specs/finetuning +--- + +Todo: @hiro diff --git a/docs/docs/specs/messages.md b/docs/docs/specs/engineering/messages.md similarity index 99% rename from docs/docs/specs/messages.md rename to docs/docs/specs/engineering/messages.md index ba3158cdf7..62a721fa82 100644 --- a/docs/docs/specs/messages.md +++ b/docs/docs/specs/engineering/messages.md @@ -1,5 +1,6 @@ --- title: Messages +slug: /specs/messages --- :::caution diff --git a/docs/docs/specs/models.md b/docs/docs/specs/engineering/models.md similarity index 82% rename from docs/docs/specs/models.md rename to docs/docs/specs/engineering/models.md index f9ea267246..c47a62bab8 100644 --- a/docs/docs/specs/models.md +++ b/docs/docs/specs/engineering/models.md @@ -1,5 +1,6 @@ --- title: Models +slug: /specs/models --- :::caution @@ -13,7 +14,7 @@ This is currently under development. In Jan, models are primary entities with the following capabilities: - Users can import, configure, and run models locally. -- An [OpenAI Model API](https://platform.openai.com/docs/api-reference/models) compatible endpoint at `localhost:3000/v1/models`. +- An [OpenAI Model API](https://platform.openai.com/docs/api-reference/models) compatible endpoint at `localhost:1337/v1/models`. - Supported model formats: `ggufv3`, and more. ## Folder Structure @@ -64,17 +65,19 @@ Here's a standard example `model.json` for a GGUF model. "state": enum[null, "downloading", "ready", "starting", "stopping", ...] "format": "ggufv3", // Defaults to "ggufv3" "settings": { // Models are initialized with settings - "ctx_len": "2048", - "ngl": "100", - "embedding": "true", - "n_parallel": "4", + "ctx_len": 2048, + "ngl": 100, + "embedding": true, + "n_parallel": 4, }, "parameters": { // Models are called parameters - "temperature": "0.7", - "token_limit": "2048", - "top_k": "0", - "top_p": "1", - "stream": "true" + "stream": true, + "max_tokens": 2048, + "stop": [""], // This usually can be left blank, only used with specific need from model author + "frequency_penalty": 0, + "presence_penalty": 0, + "temperature": 0.7, + "top_p": 0.95 }, "metadata": {}, // Defaults to {} "assets": [ // Defaults to current dir @@ -82,6 +85,10 @@ Here's a standard example `model.json` for a GGUF model. ] ``` +The model settings in the example can be found at: [Nitro's model settings](https://nitro.jan.ai/features/load-unload#table-of-parameters) + +The model parameters in the example can be found at: [Nitro's model parameters](https://nitro.jan.ai/api-reference#tag/Chat-Completion) + ## API Reference Jan's Model API is compatible with [OpenAI's Models API](https://platform.openai.com/docs/api-reference/models), with additional methods for managing and running models locally. diff --git a/docs/docs/specs/prompts.md b/docs/docs/specs/engineering/prompts.md similarity index 53% rename from docs/docs/specs/prompts.md rename to docs/docs/specs/engineering/prompts.md index 2ec008d8a6..37422b5172 100644 --- a/docs/docs/specs/prompts.md +++ b/docs/docs/specs/engineering/prompts.md @@ -1,7 +1,8 @@ --- title: Prompts +slug: /specs/prompts --- - [ ] /prompts folder - [ ] How to add to prompts -- [ ] Assistants can have suggested Prompts \ No newline at end of file +- [ ] Assistants can have suggested Prompts diff --git a/docs/docs/specs/threads.md b/docs/docs/specs/engineering/threads.md similarity index 99% rename from docs/docs/specs/threads.md rename to docs/docs/specs/engineering/threads.md index b75afd84be..982c4f8cbe 100644 --- a/docs/docs/specs/threads.md +++ b/docs/docs/specs/engineering/threads.md @@ -1,5 +1,6 @@ --- title: Threads +slug: /specs/threads --- :::caution diff --git a/docs/docs/specs/fine-tuning.md b/docs/docs/specs/fine-tuning.md deleted file mode 100644 index 281a065b20..0000000000 --- a/docs/docs/specs/fine-tuning.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Fine-tuning" ---- -Todo: @hiro \ No newline at end of file diff --git a/docs/docs/specs/home.md b/docs/docs/specs/home.md deleted file mode 100644 index 6e6ba714a4..0000000000 --- a/docs/docs/specs/home.md +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Home ---- \ No newline at end of file diff --git a/docs/docs/specs/hub.md b/docs/docs/specs/hub.md deleted file mode 100644 index 16a7d639d3..0000000000 --- a/docs/docs/specs/hub.md +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: Hub ---- \ No newline at end of file diff --git a/docs/docs/specs/img/chat-screen.png b/docs/docs/specs/img/chat-screen.png new file mode 100644 index 0000000000..53416c43ba Binary files /dev/null and b/docs/docs/specs/img/chat-screen.png differ diff --git a/docs/docs/specs/img/hub-screen.png b/docs/docs/specs/img/hub-screen.png new file mode 100644 index 0000000000..66cb90e43e Binary files /dev/null and b/docs/docs/specs/img/hub-screen.png differ diff --git a/docs/docs/specs/img/settings-screen.png b/docs/docs/specs/img/settings-screen.png new file mode 100644 index 0000000000..41080744e8 Binary files /dev/null and b/docs/docs/specs/img/settings-screen.png differ diff --git a/docs/docs/specs/img/system-screen.png b/docs/docs/specs/img/system-screen.png new file mode 100644 index 0000000000..eeca16d6ad Binary files /dev/null and b/docs/docs/specs/img/system-screen.png differ diff --git a/docs/docs/specs/product/chat.md b/docs/docs/specs/product/chat.md new file mode 100644 index 0000000000..28969f3486 --- /dev/null +++ b/docs/docs/specs/product/chat.md @@ -0,0 +1,17 @@ +--- +title: Chat +slug: /specs/chat +--- + +## Overview + +A home screen for users to chat with [assistants](/specs/assistants) via conversation [threads](/specs/threads). + +![alt text](../img/chat-screen.png) + +## User Stories + + + +- Users can chat with `Jan` the default assistant +- Users can customize chat settings like model parameters via both the GUI & `thread.json` diff --git a/docs/docs/specs/product/hub.md b/docs/docs/specs/product/hub.md new file mode 100644 index 0000000000..c2523b0fb9 --- /dev/null +++ b/docs/docs/specs/product/hub.md @@ -0,0 +1,18 @@ +--- +title: Hub +slug: /specs/hub +--- + +## Overview + +The Hub is like a store for everything, where users can discover and download models, assistants, and more. + +![alt text](../img/hub-screen.png) + +## User Stories + + + +- Users can discover recommended models (Jan ships with a few preconfigured `model.json` files) +- Users can download models suitable for their devices, e.g. compatible with their RAM +- Users can download models via a HuggingFace URL (coming soon) diff --git a/docs/docs/specs/product/settings.md b/docs/docs/specs/product/settings.md new file mode 100644 index 0000000000..a80c500345 --- /dev/null +++ b/docs/docs/specs/product/settings.md @@ -0,0 +1,32 @@ +--- +title: Settings +slug: /specs/settings +--- + +## Overview + +A settings page for users to add extensions, configure model settings, change app appearance, add keyboard shortcuts, and a plethora of other personalizations. + +![alt text](../img/settings-screen.png) + +## User Stories + + + +### General Settings + +- Users can customize `port` number +- Users can customize `janroot` folder location + +### Extensions Settings + +- Users can add, delete, and configure extensions + +### Model Settings + +- Users can configure default model parameters and settings +- Users can delete models + +### Appearance + +- Users can set color themes and dark/light modes diff --git a/docs/docs/specs/product/system-monitor.md b/docs/docs/specs/product/system-monitor.md new file mode 100644 index 0000000000..52d11a2720 --- /dev/null +++ b/docs/docs/specs/product/system-monitor.md @@ -0,0 +1,17 @@ +--- +title: System Monitor +slug: /specs/system-monitor +--- + +## Overview + +An activity screen to monitor system health and running models. + +![alt text](../img/system-screen.png) + +## User Stories + + + +- Users can see disk and ram utilization +- Users can start and stop models based on system health diff --git a/docs/docs/specs/settings.md b/docs/docs/specs/settings.md deleted file mode 100644 index a8cf8809cc..0000000000 --- a/docs/docs/specs/settings.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Settings ---- - -- [ ] .jan folder in jan root \ No newline at end of file diff --git a/docs/docs/specs/system-monitor.md b/docs/docs/specs/system-monitor.md deleted file mode 100644 index cf4a2b22f2..0000000000 --- a/docs/docs/specs/system-monitor.md +++ /dev/null @@ -1,3 +0,0 @@ ---- -title: System Monitor ---- \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index 7b3ccf078d..edef458cd7 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -66,10 +66,10 @@ const sidebars = { collapsible: true, collapsed: false, items: [ - "specs/home", - "specs/hub", - "specs/system-monitor", - "specs/settings", + "specs/product/chat", + "specs/product/hub", + "specs/product/system-monitor", + "specs/product/settings", ], }, @@ -79,16 +79,16 @@ const sidebars = { collapsible: true, collapsed: false, items: [ - "specs/chats", - "specs/models", - "specs/threads", - "specs/messages", - "specs/assistants", - // "specs/files", - // "specs/jan", - // "specs/fine-tuning", - // "specs/settings", - // "specs/prompts", + "specs/engineering/chats", + "specs/engineering/models", + "specs/engineering/threads", + "specs/engineering/messages", + "specs/engineering/assistants", + // "specs/engineering/files", + // "specs/engineering/jan", + // "specs/engineering/fine-tuning", + // "specs/engineering/settings", + // "specs/engineering/prompts", ], }, ], diff --git a/electron/handlers/app.ts b/electron/handlers/app.ts index 022e4d61a9..3738a79708 100644 --- a/electron/handlers/app.ts +++ b/electron/handlers/app.ts @@ -29,7 +29,8 @@ export function handleAppIPCs() { * @param _event - The IPC event object. */ ipcMain.handle("openAppDirectory", async (_event) => { - shell.openPath(app.getPath("userData")); + const userSpacePath = join(app.getPath('home'), 'jan') + shell.openPath(userSpacePath); }); /** diff --git a/electron/handlers/download.ts b/electron/handlers/download.ts index 3a1fc36d1e..316576e896 100644 --- a/electron/handlers/download.ts +++ b/electron/handlers/download.ts @@ -34,22 +34,7 @@ export function handleDownloaderIPCs() { ipcMain.handle('abortDownload', async (_event, fileName) => { const rq = DownloadManager.instance.networkRequests[fileName] DownloadManager.instance.networkRequests[fileName] = undefined - const userDataPath = app.getPath('userData') - const fullPath = join(userDataPath, fileName) rq?.abort() - let result = 'NULL' - unlink(fullPath, function (err) { - if (err && err.code == 'ENOENT') { - result = `File not exist: ${err}` - } else if (err) { - result = `File delete error: ${err}` - } else { - result = 'File deleted successfully' - } - console.debug( - `Delete file ${fileName} from ${fullPath} result: ${result}` - ) - }) }) /** diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index c1e8a85e4d..6ed67b2de6 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,6 +1,7 @@ import { app, ipcMain } from 'electron' import * as fs from 'fs' import { join } from 'path' +import readline from 'readline' /** * Handles file system operations. @@ -97,7 +98,7 @@ export function handleFsIPCs() { */ ipcMain.handle('rmdir', async (event, path: string): Promise => { return new Promise((resolve, reject) => { - fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => { + fs.rm(join(userSpacePath, path), { recursive: true }, (err) => { if (err) { reject(err) } else { @@ -153,4 +154,45 @@ export function handleFsIPCs() { return result }) + + /** + * Appends data to a file in the user data directory. + * @param event - The event object. + * @param path - The path of the file to append to. + * @param data - The data to append to the file. + * @returns A promise that resolves when the file has been written. + */ + ipcMain.handle('appendFile', async (_event, path: string, data: string) => { + return new Promise((resolve, reject) => { + fs.appendFile(join(userSpacePath, path), data, 'utf8', (err) => { + if (err) { + reject(err) + } else { + resolve(data) + } + }) + }) + }) + + ipcMain.handle('readLineByLine', async (_event, path: string) => { + const fullPath = join(userSpacePath, path) + + return new Promise((res, rej) => { + try { + const readInterface = readline.createInterface({ + input: fs.createReadStream(fullPath), + }) + const lines: any = [] + readInterface + .on('line', function (line) { + lines.push(line) + }) + .on('close', function () { + res(lines) + }) + } catch (err) { + rej(err) + } + }) + }) } diff --git a/electron/preload.ts b/electron/preload.ts index dfba13bd12..a72d6a5cb0 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -38,6 +38,7 @@ * @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} appendFile - Appends the given data to the file 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. @@ -58,7 +59,7 @@ import { useFacade } from './core/plugin/facade' useFacade() -const { contextBridge, ipcRenderer } = require('electron') +const { contextBridge, ipcRenderer, shell } = require('electron') contextBridge.exposeInMainWorld('electronAPI', { invokePluginFunc: (plugin: any, method: any, ...args: any[]) => @@ -88,7 +89,8 @@ contextBridge.exposeInMainWorld('electronAPI', { deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath), - isDirectory: (filePath: string) => ipcRenderer.invoke('isDirectory', filePath), + isDirectory: (filePath: string) => + ipcRenderer.invoke('isDirectory', filePath), getUserSpace: () => ipcRenderer.invoke('getUserSpace'), @@ -99,10 +101,17 @@ contextBridge.exposeInMainWorld('electronAPI', { listFiles: (path: string) => ipcRenderer.invoke('listFiles', path), + appendFile: (path: string, data: string) => + ipcRenderer.invoke('appendFile', path, data), + + readLineByLine: (path: string) => ipcRenderer.invoke('readLineByLine', path), + mkdir: (path: string) => ipcRenderer.invoke('mkdir', path), rmdir: (path: string) => ipcRenderer.invoke('rmdir', path), + openFileExplorer: (path: string) => shell.openPath(path), + installRemotePlugin: (pluginName: string) => ipcRenderer.invoke('installRemotePlugin', pluginName), diff --git a/package.json b/package.json index 036542c38b..cb670a096d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "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:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\"", + "build:plugins": "rimraf ./electron/core/pre-install/*.tgz && concurrently --kill-others-on-fail \"cd ./plugins/conversational-json && npm install && npm run build:publish\" \"cd ./plugins/inference-plugin && npm install && npm run build:publish\" \"cd ./plugins/model-plugin && npm install && npm run build:publish\" \"cd ./plugins/monitoring-plugin && npm install && npm run build:publish\" \"cd ./plugins/assistant-plugin && npm install && npm run build:publish\"", "build:test": "yarn build:web && yarn workspace jan build:test", "build": "yarn build:web && yarn workspace jan build", "build:publish": "yarn build:web && yarn workspace jan build:publish" diff --git a/plugins/assistant-plugin/README.md b/plugins/assistant-plugin/README.md new file mode 100644 index 0000000000..16cde13924 --- /dev/null +++ b/plugins/assistant-plugin/README.md @@ -0,0 +1,77 @@ +# Jan Assistant plugin + +Created using Jan app example + +# 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`. + + ```typescript + import { core } from "@janhq/core"; + + function onStart(): Promise { + 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! diff --git a/plugins/assistant-plugin/package.json b/plugins/assistant-plugin/package.json new file mode 100644 index 0000000000..1a850beb6d --- /dev/null +++ b/plugins/assistant-plugin/package.json @@ -0,0 +1,33 @@ +{ + "name": "@janhq/assistant-plugin", + "version": "1.0.9", + "description": "Assistant", + "icon": "https://raw.githubusercontent.com/tailwindlabs/heroicons/88e98b0c2b458553fbadccddc2d2f878edc0387b/src/20/solid/cpu-chip.svg", + "main": "dist/index.js", + "module": "dist/module.js", + "author": "Jan ", + "license": "AGPL-3.0", + "url": "/plugins/assistant-plugin/index.js", + "activationPoints": [ + "init" + ], + "scripts": { + "build": "tsc -b . && webpack --config webpack.config.js", + "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/core/pre-install" + }, + "devDependencies": { + "rimraf": "^3.0.2", + "webpack": "^5.88.2", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "@janhq/core": "file:../../core", + "path-browserify": "^1.0.1", + "ts-loader": "^9.5.0" + }, + "files": [ + "dist/*", + "package.json", + "README.md" + ] +} diff --git a/plugins/assistant-plugin/src/@types/global.d.ts b/plugins/assistant-plugin/src/@types/global.d.ts new file mode 100644 index 0000000000..3b45ccc5ad --- /dev/null +++ b/plugins/assistant-plugin/src/@types/global.d.ts @@ -0,0 +1 @@ +declare const MODULE: string; diff --git a/plugins/assistant-plugin/src/index.ts b/plugins/assistant-plugin/src/index.ts new file mode 100644 index 0000000000..a286b04bda --- /dev/null +++ b/plugins/assistant-plugin/src/index.ts @@ -0,0 +1,107 @@ +import { PluginType, fs, Assistant } from "@janhq/core"; +import { AssistantPlugin } from "@janhq/core/lib/plugins"; +import { join } from "path"; + +export default class JanAssistantPlugin implements AssistantPlugin { + private static readonly _homeDir = "assistants"; + + type(): PluginType { + return PluginType.Assistant; + } + + onLoad(): void { + // making the assistant directory + fs.mkdir(JanAssistantPlugin._homeDir).then(() => { + this.createJanAssistant(); + }); + } + + /** + * Called when the plugin is unloaded. + */ + onUnload(): void {} + + async createAssistant(assistant: Assistant): Promise { + // assuming that assistants/ directory is already created in the onLoad above + + // TODO: check if the directory already exists, then ignore creation for now + + const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id); + await fs.mkdir(assistantDir); + + // store the assistant metadata json + const assistantMetadataPath = join(assistantDir, "assistant.json"); + try { + await fs.writeFile( + assistantMetadataPath, + JSON.stringify(assistant, null, 2) + ); + } catch (err) { + console.error(err); + } + } + + async getAssistants(): Promise { + // get all the assistant directories + // get all the assistant metadata json + const results: Assistant[] = []; + const allFileName: string[] = await fs.listFiles( + JanAssistantPlugin._homeDir + ); + for (const fileName of allFileName) { + const filePath = join(JanAssistantPlugin._homeDir, fileName); + const isDirectory = await fs.isDirectory(filePath); + if (!isDirectory) { + // if not a directory, ignore + continue; + } + + const jsonFiles: string[] = (await fs.listFiles(filePath)).filter( + (file: string) => file === "assistant.json" + ); + + if (jsonFiles.length !== 1) { + // has more than one assistant file -> ignore + continue; + } + + const assistant: Assistant = JSON.parse( + await fs.readFile(join(filePath, jsonFiles[0])) + ); + + results.push(assistant); + } + + return results; + } + + async deleteAssistant(assistant: Assistant): Promise { + if (assistant.id === "jan") { + return Promise.reject("Cannot delete Jan Assistant"); + } + + // remove the directory + const assistantDir = join(JanAssistantPlugin._homeDir, assistant.id); + await fs.rmdir(assistantDir); + return Promise.resolve(); + } + + private async createJanAssistant(): Promise { + const janAssistant: Assistant = { + avatar: "", + thread_location: undefined, // TODO: make this property ? + id: "jan", + object: "assistant", // TODO: maybe we can set default value for this? + created_at: Date.now(), + name: "Jan Assistant", + description: "Just Jan Assistant", + model: "*", + instructions: "Your name is Jan.", + tools: undefined, + file_ids: [], + metadata: undefined, + }; + + await this.createAssistant(janAssistant); + } +} diff --git a/plugins/assistant-plugin/tsconfig.json b/plugins/assistant-plugin/tsconfig.json new file mode 100644 index 0000000000..2477d58ce5 --- /dev/null +++ b/plugins/assistant-plugin/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "ES6", + "moduleResolution": "node", + "outDir": "./dist", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": false, + "skipLibCheck": true, + "rootDir": "./src" + }, + "include": ["./src"] +} diff --git a/plugins/assistant-plugin/webpack.config.js b/plugins/assistant-plugin/webpack.config.js new file mode 100644 index 0000000000..74d16fc8e7 --- /dev/null +++ b/plugins/assistant-plugin/webpack.config.js @@ -0,0 +1,38 @@ +const path = require("path"); +const webpack = require("webpack"); +const packageJson = require("./package.json"); + +module.exports = { + experiments: { outputModule: true }, + entry: "./src/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/, + }, + ], + }, + output: { + filename: "index.js", // Adjust the output file name as needed + path: path.resolve(__dirname, "dist"), + library: { type: "module" }, // Specify ESM output format + }, + plugins: [ + new webpack.DefinePlugin({ + MODULE: JSON.stringify(`${packageJson.name}/${packageJson.module}`), + }), + ], + resolve: { + extensions: [".ts", ".js"], + fallback: { + path: require.resolve("path-browserify"), + }, + }, + optimization: { + minimize: false, + }, + // Add loaders and other configuration as needed for your project +}; diff --git a/plugins/conversational-json/src/index.ts b/plugins/conversational-json/src/index.ts index 94082bb453..500fcb5263 100644 --- a/plugins/conversational-json/src/index.ts +++ b/plugins/conversational-json/src/index.ts @@ -1,14 +1,16 @@ import { PluginType, fs } from '@janhq/core' import { ConversationalPlugin } from '@janhq/core/lib/plugins' -import { Thread } from '@janhq/core/lib/types' +import { Thread, ThreadMessage } from '@janhq/core/lib/types' import { join } from 'path' /** * JSONConversationalPlugin is a ConversationalPlugin implementation that provides - * functionality for managing conversations. + * functionality for managing threads. */ export default class JSONConversationalPlugin implements ConversationalPlugin { private static readonly _homeDir = 'threads' + private static readonly _threadInfoFileName = 'thread.json' + private static readonly _threadMessagesFileName = 'messages.jsonl' /** * Returns the type of the plugin. @@ -35,13 +37,11 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { /** * Returns a Promise that resolves to an array of Conversation objects. */ - async getConversations(): Promise { + async getThreads(): Promise { try { - const convoIds = await this.getConversationDocs() + const threadDirs = await this.getValidThreadDirs() - const promises = convoIds.map((conversationId) => { - return this.readConvo(conversationId) - }) + const promises = threadDirs.map((dirName) => this.readThread(dirName)) const promiseResults = await Promise.allSettled(promises) const convos = promiseResults .map((result) => { @@ -51,10 +51,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { }) .filter((convo) => convo != null) convos.sort( - (a, b) => - new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + (a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime() ) - console.debug('getConversations: ', JSON.stringify(convos, null, 2)) + console.debug('getThreads', JSON.stringify(convos, null, 2)) return convos } catch (error) { console.error(error) @@ -63,55 +62,145 @@ export default class JSONConversationalPlugin implements ConversationalPlugin { } /** - * Saves a Conversation object to a Markdown file. - * @param conversation The Conversation object to save. + * Saves a Thread object to a json file. + * @param thread The Thread object to save. */ - saveConversation(conversation: Thread): Promise { - return fs - .mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`) - .then(() => - fs.writeFile( - join( - JSONConversationalPlugin._homeDir, - conversation.id, - `${conversation.id}.json` - ), - JSON.stringify(conversation) - ) + async saveThread(thread: Thread): Promise { + try { + const threadDirPath = join(JSONConversationalPlugin._homeDir, thread.id) + const threadJsonPath = join( + threadDirPath, + JSONConversationalPlugin._threadInfoFileName ) + await fs.mkdir(threadDirPath) + await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2)) + Promise.resolve() + } catch (err) { + Promise.reject(err) + } } /** - * Deletes a conversation with the specified ID. - * @param conversationId The ID of the conversation to delete. + * Delete a thread with the specified ID. + * @param threadId The ID of the thread to delete. */ - deleteConversation(conversationId: string): Promise { - return fs.rmdir( - join(JSONConversationalPlugin._homeDir, `${conversationId}`) - ) + deleteThread(threadId: string): Promise { + return fs.rmdir(join(JSONConversationalPlugin._homeDir, `${threadId}`)) + } + + async addNewMessage(message: ThreadMessage): Promise { + try { + const threadDirPath = join( + JSONConversationalPlugin._homeDir, + message.thread_id + ) + const threadMessagePath = join( + threadDirPath, + JSONConversationalPlugin._threadMessagesFileName + ) + await fs.mkdir(threadDirPath) + await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n') + Promise.resolve() + } catch (err) { + Promise.reject(err) + } + } + + async writeMessages( + threadId: string, + messages: ThreadMessage[] + ): Promise { + try { + const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId) + const threadMessagePath = join( + threadDirPath, + JSONConversationalPlugin._threadMessagesFileName + ) + await fs.mkdir(threadDirPath) + await fs.writeFile( + threadMessagePath, + messages.map((msg) => JSON.stringify(msg)).join('\n') + ) + Promise.resolve() + } catch (err) { + Promise.reject(err) + } } /** - * A promise builder for reading a conversation from a file. - * @param convoId the conversation id we are reading from. - * @returns data of the conversation + * A promise builder for reading a thread from a file. + * @param threadDirName the thread dir we are reading from. + * @returns data of the thread */ - private async readConvo(convoId: string): Promise { + private async readThread(threadDirName: string): Promise { return fs.readFile( - join(JSONConversationalPlugin._homeDir, convoId, `${convoId}.json`) + join( + JSONConversationalPlugin._homeDir, + threadDirName, + JSONConversationalPlugin._threadInfoFileName + ) ) } /** - * 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. + * Returns a Promise that resolves to an array of thread directories. * @private */ - private async getConversationDocs(): Promise { - return fs - .listFiles(JSONConversationalPlugin._homeDir) - .then((files: string[]) => { - return Promise.all(files.filter((file) => file.startsWith('jan-'))) + private async getValidThreadDirs(): Promise { + const fileInsideThread: string[] = await fs.listFiles( + JSONConversationalPlugin._homeDir + ) + + const threadDirs: string[] = [] + for (let i = 0; i < fileInsideThread.length; i++) { + const path = join(JSONConversationalPlugin._homeDir, fileInsideThread[i]) + const isDirectory = await fs.isDirectory(path) + if (!isDirectory) { + console.debug(`Ignore ${path} because it is not a directory`) + continue + } + + const isHavingThreadInfo = (await fs.listFiles(path)).includes( + JSONConversationalPlugin._threadInfoFileName + ) + if (!isHavingThreadInfo) { + console.debug(`Ignore ${path} because it does not have thread info`) + continue + } + + threadDirs.push(fileInsideThread[i]) + } + return threadDirs + } + + async getAllMessages(threadId: string): Promise { + try { + const threadDirPath = join(JSONConversationalPlugin._homeDir, threadId) + const isDir = await fs.isDirectory(threadDirPath) + if (!isDir) { + throw Error(`${threadDirPath} is not directory`) + } + + const files: string[] = await fs.listFiles(threadDirPath) + if (!files.includes(JSONConversationalPlugin._threadMessagesFileName)) { + throw Error(`${threadDirPath} not contains message file`) + } + + const messageFilePath = join( + threadDirPath, + JSONConversationalPlugin._threadMessagesFileName + ) + + const result = await fs.readLineByLine(messageFilePath) + + const messages: ThreadMessage[] = [] + result.forEach((line: string) => { + messages.push(JSON.parse(line) as ThreadMessage) }) + return messages + } catch (err) { + console.error(err) + return [] + } } } diff --git a/plugins/inference-plugin/download.bat b/plugins/inference-plugin/download.bat new file mode 100644 index 0000000000..3dfe34218a --- /dev/null +++ b/plugins/inference-plugin/download.bat @@ -0,0 +1,4 @@ +@echo off +set /p NITRO_VERSION=<./nitro/version.txt +.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda +.\node_modules\.bin\download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu diff --git a/plugins/inference-plugin/nitro/version.txt b/plugins/inference-plugin/nitro/version.txt index a34eaa5d03..44a7df273e 100644 --- a/plugins/inference-plugin/nitro/version.txt +++ b/plugins/inference-plugin/nitro/version.txt @@ -1 +1 @@ -0.1.11 \ No newline at end of file +0.1.17 \ No newline at end of file diff --git a/plugins/inference-plugin/package.json b/plugins/inference-plugin/package.json index f19c864cd3..97d9fc7c09 100644 --- a/plugins/inference-plugin/package.json +++ b/plugins/inference-plugin/package.json @@ -13,9 +13,9 @@ ], "scripts": { "build": "tsc -b . && webpack --config webpack.config.js", - "downloadnitro:linux": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.zip -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.zip -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh", - "downloadnitro:darwin": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.zip -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.zip -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro", - "downloadnitro:win32": "download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64.zip -e --strip 1 -o ./nitro/win-cpu && download https://github.com/janhq/nitro/releases/download/v%NITRO_VERSION%/nitro-%NITRO_VERSION%-win-amd64-cuda.zip -e --strip 1 -o ./nitro/win-cuda", + "downloadnitro:linux": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64.tar.gz -e --strip 1 -o ./nitro/linux-cpu && chmod +x ./nitro/linux-cpu/nitro && chmod +x ./nitro/linux-start.sh && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-linux-amd64-cuda.tar.gz -e --strip 1 -o ./nitro/linux-cuda && chmod +x ./nitro/linux-cuda/nitro && chmod +x ./nitro/linux-start.sh", + "downloadnitro:darwin": "NITRO_VERSION=$(cat ./nitro/version.txt) && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-arm64.tar.gz -e --strip 1 -o ./nitro/mac-arm64 && chmod +x ./nitro/mac-arm64/nitro && download https://github.com/janhq/nitro/releases/download/v${NITRO_VERSION}/nitro-${NITRO_VERSION}-mac-amd64.tar.gz -e --strip 1 -o ./nitro/mac-x64 && chmod +x ./nitro/mac-x64/nitro", + "downloadnitro:win32": "download.bat", "downloadnitro": "run-script-os", "build:publish:darwin": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && ../../.github/scripts/auto-sign.sh && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", "build:publish:win32": "rimraf *.tgz --glob && npm run build && npm run downloadnitro && cpx \"nitro/**\" \"dist/nitro\" && npm pack && cpx *.tgz ../../electron/core/pre-install", diff --git a/plugins/inference-plugin/src/helpers/sse.ts b/plugins/inference-plugin/src/helpers/sse.ts index 9877512218..f427e443c9 100644 --- a/plugins/inference-plugin/src/helpers/sse.ts +++ b/plugins/inference-plugin/src/helpers/sse.ts @@ -4,7 +4,10 @@ import { Observable } from "rxjs"; * @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[], controller?: AbortController): Observable { +export function requestInference( + recentMessages: any[], + controller?: AbortController +): Observable { return new Observable((subscriber) => { const requestBody = JSON.stringify({ messages: recentMessages, @@ -20,7 +23,7 @@ export function requestInference(recentMessages: any[], controller?: AbortContro "Access-Control-Allow-Origin": "*", }, body: requestBody, - signal: controller?.signal + signal: controller?.signal, }) .then(async (response) => { const stream = response.body; diff --git a/plugins/inference-plugin/src/index.ts b/plugins/inference-plugin/src/index.ts index 167ce0626f..7a1f851866 100644 --- a/plugins/inference-plugin/src/index.ts +++ b/plugins/inference-plugin/src/index.ts @@ -8,19 +8,22 @@ import { ChatCompletionRole, + ContentType, EventName, MessageRequest, MessageStatus, + ModelSettingParams, PluginType, + ThreadContent, ThreadMessage, events, executeOnMain, + getUserSpace, } from "@janhq/core"; import { InferencePlugin } from "@janhq/core/lib/plugins"; import { requestInference } from "./helpers/sse"; import { ulid } from "ulid"; import { join } from "path"; -import { getUserSpace } from "@janhq/core"; /** * A class that implements the InferencePlugin interface from the @janhq/core package. @@ -56,14 +59,20 @@ export default class JanInferencePlugin implements InferencePlugin { /** * Initializes the model with the specified file name. - * @param {string} modelFileName - The file name of the model file. + * @param {string} modelId - The ID of the model to initialize. * @returns {Promise} A promise that resolves when the model is initialized. */ - async initModel(modelFileName: string): Promise { + async initModel( + modelId: string, + settings?: ModelSettingParams + ): Promise { const userSpacePath = await getUserSpace(); - const modelFullPath = join(userSpacePath, modelFileName); + const modelFullPath = join(userSpacePath, "models", modelId, modelId); - return executeOnMain(MODULE, "initModel", modelFullPath); + return executeOnMain(MODULE, "initModel", { + modelFullPath, + settings, + }); } /** @@ -89,18 +98,21 @@ export default class JanInferencePlugin implements InferencePlugin { * @returns {Promise} A promise that resolves with the inference response. */ async inferenceRequest(data: MessageRequest): Promise { + const timestamp = Date.now(); const message: ThreadMessage = { - threadId: data.threadId, - content: "", - createdAt: new Date().toISOString(), + thread_id: data.threadId, + created: timestamp, + updated: timestamp, status: MessageStatus.Ready, + id: "", + role: ChatCompletionRole.Assistant, + object: "thread.message", + content: [], }; return new Promise(async (resolve, reject) => { requestInference(data.messages ?? []).subscribe({ - next: (content) => { - message.content = content; - }, + next: (_content) => {}, complete: async () => { resolve(message); }, @@ -121,33 +133,49 @@ export default class JanInferencePlugin implements InferencePlugin { data: MessageRequest, instance: JanInferencePlugin ) { + const timestamp = Date.now(); const message: ThreadMessage = { - threadId: data.threadId, - content: "", - role: ChatCompletionRole.Assistant, - createdAt: new Date().toISOString(), id: ulid(), + thread_id: data.threadId, + assistant_id: data.assistantId, + role: ChatCompletionRole.Assistant, + content: [], status: MessageStatus.Pending, + created: timestamp, + updated: timestamp, + object: "thread.message", }; events.emit(EventName.OnNewMessageResponse, message); + console.log(JSON.stringify(data, null, 2)); instance.isCancelled = false; instance.controller = new AbortController(); requestInference(data.messages, instance.controller).subscribe({ next: (content) => { - message.content = content; + const messageContent: ThreadContent = { + type: ContentType.Text, + text: { + value: content.trim(), + annotations: [], + }, + }; + message.content = [messageContent]; events.emit(EventName.OnMessageResponseUpdate, message); }, complete: async () => { - message.content = message.content.trim(); message.status = MessageStatus.Ready; events.emit(EventName.OnMessageResponseFinished, message); }, error: async (err) => { - message.content = - message.content.trim() + - (instance.isCancelled ? "" : "\n" + "Error occurred: " + err.message); + const messageContent: ThreadContent = { + type: ContentType.Text, + text: { + value: "Error occurred: " + err.message, + annotations: [], + }, + }; + message.content = [messageContent]; message.status = MessageStatus.Ready; events.emit(EventName.OnMessageResponseUpdate, message); }, diff --git a/plugins/inference-plugin/src/module.ts b/plugins/inference-plugin/src/module.ts index 74cc9d89ce..b90cba740e 100644 --- a/plugins/inference-plugin/src/module.ts +++ b/plugins/inference-plugin/src/module.ts @@ -35,10 +35,20 @@ interface InitModelResponse { * TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package * TODO: Should it be startModel instead? */ -function initModel(modelFile: string): Promise { +function initModel(wrapper: any): Promise { // 1. Check if the model file exists - currentModelFile = modelFile; - log.info("Started to load model " + modelFile); + currentModelFile = wrapper.modelFullPath; + log.info("Started to load model " + wrapper.modelFullPath); + + const settings = { + llama_model_path: currentModelFile, + ctx_len: 2048, + ngl: 100, + cont_batching: false, + embedding: false, // Always enable embedding mode on + ...wrapper.settings, + }; + log.info(`Load model settings: ${JSON.stringify(settings, null, 2)}`); return ( // 1. Check if the port is used, if used, attempt to unload model / kill nitro process @@ -47,12 +57,12 @@ function initModel(modelFile: string): Promise { // 2. Spawn the Nitro subprocess .then(spawnNitroProcess) // 4. Load the model into the Nitro subprocess (HTTP POST request) - .then(loadLLMModel) + .then(() => loadLLMModel(settings)) // 5. Check if the model is loaded successfully .then(validateModelStatus) .catch((err) => { log.error("error: " + JSON.stringify(err)); - return { error: err, modelFile }; + return { error: err, currentModelFile }; }) ); } @@ -61,22 +71,14 @@ function initModel(modelFile: string): Promise { * Loads a LLM model into the Nitro subprocess by sending a HTTP POST request. * @returns A Promise that resolves when the model is loaded successfully, or rejects with an error message if the model is not found or fails to load. */ -function loadLLMModel(): Promise { - const config = { - llama_model_path: currentModelFile, - ctx_len: 2048, - ngl: 100, - cont_batching: false, - embedding: false, // Always enable embedding mode on - }; - +function loadLLMModel(settings): Promise { // Load model config return fetchRetry(NITRO_HTTP_LOAD_MODEL_URL, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(config), + body: JSON.stringify(settings), retries: 3, retryDelay: 500, }).catch((err) => { @@ -151,7 +153,7 @@ function checkAndUnloadNitro() { "Content-Type": "application/json", }, }).catch((err) => { - console.log(err); + console.error(err); // Fallback to kill the port return killSubprocess(); }); @@ -195,7 +197,7 @@ async function spawnNitroProcess(): Promise { // Handle subprocess output subprocess.stdout.on("data", (data) => { - console.log(`stdout: ${data}`); + console.debug(`stdout: ${data}`); }); subprocess.stderr.on("data", (data) => { @@ -204,7 +206,7 @@ async function spawnNitroProcess(): Promise { }); subprocess.on("close", (code) => { - console.log(`child process exited with code ${code}`); + console.debug(`child process exited with code ${code}`); subprocess = null; reject(`Nitro process exited. ${code ?? ""}`); }); diff --git a/plugins/model-plugin/src/helpers/modelParser.ts b/plugins/model-plugin/src/helpers/modelParser.ts index 242dc7e637..3a397fb7da 100644 --- a/plugins/model-plugin/src/helpers/modelParser.ts +++ b/plugins/model-plugin/src/helpers/modelParser.ts @@ -1,32 +1,46 @@ import { ModelCatalog } from '@janhq/core' -export function parseToModel(schema: ModelSchema): ModelCatalog { +export const parseToModel = (modelGroup): ModelCatalog => { const modelVersions = [] - schema.versions.forEach((v) => { - const version = { + modelGroup.versions.forEach((v) => { + const model = { + object: 'model', + version: modelGroup.version, + source_url: v.downloadLink, + id: v.name, name: v.name, - quantMethod: v.quantMethod, - bits: v.bits, - size: v.size, - maxRamRequired: v.maxRamRequired, - usecase: v.usecase, - downloadLink: v.downloadLink, + owned_by: 'you', + created: 0, + description: modelGroup.longDescription, + state: 'to_download', + settings: v.settings, + parameters: v.parameters, + metadata: { + engine: '', + quantization: v.quantMethod, + size: v.size, + binaries: [], + maxRamRequired: v.maxRamRequired, + author: modelGroup.author, + avatarUrl: modelGroup.avatarUrl, + }, } - modelVersions.push(version) + modelVersions.push(model) }) - const model: ModelCatalog = { - id: schema.id, - name: schema.name, - shortDescription: schema.shortDescription, - avatarUrl: schema.avatarUrl, - author: schema.author, - version: schema.version, - modelUrl: schema.modelUrl, - tags: schema.tags, - longDescription: schema.longDescription, - releaseDate: 0, + const modelCatalog: ModelCatalog = { + id: modelGroup.id, + name: modelGroup.name, + avatarUrl: modelGroup.avatarUrl, + shortDescription: modelGroup.shortDescription, + longDescription: modelGroup.longDescription, + author: modelGroup.author, + version: modelGroup.version, + modelUrl: modelGroup.modelUrl, + releaseDate: modelGroup.createdAt, + tags: modelGroup.tags, availableVersions: modelVersions, } - return model + + return modelCatalog } diff --git a/plugins/model-plugin/src/index.ts b/plugins/model-plugin/src/index.ts index 5fb4870170..7ca63e7086 100644 --- a/plugins/model-plugin/src/index.ts +++ b/plugins/model-plugin/src/index.ts @@ -1,4 +1,4 @@ -import { PluginType, fs, downloadFile } from '@janhq/core' +import { PluginType, fs, downloadFile, abortDownload } from '@janhq/core' import { ModelPlugin } from '@janhq/core/lib/plugins' import { Model, ModelCatalog } from '@janhq/core/lib/types' import { parseToModel } from './helpers/modelParser' @@ -9,6 +9,8 @@ import { join } from 'path' */ export default class JanModelPlugin implements ModelPlugin { private static readonly _homeDir = 'models' + private static readonly _modelMetadataFileName = 'model.json' + /** * Implements type from JanPlugin. * @override @@ -42,12 +44,25 @@ export default class JanModelPlugin implements ModelPlugin { */ async downloadModel(model: Model): Promise { // create corresponding directory - const directoryPath = join(JanModelPlugin._homeDir, model.name) + const directoryPath = join(JanModelPlugin._homeDir, model.id) await fs.mkdir(directoryPath) // path to model binary const path = join(directoryPath, model.id) - downloadFile(model.downloadLink, path) + downloadFile(model.source_url, path) + } + + /** + * Cancels the download of a specific machine learning model. + * @param {string} modelId - The ID of the model whose download is to be cancelled. + * @returns {Promise} A promise that resolves when the download has been cancelled. + */ + async cancelModelDownload(modelId: string): Promise { + return abortDownload(join(JanModelPlugin._homeDir, modelId, modelId)).then( + () => { + fs.rmdir(join(JanModelPlugin._homeDir, modelId)) + } + ) } /** @@ -55,12 +70,10 @@ export default class JanModelPlugin implements ModelPlugin { * @param filePath - The path to the model file to delete. * @returns A Promise that resolves when the model is deleted. */ - async deleteModel(filePath: string): Promise { + async deleteModel(modelId: string): Promise { try { - await Promise.allSettled([ - fs.deleteFile(filePath), - fs.deleteFile(`${filePath}.json`), - ]) + const dirPath = join(JanModelPlugin._homeDir, modelId) + await fs.rmdir(dirPath) } catch (err) { console.error(err) } @@ -72,11 +85,14 @@ export default class JanModelPlugin implements ModelPlugin { * @returns A Promise that resolves when the model is saved. */ async saveModel(model: Model): Promise { - const directoryPath = join(JanModelPlugin._homeDir, model.name) - const jsonFilePath = join(directoryPath, `${model.id}.json`) + const jsonFilePath = join( + JanModelPlugin._homeDir, + model.id, + JanModelPlugin._modelMetadataFileName + ) try { - await fs.writeFile(jsonFilePath, JSON.stringify(model)) + await fs.writeFile(jsonFilePath, JSON.stringify(model, null, 2)) } catch (err) { console.error(err) } @@ -98,7 +114,7 @@ export default class JanModelPlugin implements ModelPlugin { } const jsonFiles: string[] = (await fs.listFiles(modelDirPath)).filter( - (file: string) => file.endsWith('.json') + (fileName: string) => fileName === JanModelPlugin._modelMetadataFileName ) for (const json of jsonFiles) { diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx new file mode 100644 index 0000000000..42f975aafd --- /dev/null +++ b/web/containers/CardSidebar/index.tsx @@ -0,0 +1,87 @@ +import { ReactNode, useState } from 'react' +import { Fragment } from 'react' + +import { Menu, Transition } from '@headlessui/react' +import { + ChevronDownIcon, + EllipsisVerticalIcon, +} from '@heroicons/react/20/solid' +import { twMerge } from 'tailwind-merge' + +interface Props { + children: ReactNode + title: string + onRevealInFinderClick: (type: string) => void + onViewJsonClick: (type: string) => void +} +export default function CardSidebar({ + children, + title, + onRevealInFinderClick, + onViewJsonClick, +}: Props) { + const [show, setShow] = useState(true) + + return ( + + ) +} diff --git a/web/containers/DropdownListSidebar/index.tsx b/web/containers/DropdownListSidebar/index.tsx new file mode 100644 index 0000000000..91d194fbdb --- /dev/null +++ b/web/containers/DropdownListSidebar/index.tsx @@ -0,0 +1,104 @@ +import { Fragment, useEffect, useState } from 'react' + +import { Listbox, Transition } from '@headlessui/react' +import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' + +import { Model } from '@janhq/core/lib/types' +import { atom, useSetAtom } from 'jotai' +import { twMerge } from 'tailwind-merge' + +import { getDownloadedModels } from '@/hooks/useGetDownloadedModels' + +export const selectedModelAtom = atom(undefined) + +export default function DropdownListSidebar() { + const [downloadedModels, setDownloadedModels] = useState([]) + const [selected, setSelected] = useState() + const setSelectedModel = useSetAtom(selectedModelAtom) + + useEffect(() => { + getDownloadedModels().then((downloadedModels) => { + setDownloadedModels(downloadedModels) + + if (downloadedModels.length > 0) { + setSelected(downloadedModels[0]) + setSelectedModel(downloadedModels[0]) + } + }) + }, []) + + if (!selected) return null + + return ( + { + setSelected(model) + setSelectedModel(model) + }} + > + {({ open }) => ( + <> +
+ + {selected.name} + + + + + + + {downloadedModels.map((model) => ( + + twMerge( + active ? 'bg-indigo-600 text-white' : 'text-gray-900', + 'relative cursor-default select-none py-2 pl-3 pr-9' + ) + } + value={model} + > + {({ selected, active }) => ( + <> + + {model.name} + + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+ + )} +
+ ) +} diff --git a/web/containers/ItemCardSidebar/index.tsx b/web/containers/ItemCardSidebar/index.tsx new file mode 100644 index 0000000000..b6a7bacbd9 --- /dev/null +++ b/web/containers/ItemCardSidebar/index.tsx @@ -0,0 +1,20 @@ +type Props = { + title: string + description?: string +} + +export default function ItemCardSidebar({ description, title }: Props) { + return ( +
+
+ {title} +
+ +
+ ) +} diff --git a/web/containers/JanImage/index.tsx b/web/containers/JanImage/index.tsx deleted file mode 100644 index a3b11cff48..0000000000 --- a/web/containers/JanImage/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' - -type Props = { - imageUrl: string - className?: string - alt?: string - width?: number - height?: number -} - -const JanImage: React.FC = ({ - imageUrl, - className = '', - alt = '', - width, - height, -}) => { - const [attempt, setAttempt] = React.useState(0) - - return ( - {alt} setAttempt(attempt + 1)} - /> - ) -} - -export default JanImage diff --git a/web/containers/Layout/BottomBar/DownloadingState/index.tsx b/web/containers/Layout/BottomBar/DownloadingState/index.tsx index c825baa962..7eb28d7723 100644 --- a/web/containers/Layout/BottomBar/DownloadingState/index.tsx +++ b/web/containers/Layout/BottomBar/DownloadingState/index.tsx @@ -1,5 +1,7 @@ import { Fragment } from 'react' +import { PluginType } from '@janhq/core' +import { ModelPlugin } from '@janhq/core/lib/plugins' import { Progress, Modal, @@ -10,12 +12,18 @@ import { ModalTrigger, } from '@janhq/uikit' +import { useAtomValue } from 'jotai' + import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' +import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' +import { pluginManager } from '@/plugin' + export default function DownloadingState() { const { downloadStates } = useDownloadState() + const models = useAtomValue(downloadingModelsAtom) const totalCurrentProgress = downloadStates .map((a) => a.size.transferred + a.size.transferred) @@ -67,11 +75,17 @@ export default function DownloadingState() { diff --git a/web/containers/Layout/Ribbon/index.tsx b/web/containers/Layout/Ribbon/index.tsx index 09f29ced77..6babadb9dc 100644 --- a/web/containers/Layout/Ribbon/index.tsx +++ b/web/containers/Layout/Ribbon/index.tsx @@ -20,15 +20,12 @@ import { twMerge } from 'tailwind-merge' import LogoMark from '@/containers/Brand/Logo/Mark' -import { FeatureToggleContext } from '@/context/FeatureToggle' - import { MainViewState } from '@/constants/screens' import { useMainViewState } from '@/hooks/useMainViewState' export default function RibbonNav() { const { mainViewState, setMainViewState } = useMainViewState() - const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) const onMenuClick = (state: MainViewState) => { if (mainViewState === state) return @@ -49,8 +46,6 @@ export default function RibbonNav() { ] const secondaryMenus = [ - // Add menu if experimental feature - ...(experimentalFeatureEnabed ? [] : []), { name: 'Explore Models', icon: , diff --git a/web/containers/Layout/TopBar/CommandSearch/index.tsx b/web/containers/Layout/TopBar/CommandSearch/index.tsx index cd7b815d60..31896e81f4 100644 --- a/web/containers/Layout/TopBar/CommandSearch/index.tsx +++ b/web/containers/Layout/TopBar/CommandSearch/index.tsx @@ -11,6 +11,7 @@ import { CommandList, } from '@janhq/uikit' +import { useSetAtom } from 'jotai' import { MessageCircleIcon, SettingsIcon, @@ -27,9 +28,12 @@ import { MainViewState } from '@/constants/screens' import { useMainViewState } from '@/hooks/useMainViewState' +import { showRightSideBarAtom } from '@/screens/Chat/Sidebar' + export default function CommandSearch() { - const { experimentalFeatureEnabed } = useContext(FeatureToggleContext) const { setMainViewState } = useMainViewState() + const [open, setOpen] = useState(false) + const setShowRightSideBar = useSetAtom(showRightSideBarAtom) const menus = [ { @@ -44,8 +48,6 @@ export default function CommandSearch() { ), state: MainViewState.Chat, }, - // Added experimental feature here - ...(experimentalFeatureEnabed ? [] : []), { name: 'Explore Models', icon: , @@ -64,8 +66,6 @@ export default function CommandSearch() { }, ] - const [open, setOpen] = useState(false) - useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { @@ -123,6 +123,13 @@ export default function CommandSearch() { + ) } diff --git a/web/containers/ModalCancelDownload/index.tsx b/web/containers/ModalCancelDownload/index.tsx index 33476fda16..07d62fa0ce 100644 --- a/web/containers/ModalCancelDownload/index.tsx +++ b/web/containers/ModalCancelDownload/index.tsx @@ -1,6 +1,8 @@ import { useMemo } from 'react' -import { ModelVersion } from '@janhq/core/lib/types' +import { PluginType } from '@janhq/core' +import { ModelPlugin } from '@janhq/core/lib/plugins' +import { Model } from '@janhq/core/lib/types' import { Modal, @@ -19,8 +21,11 @@ import { useDownloadState } from '@/hooks/useDownloadState' import { formatDownloadPercentage } from '@/utils/converter' +import { downloadingModelsAtom } from '@/helpers/atoms/Model.atom' +import { pluginManager } from '@/plugin' + type Props = { - suitableModel: ModelVersion + suitableModel: Model isFromList?: boolean } @@ -34,6 +39,7 @@ export default function ModalCancelDownload({ // eslint-disable-next-line react-hooks/exhaustive-deps [suitableModel.name] ) + const models = useAtomValue(downloadingModelsAtom) const downloadState = useAtomValue(downloadAtom) return ( @@ -66,10 +72,15 @@ export default function ModalCancelDownload({ - - )} - {isSettingInstruction && ( -
-