diff --git a/core/src/browser/fs.ts b/core/src/browser/fs.ts index 164e3b647..cca9bb1d3 100644 --- a/core/src/browser/fs.ts +++ b/core/src/browser/fs.ts @@ -55,20 +55,6 @@ const unlinkSync = (...args: any[]) => globalThis.core.api?.unlinkSync(...args) */ const appendFileSync = (...args: any[]) => globalThis.core.api?.appendFileSync(...args) -/** - * Synchronizes a file from a source path to a destination path. - * @param {string} src - The source path of the file to be synchronized. - * @param {string} dest - The destination path where the file will be synchronized to. - * @returns {Promise} - A promise that resolves when the file has been successfully synchronized. - */ -const syncFile: (src: string, dest: string) => Promise = (src, dest) => - globalThis.core.api?.syncFile(src, dest) - -/** - * Copy file sync. - */ -const copyFileSync = (...args: any[]) => globalThis.core.api?.copyFileSync(...args) - const copyFile: (src: string, dest: string) => Promise = (src, dest) => globalThis.core.api?.copyFile(src, dest) @@ -95,9 +81,7 @@ export const fs = { rm, unlinkSync, appendFileSync, - copyFileSync, copyFile, - syncFile, fileStat, writeBlob, } diff --git a/core/src/node/api/processors/download.ts b/core/src/node/api/processors/download.ts index a4af47400..07486bdf8 100644 --- a/core/src/node/api/processors/download.ts +++ b/core/src/node/api/processors/download.ts @@ -1,6 +1,6 @@ import { resolve, sep } from 'path' import { DownloadEvent } from '../../../types/api' -import { normalizeFilePath } from '../../helper/path' +import { normalizeFilePath, validatePath } from '../../helper/path' import { getJanDataFolderPath } from '../../helper' import { DownloadManager } from '../../helper/download' import { createWriteStream, renameSync } from 'fs' @@ -37,6 +37,7 @@ export class Downloader implements Processor { const modelId = array.pop() ?? '' const destination = resolve(getJanDataFolderPath(), normalizedPath) + validatePath(destination) const rq = request({ url, strictSSL, proxy }) // Put request to download manager instance diff --git a/core/src/node/api/processors/fs.ts b/core/src/node/api/processors/fs.ts index a66f5a0e9..0557d2187 100644 --- a/core/src/node/api/processors/fs.ts +++ b/core/src/node/api/processors/fs.ts @@ -1,5 +1,5 @@ -import { join } from 'path' -import { normalizeFilePath } from '../../helper/path' +import { join, resolve } from 'path' +import { normalizeFilePath, validatePath } from '../../helper/path' import { getJanDataFolderPath } from '../../helper' import { Processor } from './Processor' import fs from 'fs' @@ -15,17 +15,29 @@ export class FileSystem implements Processor { process(route: string, ...args: any): any { const instance = this as any const func = instance[route] - if (func) { return func(...args) } else { return import(FileSystem.moduleName).then((mdl) => mdl[route]( - ...args.map((arg: any) => { - return typeof arg === 'string' && - (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) - ? join(getJanDataFolderPath(), normalizeFilePath(arg)) - : arg + ...args.map((arg: any, index: number) => { + if(index !== 0) { + return arg + } + if (index === 0 && typeof arg !== 'string') { + throw new Error(`Invalid argument ${JSON.stringify(args)}`) + } + const path = + (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) + ? join(getJanDataFolderPath(), normalizeFilePath(arg)) + : arg + + if(path.startsWith(`http://`) || path.startsWith(`https://`)) { + return path + } + const absolutePath = resolve(path) + validatePath(absolutePath) + return absolutePath }) ) ) @@ -42,8 +54,11 @@ export class FileSystem implements Processor { path = join(getJanDataFolderPath(), normalizeFilePath(path)) } + const absolutePath = resolve(path) + validatePath(absolutePath) + return new Promise((resolve, reject) => { - fs.rm(path, { recursive: true, force: true }, (err) => { + fs.rm(absolutePath, { recursive: true, force: true }, (err) => { if (err) { reject(err) } else { @@ -63,8 +78,11 @@ export class FileSystem implements Processor { path = join(getJanDataFolderPath(), normalizeFilePath(path)) } + const absolutePath = resolve(path) + validatePath(absolutePath) + return new Promise((resolve, reject) => { - fs.mkdir(path, { recursive: true }, (err) => { + fs.mkdir(absolutePath, { recursive: true }, (err) => { if (err) { reject(err) } else { @@ -73,4 +91,5 @@ export class FileSystem implements Processor { }) }) } + } diff --git a/core/src/node/api/processors/fsExt.ts b/core/src/node/api/processors/fsExt.ts index 4787da65b..155732cfc 100644 --- a/core/src/node/api/processors/fsExt.ts +++ b/core/src/node/api/processors/fsExt.ts @@ -1,6 +1,6 @@ import { join } from 'path' import fs from 'fs' -import { appResourcePath, normalizeFilePath } from '../../helper/path' +import { appResourcePath, normalizeFilePath, validatePath } from '../../helper/path' import { getJanDataFolderPath, getJanDataFolderPath as getPath } from '../../helper' import { Processor } from './Processor' import { FileStat } from '../../../types' @@ -18,19 +18,6 @@ export class FSExt implements Processor { return func(...args) } - // Handles the 'syncFile' IPC event. This event is triggered to synchronize a file from a source path to a destination path. - syncFile(src: string, dest: string) { - const reflect = require('@alumna/reflect') - return reflect({ - src, - dest, - recursive: true, - delete: false, - overwrite: true, - errorOnExist: false, - }) - } - // Handles the 'getJanDataFolderPath' IPC event. This event is triggered to get the user space path. getJanDataFolderPath() { return Promise.resolve(getPath()) @@ -70,14 +57,18 @@ export class FSExt implements Processor { writeBlob(path: string, data: any) { try { const normalizedPath = normalizeFilePath(path) + const dataBuffer = Buffer.from(data, 'base64') - fs.writeFileSync(join(getJanDataFolderPath(), normalizedPath), dataBuffer) + const writePath = join(getJanDataFolderPath(), normalizedPath) + validatePath(writePath) + fs.writeFileSync(writePath, dataBuffer) } catch (err) { console.error(`writeFile ${path} result: ${err}`) } } copyFile(src: string, dest: string): Promise { + validatePath(dest) return new Promise((resolve, reject) => { fs.copyFile(src, dest, (err) => { if (err) { diff --git a/core/src/node/helper/config.ts b/core/src/node/helper/config.ts index ee9a1f856..1a341a625 100644 --- a/core/src/node/helper/config.ts +++ b/core/src/node/helper/config.ts @@ -7,7 +7,8 @@ import childProcess from 'child_process' const configurationFileName = 'settings.json' // TODO: do no specify app name in framework module -const defaultJanDataFolder = join(os.homedir(), 'jan') +// TODO: do not default the os.homedir +const defaultJanDataFolder = join(os?.homedir() || '', 'jan') const defaultAppConfig: AppConfiguration = { data_folder: defaultJanDataFolder, quick_ask: false, diff --git a/core/src/node/helper/download.ts b/core/src/node/helper/download.ts index b7560d100..51a0b0a8f 100644 --- a/core/src/node/helper/download.ts +++ b/core/src/node/helper/download.ts @@ -11,7 +11,7 @@ export class DownloadManager { // store the download information with key is model id public downloadProgressMap: Record = {} - // store the download infomation with key is normalized file path + // store the download information with key is normalized file path public downloadInfo: Record = {} constructor() { diff --git a/core/src/node/helper/path.ts b/core/src/node/helper/path.ts index c20889f4c..a2d57ed3e 100644 --- a/core/src/node/helper/path.ts +++ b/core/src/node/helper/path.ts @@ -1,4 +1,5 @@ -import { join } from 'path' +import { join, resolve } from 'path' +import { getJanDataFolderPath } from './config' /** * Normalize file path @@ -33,3 +34,11 @@ export async function appResourcePath(): Promise { // server return join(global.core.appPath(), '../../..') } + +export function validatePath(path: string) { + const janDataFolderPath = getJanDataFolderPath() + const absolutePath = resolve(__dirname, path) + if (!absolutePath.startsWith(janDataFolderPath)) { + throw new Error(`Invalid path: ${absolutePath}`) + } +} diff --git a/core/src/types/api/index.ts b/core/src/types/api/index.ts index a9d0341bd..e50dce6de 100644 --- a/core/src/types/api/index.ts +++ b/core/src/types/api/index.ts @@ -90,7 +90,6 @@ export enum ExtensionRoute { } export enum FileSystemRoute { appendFileSync = 'appendFileSync', - copyFileSync = 'copyFileSync', unlinkSync = 'unlinkSync', existsSync = 'existsSync', readdirSync = 'readdirSync', @@ -100,7 +99,6 @@ export enum FileSystemRoute { writeFileSync = 'writeFileSync', } export enum FileManagerRoute { - syncFile = 'syncFile', copyFile = 'copyFile', getJanDataFolderPath = 'getJanDataFolderPath', getResourcePath = 'getResourcePath', diff --git a/core/src/types/assistant/assistantEntity.ts b/core/src/types/assistant/assistantEntity.ts index 733dbea8d..27592e26b 100644 --- a/core/src/types/assistant/assistantEntity.ts +++ b/core/src/types/assistant/assistantEntity.ts @@ -6,6 +6,7 @@ export type AssistantTool = { type: string enabled: boolean + useTimeWeightedRetriever?: boolean settings: any } diff --git a/core/src/types/message/messageEntity.ts b/core/src/types/message/messageEntity.ts index f96919a8b..26bcad1a7 100644 --- a/core/src/types/message/messageEntity.ts +++ b/core/src/types/message/messageEntity.ts @@ -83,6 +83,8 @@ export enum MessageStatus { export enum ErrorCode { InvalidApiKey = 'invalid_api_key', + AuthenticationError = 'authentication_error', + InsufficientQuota = 'insufficient_quota', InvalidRequestError = 'invalid_request_error', diff --git a/electron/preload.ts b/electron/preload.ts index 6ac259e0d..05f48d37a 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -3,8 +3,9 @@ * @module preload */ -import { APIEvents, APIRoutes } from '@janhq/core/node' +import { APIEvents, APIRoutes, AppConfiguration, getAppConfigurations, updateAppConfiguration } from '@janhq/core/node' import { contextBridge, ipcRenderer } from 'electron' +import { readdirSync } from 'fs' const interfaces: { [key: string]: (...args: any[]) => any } = {} @@ -12,7 +13,9 @@ const interfaces: { [key: string]: (...args: any[]) => any } = {} APIRoutes.forEach((method) => { // For each method, create a function on the interfaces object // This function invokes the method on the ipcRenderer with any provided arguments + interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args) + }) // Loop over each method in APIEvents @@ -22,6 +25,33 @@ APIEvents.forEach((method) => { // The handler for the event is provided as an argument to the function interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) }) + + +interfaces['changeDataFolder'] = async path => { + const appConfiguration: AppConfiguration = await ipcRenderer.invoke('getAppConfigurations') + const currentJanDataFolder = appConfiguration.data_folder + appConfiguration.data_folder = path + const reflect = require('@alumna/reflect') + const { err } = await reflect({ + src: currentJanDataFolder, + dest: path, + recursive: true, + delete: false, + overwrite: true, + errorOnExist: false, + }) + if (err) { + console.error(err) + throw err + } + await ipcRenderer.invoke('updateAppConfiguration', appConfiguration) +} + +interfaces['isDirectoryEmpty'] = async path => { + const dirChildren = await readdirSync(path) + return dirChildren.filter((x) => x !== '.DS_Store').length === 0 +} + // Expose the 'interfaces' object in the main world under the name 'electronAPI' // This allows the renderer process to access these methods directly contextBridge.exposeInMainWorld('electronAPI', { diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 53d3ed0d5..12441995e 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -126,6 +126,7 @@ export default class JanAssistantExtension extends AssistantExtension { { type: 'retrieval', enabled: false, + useTimeWeightedRetriever: false, settings: { top_k: 2, chunk_size: 1024, diff --git a/extensions/assistant-extension/src/node/index.ts b/extensions/assistant-extension/src/node/index.ts index 46835614d..83a4a1983 100644 --- a/extensions/assistant-extension/src/node/index.ts +++ b/extensions/assistant-extension/src/node/index.ts @@ -11,13 +11,14 @@ export function toolRetrievalUpdateTextSplitter( export async function toolRetrievalIngestNewDocument( file: string, model: string, - engine: string + engine: string, + useTimeWeighted: boolean ) { const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file)) const threadPath = path.dirname(filePath.replace('files', '')) retrieval.updateEmbeddingEngine(model, engine) return retrieval - .ingestAgentKnowledge(filePath, `${threadPath}/memory`) + .ingestAgentKnowledge(filePath, `${threadPath}/memory`, useTimeWeighted) .catch((err) => { console.error(err) }) @@ -33,8 +34,11 @@ export async function toolRetrievalLoadThreadMemory(threadId: string) { }) } -export async function toolRetrievalQueryResult(query: string) { - return retrieval.generateResult(query).catch((err) => { +export async function toolRetrievalQueryResult( + query: string, + useTimeWeighted: boolean = false +) { + return retrieval.generateResult(query, useTimeWeighted).catch((err) => { console.error(err) }) } diff --git a/extensions/assistant-extension/src/node/retrieval.ts b/extensions/assistant-extension/src/node/retrieval.ts index 52193f221..28d629aa8 100644 --- a/extensions/assistant-extension/src/node/retrieval.ts +++ b/extensions/assistant-extension/src/node/retrieval.ts @@ -2,11 +2,16 @@ import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter' import { formatDocumentsAsString } from 'langchain/util/document' import { PDFLoader } from 'langchain/document_loaders/fs/pdf' +import { TimeWeightedVectorStoreRetriever } from 'langchain/retrievers/time_weighted' +import { MemoryVectorStore } from 'langchain/vectorstores/memory' + import { HNSWLib } from 'langchain/vectorstores/hnswlib' import { OpenAIEmbeddings } from 'langchain/embeddings/openai' import { readEmbeddingEngine } from './engine' +import path from 'path' + export class Retrieval { public chunkSize: number = 100 public chunkOverlap?: number = 0 @@ -15,8 +20,25 @@ export class Retrieval { private embeddingModel?: OpenAIEmbeddings = undefined private textSplitter?: RecursiveCharacterTextSplitter + // to support time-weighted retrieval + private timeWeightedVectorStore: MemoryVectorStore + private timeWeightedretriever: any | TimeWeightedVectorStoreRetriever + constructor(chunkSize: number = 4000, chunkOverlap: number = 200) { this.updateTextSplitter(chunkSize, chunkOverlap) + + // declare time-weighted retriever and storage + this.timeWeightedVectorStore = new MemoryVectorStore( + new OpenAIEmbeddings( + { openAIApiKey: 'nitro-embedding' }, + { basePath: 'http://127.0.0.1:3928/v1' } + ) + ) + this.timeWeightedretriever = new TimeWeightedVectorStoreRetriever({ + vectorStore: this.timeWeightedVectorStore, + memoryStream: [], + searchKwargs: 2, + }) } public updateTextSplitter(chunkSize: number, chunkOverlap: number): void { @@ -44,11 +66,15 @@ export class Retrieval { openAIApiKey: settings.api_key, }) } + + // update time-weighted embedding model + this.timeWeightedVectorStore.embeddings = this.embeddingModel } public ingestAgentKnowledge = async ( filePath: string, - memoryPath: string + memoryPath: string, + useTimeWeighted: boolean ): Promise => { const loader = new PDFLoader(filePath, { splitPages: true, @@ -57,6 +83,13 @@ export class Retrieval { const doc = await loader.load() const docs = await this.textSplitter!.splitDocuments(doc) const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel) + + // add documents with metadata by using the time-weighted retriever in order to support time-weighted retrieval + if (useTimeWeighted && this.timeWeightedretriever) { + await ( + this.timeWeightedretriever as TimeWeightedVectorStoreRetriever + ).addDocuments(docs) + } return vectorStore.save(memoryPath) } @@ -67,10 +100,25 @@ export class Retrieval { return Promise.resolve() } - public generateResult = async (query: string): Promise => { + public generateResult = async ( + query: string, + useTimeWeighted: boolean + ): Promise => { + if (useTimeWeighted) { + if (!this.timeWeightedretriever) { + return Promise.resolve(' ') + } + // use invoke because getRelevantDocuments is deprecated + const relevantDocs = await this.timeWeightedretriever.invoke(query) + const serializedDoc = formatDocumentsAsString(relevantDocs) + return Promise.resolve(serializedDoc) + } + if (!this.retriever) { return Promise.resolve(' ') } + + // should use invoke(query) because getRelevantDocuments is deprecated const relevantDocs = await this.retriever.getRelevantDocuments(query) const serializedDoc = formatDocumentsAsString(relevantDocs) return Promise.resolve(serializedDoc) diff --git a/extensions/assistant-extension/src/tools/retrieval.ts b/extensions/assistant-extension/src/tools/retrieval.ts index a1a641941..763192287 100644 --- a/extensions/assistant-extension/src/tools/retrieval.ts +++ b/extensions/assistant-extension/src/tools/retrieval.ts @@ -37,7 +37,8 @@ export class RetrievalTool extends InferenceTool { 'toolRetrievalIngestNewDocument', docFile, data.model?.id, - data.model?.engine + data.model?.engine, + tool?.useTimeWeightedRetriever ?? false ) } else { return Promise.resolve(data) @@ -78,7 +79,8 @@ export class RetrievalTool extends InferenceTool { const retrievalResult = await executeOnMain( NODE, 'toolRetrievalQueryResult', - prompt + prompt, + tool?.useTimeWeightedRetriever ?? false ) console.debug('toolRetrievalQueryResult', retrievalResult) diff --git a/extensions/inference-anthropic-extension/package.json b/extensions/inference-anthropic-extension/package.json index 53d685b46..a9d30a8e5 100644 --- a/extensions/inference-anthropic-extension/package.json +++ b/extensions/inference-anthropic-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/inference-anthropic-extension", "productName": "Anthropic Inference Engine", - "version": "1.0.1", + "version": "1.0.2", "description": "This extension enables Anthropic chat completion API calls", "main": "dist/index.js", "module": "dist/module.js", diff --git a/extensions/inference-anthropic-extension/resources/models.json b/extensions/inference-anthropic-extension/resources/models.json index 363e0bd38..1462837ac 100644 --- a/extensions/inference-anthropic-extension/resources/models.json +++ b/extensions/inference-anthropic-extension/resources/models.json @@ -19,10 +19,7 @@ }, "metadata": { "author": "Anthropic", - "tags": [ - "General", - "Big Context Length" - ] + "tags": ["General", "Big Context Length"] }, "engine": "anthropic" }, @@ -46,10 +43,7 @@ }, "metadata": { "author": "Anthropic", - "tags": [ - "General", - "Big Context Length" - ] + "tags": ["General", "Big Context Length"] }, "engine": "anthropic" }, @@ -73,11 +67,32 @@ }, "metadata": { "author": "Anthropic", - "tags": [ - "General", - "Big Context Length" - ] + "tags": ["General", "Big Context Length"] + }, + "engine": "anthropic" + }, + { + "sources": [ + { + "url": "https://www.anthropic.com/" + } + ], + "id": "claude-3-5-sonnet-20240620", + "object": "model", + "name": "Claude 3.5 Sonnet", + "version": "1.0", + "description": "Claude 3.5 Sonnet raises the industry bar for intelligence, outperforming competitor models and Claude 3 Opus on a wide range of evaluations, with the speed and cost of our mid-tier model, Claude 3 Sonnet.", + "format": "api", + "settings": {}, + "parameters": { + "max_tokens": 4096, + "temperature": 0.7, + "stream": true + }, + "metadata": { + "author": "Anthropic", + "tags": ["General", "Big Context Length"] }, "engine": "anthropic" } -] \ No newline at end of file +] diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt index 1f7716999..2b2a18d26 100644 --- a/extensions/inference-nitro-extension/bin/version.txt +++ b/extensions/inference-nitro-extension/bin/version.txt @@ -1 +1 @@ -0.4.13 +0.4.20 diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 21a345d6a..3150108c4 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/inference-cortex-extension", "productName": "Cortex Inference Engine", - "version": "1.0.13", + "version": "1.0.14", "description": "This extension embeds cortex.cpp, a lightweight inference engine written in C++. See https://nitro.jan.ai.\nAdditional dependencies could be installed to run without Cuda Toolkit installation.", "main": "dist/index.js", "node": "dist/node/index.cjs.js", @@ -10,7 +10,7 @@ "scripts": { "test": "jest", "build": "tsc --module commonjs && rollup -c rollup.config.ts", - "downloadnitro:linux": "CORTEX_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-cuda-12-0.tar.gz -e --strip 1 -o ./bin/linux-cuda-12-0 && chmod +x ./bin/linux-cuda-12-0/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-cuda-11-7.tar.gz -e --strip 1 -o ./bin/linux-cuda-11-7 && chmod +x ./bin/linux-cuda-11-7/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/linux-vulkan && chmod +x ./bin/linux-vulkan/cortex-cpp", + "downloadnitro:linux": "CORTEX_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2.tar.gz -e --strip 1 -o ./bin/linux-cpu && chmod +x ./bin/linux-cpu/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2-cuda-12-0.tar.gz -e --strip 1 -o ./bin/linux-cuda-12-0 && chmod +x ./bin/linux-cuda-12-0/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-avx2-cuda-11-7.tar.gz -e --strip 1 -o ./bin/linux-cuda-11-7 && chmod +x ./bin/linux-cuda-11-7/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-linux-amd64-vulkan.tar.gz -e --strip 1 -o ./bin/linux-vulkan && chmod +x ./bin/linux-vulkan/cortex-cpp", "downloadnitro:darwin": "CORTEX_VERSION=$(cat ./bin/version.txt) && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz -o ./bin/ && mkdir -p ./bin/mac-arm64 && tar -zxvf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz --strip-components=1 -C ./bin/mac-arm64 && rm -rf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-arm64.tar.gz && chmod +x ./bin/mac-arm64/cortex-cpp && download https://github.com/janhq/cortex/releases/download/v${CORTEX_VERSION}/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz -o ./bin/ && mkdir -p ./bin/mac-amd64 && tar -zxvf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz --strip-components=1 -C ./bin/mac-amd64 && rm -rf ./bin/cortex-cpp-${CORTEX_VERSION}-mac-amd64.tar.gz && chmod +x ./bin/mac-amd64/cortex-cpp", "downloadnitro:win32": "download.bat", "downloadnitro": "run-script-os", @@ -53,7 +53,7 @@ "fetch-retry": "^5.0.6", "rxjs": "^7.8.1", "tcp-port-used": "^1.0.2", - "terminate": "^2.6.1", + "terminate": "2.6.1", "ulidx": "^2.3.0" }, "engines": { diff --git a/extensions/inference-nitro-extension/resources/models/gemma-2b/model.json b/extensions/inference-nitro-extension/resources/models/gemma-2b/model.json index e5ee3c239..68cff325a 100644 --- a/extensions/inference-nitro-extension/resources/models/gemma-2b/model.json +++ b/extensions/inference-nitro-extension/resources/models/gemma-2b/model.json @@ -8,7 +8,7 @@ "id": "gemma-2b", "object": "model", "name": "Gemma 2B Q4", - "version": "1.2", + "version": "1.3", "description": "Gemma is built from the same technology with Google's Gemini.", "format": "gguf", "settings": { @@ -22,7 +22,7 @@ "top_p": 0.95, "stream": true, "max_tokens": 8192, - "stop": [], + "stop": [""], "frequency_penalty": 0, "presence_penalty": 0 }, diff --git a/extensions/model-extension/package.json b/extensions/model-extension/package.json index c0ca949bd..6bd8bbe5e 100644 --- a/extensions/model-extension/package.json +++ b/extensions/model-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/model-extension", "productName": "Model Management", - "version": "1.0.32", + "version": "1.0.33", "description": "Model Management Extension provides model exploration and seamless downloads", "main": "dist/index.js", "node": "dist/node/index.cjs.js", diff --git a/extensions/model-extension/src/index.ts b/extensions/model-extension/src/index.ts index aa8f6603b..7561ee6ed 100644 --- a/extensions/model-extension/src/index.ts +++ b/extensions/model-extension/src/index.ts @@ -383,7 +383,12 @@ export default class JanModelExtension extends ModelExtension { // model binaries (sources) are absolute path & exist const existFiles = await Promise.all( - model.sources.map((source) => fs.existsSync(source.url)) + model.sources.map( + (source) => + // Supposed to be a local file url + !source.url.startsWith(`http://`) && + !source.url.startsWith(`https://`) + ) ) if (existFiles.every((exist) => exist)) return true diff --git a/web/containers/ErrorMessage/index.tsx b/web/containers/ErrorMessage/index.tsx index 3979c3f4f..bcd056b93 100644 --- a/web/containers/ErrorMessage/index.tsx +++ b/web/containers/ErrorMessage/index.tsx @@ -41,12 +41,13 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => { case ErrorCode.Unknown: return 'Apologies, something’s amiss!' case ErrorCode.InvalidApiKey: + case ErrorCode.AuthenticationError: case ErrorCode.InvalidRequestError: return ( Invalid API key. Please check your API key from{' '}
diff --git a/web/containers/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx index b0fed7e66..c19fb64bd 100644 --- a/web/containers/ModelDropdown/index.tsx +++ b/web/containers/ModelDropdown/index.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect, useCallback } from 'react' +import { useState, useMemo, useEffect, useCallback, useRef } from 'react' import { InferenceEngine } from '@janhq/core' import { Badge, Input, ScrollArea, Select, useClickOutside } from '@janhq/joi' @@ -70,7 +70,7 @@ const ModelDropdown = ({ const downloadStates = useAtomValue(modelDownloadStateAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) const { updateModelParameter } = useUpdateModelParameters() - + const searchInputRef = useRef(null) const configuredModels = useAtomValue(configuredModelsAtom) const featuredModel = configuredModels.filter((x) => x.metadata.tags.includes('Featured') @@ -83,7 +83,7 @@ const ModelDropdown = ({ const filteredDownloadedModels = useMemo( () => - downloadedModels + configuredModels .filter((e) => e.name.toLowerCase().includes(searchText.toLowerCase().trim()) ) @@ -104,10 +104,31 @@ const ModelDropdown = ({ ) } }) - .sort((a, b) => a.name.localeCompare(b.name)), - [downloadedModels, searchText, searchFilter] + .sort((a, b) => a.name.localeCompare(b.name)) + .sort((a, b) => { + const aInDownloadedModels = downloadedModels.some( + (item) => item.id === a.id + ) + const bInDownloadedModels = downloadedModels.some( + (item) => item.id === b.id + ) + if (aInDownloadedModels && !bInDownloadedModels) { + return -1 + } else if (!aInDownloadedModels && bInDownloadedModels) { + return 1 + } else { + return 0 + } + }), + [configuredModels, searchText, searchFilter, downloadedModels] ) + useEffect(() => { + if (open && searchInputRef.current) { + searchInputRef.current.focus() + } + }, [open]) + useEffect(() => { if (!activeThread) return let model = downloadedModels.find( @@ -258,6 +279,7 @@ const ModelDropdown = ({ setSearchText(e.target.value)} suffixIcon={ @@ -290,58 +312,58 @@ const ModelDropdown = ({
- {filteredDownloadedModels.filter( - (x) => x.engine === InferenceEngine.nitro - ).length !== 0 ? ( + {searchFilter !== 'remote' && (
Cortex
+
+ {filteredDownloadedModels + .filter((x) => { + if (searchText.length === 0) { + return downloadedModels.find((c) => c.id === x.id) + } else { + return x + } + }) + .filter((x) => x.engine === InferenceEngine.nitro).length !== + 0 ? (
    {filteredDownloadedModels ? filteredDownloadedModels .filter((x) => x.engine === InferenceEngine.nitro) - .map((model) => { - return ( -
  • onClickModelItem(model.id)} - > -

    - {model.name} -

    - -
  • - ) + .filter((x) => { + if (searchText.length === 0) { + return downloadedModels.find((c) => c.id === x.id) + } else { + return x + } }) - : null} -
-
- - ) : ( - <> - {searchFilter !== 'remote' && ( -
-
-
- Cortex -
- {searchText.length === 0 ? ( -
    - {featuredModel.map((model) => { + .map((model) => { const isDownloading = downloadingModels.some( (md) => md.id === model.id ) + const isdDownloaded = downloadedModels.some( + (c) => c.id === model.id + ) return (
  • { + if (isdDownloaded) { + onClickModelItem(model.id) + } + }} >

    {model.name} @@ -352,10 +374,12 @@ const ModelDropdown = ({ />

    - - {toGibibytes(model.metadata.size)} - - {!isDownloading ? ( + {!isdDownloaded && ( + + {toGibibytes(model.metadata.size)} + + )} + {!isDownloading && !isdDownloaded ? (
  • ) - })} -
- ) : ( -
    - {configuredModels - .filter((x) => x.engine === InferenceEngine.nitro) - .filter((e) => - e.name - .toLowerCase() - .includes(searchText.toLowerCase().trim()) - ) - .map((model) => { - const isDownloading = downloadingModels.some( - (md) => md.id === model.id - ) - return ( -
  • -
    -

    - {model.name} -

    - -
    -
    - - {toGibibytes(model.metadata.size)} - - {!isDownloading ? ( - downloadModel(model)} - /> - ) : ( - Object.values(downloadStates) - .filter((x) => x.modelId === model.id) - .map((item) => ( - - )) - )} -
    -
  • - ) - })} -
- )} -
-
+ }) + : null} + + ) : ( +
    + {featuredModel.map((model) => { + const isDownloading = downloadingModels.some( + (md) => md.id === model.id + ) + return ( +
  • +
    +

    + {model.name} +

    + +
    +
    + + {toGibibytes(model.metadata.size)} + + {!isDownloading ? ( + downloadModel(model)} + /> + ) : ( + Object.values(downloadStates) + .filter((x) => x.modelId === model.id) + .map((item) => ( + + )) + )} +
    +
  • + ) + })} +
)} - + )} {groupByEngine.map((engine, i) => { diff --git a/web/helpers/atoms/Setting.atom.ts b/web/helpers/atoms/Setting.atom.ts index 62b4ed88a..ced0fbe37 100644 --- a/web/helpers/atoms/Setting.atom.ts +++ b/web/helpers/atoms/Setting.atom.ts @@ -10,6 +10,7 @@ export const janSettingScreenAtom = atom([]) export const THEME = 'themeAppearance' export const REDUCE_TRANSPARENT = 'reduceTransparent' +export const SPELL_CHECKING = 'spellChecking' export const themesOptionsAtom = atom<{ name: string; value: string }[]>([]) export const janThemesPathAtom = atom(undefined) export const selectedThemeIdAtom = atomWithStorage(THEME, '') @@ -18,3 +19,4 @@ export const reduceTransparentAtom = atomWithStorage( REDUCE_TRANSPARENT, false ) +export const spellCheckAtom = atomWithStorage(SPELL_CHECKING, true) diff --git a/web/package.json b/web/package.json index e5106711c..53e04c3e6 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "lucide-react": "^0.291.0", "marked": "^9.1.2", "marked-highlight": "^2.0.6", - "marked-katex-extension": "^5.0.1", + "marked-katex-extension": "^5.0.2", "next": "14.2.3", "next-themes": "^0.2.1", "postcss": "8.4.31", diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 362a8be58..1ce06979c 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -1,6 +1,6 @@ import { Fragment, useCallback, useState } from 'react' -import { fs, AppConfiguration, isSubdirectory } from '@janhq/core' +import { AppConfiguration, isSubdirectory } from '@janhq/core' import { Button, Input } from '@janhq/joi' import { useAtomValue, useSetAtom } from 'jotai' import { PencilIcon, FolderOpenIcon } from 'lucide-react' @@ -51,11 +51,10 @@ const DataFolder = () => { return } - const newDestChildren: string[] = await fs.readdirSync(destFolder) - const isNotEmpty = - newDestChildren.filter((x) => x !== '.DS_Store').length > 0 + const isEmpty: boolean = + await window.core?.api?.isDirectoryEmpty(destFolder) - if (isNotEmpty) { + if (!isEmpty) { setDestinationPath(destFolder) showDestNotEmptyConfirm(true) return @@ -74,16 +73,7 @@ const DataFolder = () => { if (!destinationPath) return try { setShowLoader(true) - const appConfiguration: AppConfiguration = - await window.core?.api?.getAppConfigurations() - const currentJanDataFolder = appConfiguration.data_folder - appConfiguration.data_folder = destinationPath - const { err } = await fs.syncFile(currentJanDataFolder, destinationPath) - if (err) throw err - await window.core?.api?.updateAppConfiguration(appConfiguration) - console.debug( - `File sync finished from ${currentJanDataFolder} to ${destinationPath}` - ) + await window.core?.api?.changeDataFolder(destinationPath) localStorage.setItem(SUCCESS_SET_NEW_DESTINATION, 'true') setTimeout(() => { setShowLoader(false) diff --git a/web/screens/Settings/Appearance/index.tsx b/web/screens/Settings/Appearance/index.tsx index e91709f5b..279e0a816 100644 --- a/web/screens/Settings/Appearance/index.tsx +++ b/web/screens/Settings/Appearance/index.tsx @@ -3,13 +3,14 @@ import { useCallback } from 'react' import { useTheme } from 'next-themes' import { fs, joinPath } from '@janhq/core' -import { Button, Select } from '@janhq/joi' +import { Button, Select, Switch } from '@janhq/joi' import { useAtom, useAtomValue } from 'jotai' import { janThemesPathAtom, reduceTransparentAtom, selectedThemeIdAtom, + spellCheckAtom, themeDataAtom, themesOptionsAtom, } from '@/helpers/atoms/Setting.atom' @@ -23,6 +24,7 @@ export default function AppearanceOptions() { const [reduceTransparent, setReduceTransparent] = useAtom( reduceTransparentAtom ) + const [spellCheck, setSpellCheck] = useAtom(spellCheckAtom) const handleClickTheme = useCallback( async (e: string) => { @@ -55,7 +57,7 @@ export default function AppearanceOptions() {
Appearance

- Select of customize your interface color scheme + Select a color theme

+ + +
+
+ { className="ml-2 flex-shrink-0 text-[hsl(var(--text-secondary))]" /> } - content="Vector Database is crucial for efficient storage - and retrieval of embeddings. Consider your - specific task, available resources, and language - requirements. Experiment to find the best fit for - your specific use case." + content="Time-Weighted Retriever looks at how similar + they are and how new they are. It compares + documents based on their meaning like usual, but + also considers when they were added to give + newer ones more importance." />
- -
- -