diff --git a/core/src/api/index.ts b/core/src/api/index.ts index a3d0361e7..a232c4090 100644 --- a/core/src/api/index.ts +++ b/core/src/api/index.ts @@ -62,6 +62,7 @@ export enum FileManagerRoute { getJanDataFolderPath = 'getJanDataFolderPath', getResourcePath = 'getResourcePath', fileStat = 'fileStat', + writeBlob = 'writeBlob', } export type ApiFunction = (...args: any[]) => any diff --git a/core/src/fs.ts b/core/src/fs.ts index ea636977a..0e570d1f5 100644 --- a/core/src/fs.ts +++ b/core/src/fs.ts @@ -1,4 +1,4 @@ -import { FileStat } from "./types" +import { FileStat } from './types' /** * Writes data to a file at the specified path. @@ -6,6 +6,15 @@ import { FileStat } from "./types" */ const writeFileSync = (...args: any[]) => global.core.api?.writeFileSync(...args) +/** + * Writes blob data to a file at the specified path. + * @param path - The path to file. + * @param data - The blob data. + * @returns + */ +const writeBlob: (path: string, data: string) => Promise = (path, data) => + global.core.api?.writeBlob(path, data) + /** * Reads the contents of a file at the specified path. * @returns {Promise} A Promise that resolves with the contents of the file. @@ -60,7 +69,6 @@ const syncFile: (src: string, dest: string) => Promise = (src, dest) => */ const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args) - /** * Gets the file's stats. * @@ -70,7 +78,6 @@ const copyFileSync = (...args: any[]) => global.core.api?.copyFileSync(...args) const fileStat: (path: string) => Promise = (path) => global.core.api?.fileStat(path) - // TODO: Export `dummy` fs functions automatically // Currently adding these manually export const fs = { @@ -84,5 +91,6 @@ export const fs = { appendFileSync, copyFileSync, syncFile, - fileStat + fileStat, + writeBlob, } diff --git a/core/src/node/api/routes/fs.ts b/core/src/node/api/routes/fs.ts index 5f511af27..c5404ccce 100644 --- a/core/src/node/api/routes/fs.ts +++ b/core/src/node/api/routes/fs.ts @@ -2,6 +2,7 @@ import { FileSystemRoute } from '../../../api' import { join } from 'path' import { HttpServer } from '../HttpServer' import { getJanDataFolderPath } from '../../utils' +import { normalizeFilePath } from '../../path' export const fsRouter = async (app: HttpServer) => { const moduleName = 'fs' @@ -13,10 +14,10 @@ export const fsRouter = async (app: HttpServer) => { const result = await import(moduleName).then((mdl) => { return mdl[route]( ...body.map((arg: any) => - typeof arg === 'string' && arg.includes('file:/') - ? join(getJanDataFolderPath(), arg.replace('file:/', '')) - : arg, - ), + typeof arg === 'string' && (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) + ? join(getJanDataFolderPath(), normalizeFilePath(arg)) + : arg + ) ) }) res.status(200).send(result) diff --git a/core/src/types/assistant/assistantEntity.ts b/core/src/types/assistant/assistantEntity.ts index 91bb2bb22..733dbea8d 100644 --- a/core/src/types/assistant/assistantEntity.ts +++ b/core/src/types/assistant/assistantEntity.ts @@ -2,6 +2,13 @@ * Assistant type defines the shape of an assistant object. * @stored */ + +export type AssistantTool = { + type: string + enabled: boolean + settings: any +} + export type Assistant = { /** Represents the avatar of the user. */ avatar: string @@ -22,7 +29,7 @@ export type Assistant = { /** Represents the instructions for the object. */ instructions?: string /** Represents the tools associated with the object. */ - tools?: any + tools?: AssistantTool[] /** Represents the file identifiers associated with the object. */ file_ids: string[] /** Represents the metadata of the object. */ diff --git a/core/src/types/inference/inferenceEntity.ts b/core/src/types/inference/inferenceEntity.ts index 58b838ae7..c37e3b079 100644 --- a/core/src/types/inference/inferenceEntity.ts +++ b/core/src/types/inference/inferenceEntity.ts @@ -1,3 +1,5 @@ +import { ContentType, ContentValue } from '../message' + /** * The role of the author of this message. */ @@ -13,7 +15,32 @@ export enum ChatCompletionRole { */ export type ChatCompletionMessage = { /** The contents of the message. **/ - content?: string + content?: ChatCompletionMessageContent /** The role of the author of this message. **/ role: ChatCompletionRole } + +export type ChatCompletionMessageContent = + | string + | (ChatCompletionMessageContentText & + ChatCompletionMessageContentImage & + ChatCompletionMessageContentDoc)[] + +export enum ChatCompletionMessageContentType { + Text = 'text', + Image = 'image_url', + Doc = 'doc_url', +} + +export type ChatCompletionMessageContentText = { + type: ChatCompletionMessageContentType + text: string +} +export type ChatCompletionMessageContentImage = { + type: ChatCompletionMessageContentType + image_url: { url: string } +} +export type ChatCompletionMessageContentDoc = { + type: ChatCompletionMessageContentType + doc_url: { url: string } +} diff --git a/core/src/types/message/messageEntity.ts b/core/src/types/message/messageEntity.ts index 199743796..87e4b1997 100644 --- a/core/src/types/message/messageEntity.ts +++ b/core/src/types/message/messageEntity.ts @@ -1,5 +1,6 @@ import { ChatCompletionMessage, ChatCompletionRole } from '../inference' import { ModelInfo } from '../model' +import { Thread } from '../thread' /** * The `ThreadMessage` type defines the shape of a thread's message object. @@ -35,7 +36,10 @@ export type ThreadMessage = { export type MessageRequest = { id?: string - /** The thread id of the message request. **/ + /** + * @deprecated Use thread object instead + * The thread id of the message request. + */ threadId: string /** @@ -48,6 +52,10 @@ export type MessageRequest = { /** Settings for constructing a chat completion request **/ model?: ModelInfo + + /** The thread of this message is belong to. **/ + // TODO: deprecate threadId field + thread?: Thread } /** @@ -62,7 +70,7 @@ export enum MessageStatus { /** Message loaded with error. **/ Error = 'error', /** Message is cancelled streaming */ - Stopped = "stopped" + Stopped = 'stopped', } /** @@ -71,6 +79,7 @@ export enum MessageStatus { export enum ContentType { Text = 'text', Image = 'image', + Pdf = 'pdf', } /** @@ -80,6 +89,8 @@ export enum ContentType { export type ContentValue = { value: string annotations: string[] + name?: string + size?: number } /** diff --git a/core/src/types/model/modelEntity.ts b/core/src/types/model/modelEntity.ts index c60ab7650..727ff085f 100644 --- a/core/src/types/model/modelEntity.ts +++ b/core/src/types/model/modelEntity.ts @@ -7,6 +7,7 @@ export type ModelInfo = { settings: ModelSettingParams parameters: ModelRuntimeParams engine?: InferenceEngine + proxyEngine?: InferenceEngine } /** @@ -18,7 +19,8 @@ export enum InferenceEngine { nitro = 'nitro', openai = 'openai', triton_trtllm = 'triton_trtllm', - hf_endpoint = 'hf_endpoint', + + tool_retrieval_enabled = 'tool_retrieval_enabled', } export type ModelArtifact = { @@ -90,6 +92,13 @@ export type Model = { * The model engine. */ engine: InferenceEngine + + proxyEngine?: InferenceEngine + + /** + * Is multimodal or not. + */ + visionModel?: boolean } export type ModelMetadata = { @@ -129,4 +138,5 @@ export type ModelRuntimeParams = { stop?: string[] frequency_penalty?: number presence_penalty?: number + engine?: string } diff --git a/core/src/types/thread/index.ts b/core/src/types/thread/index.ts index c6ff6204a..32155e1cd 100644 --- a/core/src/types/thread/index.ts +++ b/core/src/types/thread/index.ts @@ -1,2 +1,3 @@ export * from './threadEntity' export * from './threadInterface' +export * from './threadEvent' diff --git a/core/src/types/thread/threadEntity.ts b/core/src/types/thread/threadEntity.ts index 4ff3aa1fc..37136eae6 100644 --- a/core/src/types/thread/threadEntity.ts +++ b/core/src/types/thread/threadEntity.ts @@ -1,3 +1,4 @@ +import { AssistantTool } from '../assistant' import { ModelInfo } from '../model' /** @@ -30,6 +31,7 @@ export type ThreadAssistantInfo = { assistant_name: string model: ModelInfo instructions?: string + tools?: AssistantTool[] } /** diff --git a/core/src/types/thread/threadEvent.ts b/core/src/types/thread/threadEvent.ts new file mode 100644 index 000000000..4b19b09c1 --- /dev/null +++ b/core/src/types/thread/threadEvent.ts @@ -0,0 +1,4 @@ +export enum ThreadEvent { + /** The `OnThreadStarted` event is emitted when a thread is started. */ + OnThreadStarted = 'OnThreadStarted', +} diff --git a/electron/handlers/fileManager.ts b/electron/handlers/fileManager.ts index f41286934..2528aef71 100644 --- a/electron/handlers/fileManager.ts +++ b/electron/handlers/fileManager.ts @@ -59,4 +59,20 @@ export function handleFileMangerIPCs() { return fileStat } ) + + ipcMain.handle( + FileManagerRoute.writeBlob, + async (_event, path: string, data: string): Promise => { + try { + const normalizedPath = normalizeFilePath(path) + const dataBuffer = Buffer.from(data, 'base64') + fs.writeFileSync( + join(getJanDataFolderPath(), normalizedPath), + dataBuffer + ) + } catch (err) { + console.error(`writeFile ${path} result: ${err}`) + } + } + ) } diff --git a/electron/handlers/fs.ts b/electron/handlers/fs.ts index 408a5fd10..34026b940 100644 --- a/electron/handlers/fs.ts +++ b/electron/handlers/fs.ts @@ -1,9 +1,9 @@ import { ipcMain } from 'electron' -import { FileSystemRoute } from '@janhq/core' -import { join } from 'path' import { getJanDataFolderPath, normalizeFilePath } from '@janhq/core/node' - +import fs from 'fs' +import { FileManagerRoute, FileSystemRoute } from '@janhq/core' +import { join } from 'path' /** * Handles file system operations. */ @@ -15,7 +15,7 @@ export function handleFsIPCs() { mdl[route]( ...args.map((arg) => typeof arg === 'string' && - (arg.includes(`file:/`) || arg.includes(`file:\\`)) + (arg.startsWith(`file:/`) || arg.startsWith(`file:\\`)) ? join(getJanDataFolderPath(), normalizeFilePath(arg)) : arg ) diff --git a/extensions/assistant-extension/package.json b/extensions/assistant-extension/package.json index 4e84aa573..f4e4dd825 100644 --- a/extensions/assistant-extension/package.json +++ b/extensions/assistant-extension/package.json @@ -3,26 +3,46 @@ "version": "1.0.0", "description": "This extension enables assistants, including Jan, a default assistant that can call all downloaded models", "main": "dist/index.js", - "module": "dist/module.js", + "node": "dist/node/index.js", "author": "Jan ", "license": "AGPL-3.0", "scripts": { - "build": "tsc -b . && webpack --config webpack.config.js", + "build": "tsc --module commonjs && rollup -c rollup.config.ts", "build:publish": "rimraf *.tgz --glob && npm run build && npm pack && cpx *.tgz ../../electron/pre-install" }, "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.5", + "@types/pdf-parse": "^1.1.4", + "cpx": "^1.5.0", "rimraf": "^3.0.2", - "webpack": "^5.88.2", - "webpack-cli": "^5.1.4" + "rollup": "^2.38.5", + "rollup-plugin-define": "^1.0.1", + "rollup-plugin-sourcemaps": "^0.6.3", + "rollup-plugin-typescript2": "^0.36.0", + "typescript": "^5.3.3" }, "dependencies": { "@janhq/core": "file:../../core", + "@langchain/community": "0.0.13", + "hnswlib-node": "^1.4.2", + "langchain": "^0.0.214", "path-browserify": "^1.0.1", + "pdf-parse": "^1.1.1", "ts-loader": "^9.5.0" }, "files": [ "dist/*", "package.json", "README.md" + ], + "bundleDependencies": [ + "@janhq/core", + "@langchain/community", + "hnswlib-node", + "langchain", + "pdf-parse" ] } diff --git a/extensions/assistant-extension/rollup.config.ts b/extensions/assistant-extension/rollup.config.ts new file mode 100644 index 000000000..7916ef9c8 --- /dev/null +++ b/extensions/assistant-extension/rollup.config.ts @@ -0,0 +1,81 @@ +import resolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import sourceMaps from "rollup-plugin-sourcemaps"; +import typescript from "rollup-plugin-typescript2"; +import json from "@rollup/plugin-json"; +import replace from "@rollup/plugin-replace"; + +const packageJson = require("./package.json"); + +const pkg = require("./package.json"); + +export default [ + { + input: `src/index.ts`, + output: [{ file: pkg.main, format: "es", sourcemap: true }], + // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') + external: [], + watch: { + include: "src/**", + }, + plugins: [ + replace({ + NODE: JSON.stringify(`${packageJson.name}/${packageJson.node}`), + EXTENSION_NAME: JSON.stringify(packageJson.name), + VERSION: JSON.stringify(packageJson.version), + }), + // Allow json resolution + json(), + // Compile TypeScript files + typescript({ useTsconfigDeclarationDir: true }), + // Compile TypeScript files + // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) + commonjs(), + // Allow node_modules resolution, so you can use 'external' to control + // which external modules to include in the bundle + // https://github.com/rollup/rollup-plugin-node-resolve#usage + resolve({ + extensions: [".js", ".ts", ".svelte"], + }), + + // Resolve source maps to the original source + sourceMaps(), + ], + }, + { + input: `src/node/index.ts`, + output: [{ dir: "dist/node", format: "cjs", sourcemap: false }], + // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') + external: [ + "@janhq/core/node", + "@langchain/community", + "langchain", + "langsmith", + "path", + "hnswlib-node", + ], + watch: { + include: "src/node/**", + }, + // inlineDynamicImports: true, + plugins: [ + // Allow json resolution + json(), + // Compile TypeScript files + typescript({ useTsconfigDeclarationDir: true }), + // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) + commonjs({ + ignoreDynamicRequires: true, + }), + // Allow node_modules resolution, so you can use 'external' to control + // which external modules to include in the bundle + // https://github.com/rollup/rollup-plugin-node-resolve#usage + resolve({ + extensions: [".ts", ".js", ".json"], + }), + + // Resolve source maps to the original source + // sourceMaps(), + ], + }, +]; diff --git a/extensions/assistant-extension/src/@types/global.d.ts b/extensions/assistant-extension/src/@types/global.d.ts index 3b45ccc5a..dc11709a4 100644 --- a/extensions/assistant-extension/src/@types/global.d.ts +++ b/extensions/assistant-extension/src/@types/global.d.ts @@ -1 +1,3 @@ -declare const MODULE: string; +declare const NODE: string; +declare const EXTENSION_NAME: string; +declare const VERSION: string; diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts index 098ab1f54..6495ea786 100644 --- a/extensions/assistant-extension/src/index.ts +++ b/extensions/assistant-extension/src/index.ts @@ -1,15 +1,151 @@ -import { fs, Assistant, AssistantExtension } from "@janhq/core"; -import { join } from "path"; +import { + fs, + Assistant, + MessageRequest, + events, + InferenceEngine, + MessageEvent, + InferenceEvent, + joinPath, + executeOnMain, + AssistantExtension, +} from "@janhq/core"; export default class JanAssistantExtension extends AssistantExtension { private static readonly _homeDir = "file://assistants"; + controller = new AbortController(); + isCancelled = false; + retrievalThreadId: string | undefined = undefined; + async onLoad() { // making the assistant directory - if (!(await fs.existsSync(JanAssistantExtension._homeDir))) - fs.mkdirSync(JanAssistantExtension._homeDir).then(() => { - this.createJanAssistant(); - }); + const assistantDirExist = await fs.existsSync( + JanAssistantExtension._homeDir, + ); + if ( + localStorage.getItem(`${EXTENSION_NAME}-version`) !== VERSION || + !assistantDirExist + ) { + if (!assistantDirExist) + await fs.mkdirSync(JanAssistantExtension._homeDir); + + // Write assistant metadata + this.createJanAssistant(); + // Finished migration + localStorage.setItem(`${EXTENSION_NAME}-version`, VERSION); + } + + // Events subscription + events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => + JanAssistantExtension.handleMessageRequest(data, this), + ); + + events.on(InferenceEvent.OnInferenceStopped, () => { + JanAssistantExtension.handleInferenceStopped(this); + }); + } + + private static async handleInferenceStopped(instance: JanAssistantExtension) { + instance.isCancelled = true; + instance.controller?.abort(); + } + + private static async handleMessageRequest( + data: MessageRequest, + instance: JanAssistantExtension, + ) { + instance.isCancelled = false; + instance.controller = new AbortController(); + + if ( + data.model?.engine !== InferenceEngine.tool_retrieval_enabled || + !data.messages || + !data.thread?.assistants[0]?.tools + ) { + return; + } + + const latestMessage = data.messages[data.messages.length - 1]; + + // Ingest the document if needed + if ( + latestMessage && + latestMessage.content && + typeof latestMessage.content !== "string" + ) { + const docFile = latestMessage.content[1]?.doc_url?.url; + if (docFile) { + await executeOnMain( + NODE, + "toolRetrievalIngestNewDocument", + docFile, + data.model?.proxyEngine, + ); + } + } + + // Load agent on thread changed + if (instance.retrievalThreadId !== data.threadId) { + await executeOnMain(NODE, "toolRetrievalLoadThreadMemory", data.threadId); + + instance.retrievalThreadId = data.threadId; + + // Update the text splitter + await executeOnMain( + NODE, + "toolRetrievalUpdateTextSplitter", + data.thread.assistants[0].tools[0]?.settings?.chunk_size ?? 4000, + data.thread.assistants[0].tools[0]?.settings?.chunk_overlap ?? 200, + ); + } + + if (latestMessage.content) { + const prompt = + typeof latestMessage.content === "string" + ? latestMessage.content + : latestMessage.content[0].text; + // Retrieve the result + console.debug("toolRetrievalQuery", latestMessage.content); + const retrievalResult = await executeOnMain( + NODE, + "toolRetrievalQueryResult", + prompt, + ); + + // Update the message content + // Using the retrieval template with the result and query + if (data.thread?.assistants[0].tools) + data.messages[data.messages.length - 1].content = + data.thread.assistants[0].tools[0].settings?.retrieval_template + ?.replace("{CONTEXT}", retrievalResult) + .replace("{QUESTION}", prompt); + } + + // Filter out all the messages that are not text + data.messages = data.messages.map((message) => { + if ( + message.content && + typeof message.content !== "string" && + (message.content.length ?? 0) > 0 + ) { + return { + ...message, + content: [message.content[0]], + }; + } + return message; + }); + + // Reroute the result to inference engine + const output = { + ...data, + model: { + ...data.model, + engine: data.model.proxyEngine, + }, + }; + events.emit(MessageEvent.OnMessageSent, output); } /** @@ -18,15 +154,21 @@ export default class JanAssistantExtension extends AssistantExtension { onUnload(): void {} async createAssistant(assistant: Assistant): Promise { - const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); + const assistantDir = await joinPath([ + JanAssistantExtension._homeDir, + assistant.id, + ]); if (!(await fs.existsSync(assistantDir))) await fs.mkdirSync(assistantDir); // store the assistant metadata json - const assistantMetadataPath = join(assistantDir, "assistant.json"); + const assistantMetadataPath = await joinPath([ + assistantDir, + "assistant.json", + ]); try { await fs.writeFileSync( assistantMetadataPath, - JSON.stringify(assistant, null, 2) + JSON.stringify(assistant, null, 2), ); } catch (err) { console.error(err); @@ -38,14 +180,17 @@ export default class JanAssistantExtension extends AssistantExtension { // get all the assistant metadata json const results: Assistant[] = []; const allFileName: string[] = await fs.readdirSync( - JanAssistantExtension._homeDir + JanAssistantExtension._homeDir, ); for (const fileName of allFileName) { - const filePath = join(JanAssistantExtension._homeDir, fileName); + const filePath = await joinPath([ + JanAssistantExtension._homeDir, + fileName, + ]); if (filePath.includes(".DS_Store")) continue; const jsonFiles: string[] = (await fs.readdirSync(filePath)).filter( - (file: string) => file === "assistant.json" + (file: string) => file === "assistant.json", ); if (jsonFiles.length !== 1) { @@ -54,8 +199,8 @@ export default class JanAssistantExtension extends AssistantExtension { } const content = await fs.readFileSync( - join(filePath, jsonFiles[0]), - "utf-8" + await joinPath([filePath, jsonFiles[0]]), + "utf-8", ); const assistant: Assistant = typeof content === "object" ? content : JSON.parse(content); @@ -72,7 +217,10 @@ export default class JanAssistantExtension extends AssistantExtension { } // remove the directory - const assistantDir = join(JanAssistantExtension._homeDir, assistant.id); + const assistantDir = await joinPath([ + JanAssistantExtension._homeDir, + assistant.id, + ]); await fs.rmdirSync(assistantDir); return Promise.resolve(); } @@ -88,7 +236,24 @@ export default class JanAssistantExtension extends AssistantExtension { description: "A default assistant that can use all downloaded models", model: "*", instructions: "", - tools: undefined, + tools: [ + { + type: "retrieval", + enabled: false, + settings: { + top_k: 2, + chunk_size: 1024, + chunk_overlap: 64, + retrieval_template: `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer. + ---------------- + CONTEXT: {CONTEXT} + ---------------- + QUESTION: {QUESTION} + ---------------- + Helpful Answer:`, + }, + }, + ], file_ids: [], metadata: undefined, }; diff --git a/extensions/assistant-extension/src/node/engine.ts b/extensions/assistant-extension/src/node/engine.ts new file mode 100644 index 000000000..54b2a6ba1 --- /dev/null +++ b/extensions/assistant-extension/src/node/engine.ts @@ -0,0 +1,13 @@ +import fs from "fs"; +import path from "path"; +import { getJanDataFolderPath } from "@janhq/core/node"; + +// Sec: Do not send engine settings over requests +// Read it manually instead +export const readEmbeddingEngine = (engineName: string) => { + const engineSettings = fs.readFileSync( + path.join(getJanDataFolderPath(), "engines", `${engineName}.json`), + "utf-8", + ); + return JSON.parse(engineSettings); +}; diff --git a/extensions/assistant-extension/src/node/index.ts b/extensions/assistant-extension/src/node/index.ts new file mode 100644 index 000000000..95a7243a4 --- /dev/null +++ b/extensions/assistant-extension/src/node/index.ts @@ -0,0 +1,39 @@ +import { getJanDataFolderPath, normalizeFilePath } from "@janhq/core/node"; +import { Retrieval } from "./tools/retrieval"; +import path from "path"; + +const retrieval = new Retrieval(); + +export async function toolRetrievalUpdateTextSplitter( + chunkSize: number, + chunkOverlap: number, +) { + retrieval.updateTextSplitter(chunkSize, chunkOverlap); + return Promise.resolve(); +} +export async function toolRetrievalIngestNewDocument( + file: string, + engine: string, +) { + const filePath = path.join(getJanDataFolderPath(), normalizeFilePath(file)); + const threadPath = path.dirname(filePath.replace("files", "")); + retrieval.updateEmbeddingEngine(engine); + await retrieval.ingestAgentKnowledge(filePath, `${threadPath}/memory`); + return Promise.resolve(); +} + +export async function toolRetrievalLoadThreadMemory(threadId: string) { + try { + await retrieval.loadRetrievalAgent( + path.join(getJanDataFolderPath(), "threads", threadId, "memory"), + ); + return Promise.resolve(); + } catch (err) { + console.debug(err); + } +} + +export async function toolRetrievalQueryResult(query: string) { + const res = await retrieval.generateResult(query); + return Promise.resolve(res); +} diff --git a/extensions/assistant-extension/src/node/tools/retrieval/index.ts b/extensions/assistant-extension/src/node/tools/retrieval/index.ts new file mode 100644 index 000000000..f9d5c4029 --- /dev/null +++ b/extensions/assistant-extension/src/node/tools/retrieval/index.ts @@ -0,0 +1,78 @@ +import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; +import { formatDocumentsAsString } from "langchain/util/document"; +import { PDFLoader } from "langchain/document_loaders/fs/pdf"; + +import { HNSWLib } from "langchain/vectorstores/hnswlib"; + +import { OpenAIEmbeddings } from "langchain/embeddings/openai"; +import { readEmbeddingEngine } from "../../engine"; + +export class Retrieval { + public chunkSize: number = 100; + public chunkOverlap?: number = 0; + private retriever: any; + + private embeddingModel: any = undefined; + private textSplitter?: RecursiveCharacterTextSplitter; + + constructor(chunkSize: number = 4000, chunkOverlap: number = 200) { + this.updateTextSplitter(chunkSize, chunkOverlap); + this.embeddingModel = new OpenAIEmbeddings({}); + } + + public updateTextSplitter(chunkSize: number, chunkOverlap: number): void { + this.chunkSize = chunkSize; + this.chunkOverlap = chunkOverlap; + this.textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: chunkSize, + chunkOverlap: chunkOverlap, + }); + } + + public updateEmbeddingEngine(engine: string): void { + // Engine settings are not compatible with the current embedding model params + // Switch case manually for now + const settings = readEmbeddingEngine(engine); + if (engine === "nitro") { + this.embeddingModel = new OpenAIEmbeddings( + { openAIApiKey: "nitro-embedding" }, + { basePath: "http://127.0.0.1:3928/v1" }, + ); + } else { + // Fallback to OpenAI Settings + this.embeddingModel = new OpenAIEmbeddings({ + configuration: { + apiKey: settings.api_key, + }, + }); + } + } + + public ingestAgentKnowledge = async ( + filePath: string, + memoryPath: string, + ): Promise => { + const loader = new PDFLoader(filePath, { + splitPages: true, + }); + const doc = await loader.load(); + const docs = await this.textSplitter!.splitDocuments(doc); + const vectorStore = await HNSWLib.fromDocuments(docs, this.embeddingModel); + return vectorStore.save(memoryPath); + }; + + public loadRetrievalAgent = async (memoryPath: string): Promise => { + const vectorStore = await HNSWLib.load(memoryPath, this.embeddingModel); + this.retriever = vectorStore.asRetriever(2); + return Promise.resolve(); + }; + + public generateResult = async (query: string): Promise => { + if (!this.retriever) { + return Promise.resolve(" "); + } + const relevantDocs = await this.retriever.getRelevantDocuments(query); + const serializedDoc = formatDocumentsAsString(relevantDocs); + return Promise.resolve(serializedDoc); + }; +} diff --git a/extensions/assistant-extension/tsconfig.json b/extensions/assistant-extension/tsconfig.json index 2477d58ce..d3794cace 100644 --- a/extensions/assistant-extension/tsconfig.json +++ b/extensions/assistant-extension/tsconfig.json @@ -1,14 +1,20 @@ { "compilerOptions": { - "target": "es2016", - "module": "ES6", "moduleResolution": "node", - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": false, + "target": "es5", + "module": "ES2020", + "lib": ["es2015", "es2016", "es2017", "dom"], + "strict": true, + "sourceMap": true, + "declaration": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declarationDir": "dist/types", + "outDir": "dist", + "importHelpers": true, + "typeRoots": ["node_modules/@types"], "skipLibCheck": true, - "rootDir": "./src" }, - "include": ["./src"] + "include": ["src"], } diff --git a/extensions/assistant-extension/webpack.config.js b/extensions/assistant-extension/webpack.config.js deleted file mode 100644 index 74d16fc8e..000000000 --- a/extensions/assistant-extension/webpack.config.js +++ /dev/null @@ -1,38 +0,0 @@ -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/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index 66becb748..61f0fd0e9 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -4,15 +4,14 @@ import { ConversationalExtension, Thread, ThreadMessage, + events, } from '@janhq/core' /** * JSONConversationalExtension is a ConversationalExtension implementation that provides * functionality for managing threads. */ -export default class JSONConversationalExtension - extends ConversationalExtension -{ +export default class JSONConversationalExtension extends ConversationalExtension { private static readonly _homeDir = 'file://threads' private static readonly _threadInfoFileName = 'thread.json' private static readonly _threadMessagesFileName = 'messages.jsonl' @@ -119,6 +118,32 @@ export default class JSONConversationalExtension ]) if (!(await fs.existsSync(threadDirPath))) await fs.mkdirSync(threadDirPath) + + if (message.content[0].type === 'image') { + const filesPath = await joinPath([threadDirPath, 'files']) + if (!(await fs.existsSync(filesPath))) await fs.mkdirSync(filesPath) + + const imagePath = await joinPath([filesPath, `${message.id}.png`]) + const base64 = message.content[0].text.annotations[0] + await this.storeImage(base64, imagePath) + // if (fs.existsSync(imagePath)) { + // message.content[0].text.annotations[0] = imagePath + // } + } + + if (message.content[0].type === 'pdf') { + const filesPath = await joinPath([threadDirPath, 'files']) + if (!(await fs.existsSync(filesPath))) await fs.mkdirSync(filesPath) + + const filePath = await joinPath([filesPath, `${message.id}.pdf`]) + const blob = message.content[0].text.annotations[0] + await this.storeFile(blob, filePath) + + if (await fs.existsSync(filePath)) { + // Use file path instead of blob + message.content[0].text.annotations[0] = `threads/${message.thread_id}/files/${message.id}.pdf` + } + } await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n') Promise.resolve() } catch (err) { @@ -126,6 +151,25 @@ export default class JSONConversationalExtension } } + async storeImage(base64: string, filePath: string): Promise { + const base64Data = base64.replace(/^data:image\/\w+;base64,/, '') + + try { + await fs.writeBlob(filePath, base64Data) + } catch (err) { + console.error(err) + } + } + + async storeFile(base64: string, filePath: string): Promise { + const base64Data = base64.replace(/^data:application\/pdf;base64,/, '') + try { + await fs.writeBlob(filePath, base64Data) + } catch (err) { + console.error(err) + } + } + async writeMessages( threadId: string, messages: ThreadMessage[] @@ -229,7 +273,11 @@ export default class JSONConversationalExtension const messages: ThreadMessage[] = [] result.forEach((line: string) => { - messages.push(JSON.parse(line) as ThreadMessage) + try { + messages.push(JSON.parse(line) as ThreadMessage) + } catch (err) { + console.error(err) + } }) return messages } catch (err) { diff --git a/extensions/inference-nitro-extension/bin/version.txt b/extensions/inference-nitro-extension/bin/version.txt index f2722b133..769ed6ae7 100644 --- a/extensions/inference-nitro-extension/bin/version.txt +++ b/extensions/inference-nitro-extension/bin/version.txt @@ -1 +1 @@ -0.2.12 +0.2.14 diff --git a/extensions/inference-nitro-extension/package.json b/extensions/inference-nitro-extension/package.json index 9379e194b..44727eb70 100644 --- a/extensions/inference-nitro-extension/package.json +++ b/extensions/inference-nitro-extension/package.json @@ -40,6 +40,7 @@ "dependencies": { "@janhq/core": "file:../../core", "@rollup/plugin-replace": "^5.0.5", + "@types/os-utils": "^0.0.4", "fetch-retry": "^5.0.6", "path-browserify": "^1.0.1", "rxjs": "^7.8.1", diff --git a/extensions/inference-nitro-extension/src/index.ts b/extensions/inference-nitro-extension/src/index.ts index 735383a61..0e6edb992 100644 --- a/extensions/inference-nitro-extension/src/index.ts +++ b/extensions/inference-nitro-extension/src/index.ts @@ -50,7 +50,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { ngl: 100, cpu_threads: 1, cont_batching: false, - embedding: false, + embedding: true, }; controller = new AbortController(); @@ -83,19 +83,19 @@ export default class JanInferenceNitroExtension extends InferenceExtension { // Events subscription events.on(MessageEvent.OnMessageSent, (data: MessageRequest) => - this.onMessageRequest(data) + this.onMessageRequest(data), ); events.on(ModelEvent.OnModelInit, (model: Model) => - this.onModelInit(model) + this.onModelInit(model), ); events.on(ModelEvent.OnModelStop, (model: Model) => - this.onModelStop(model) + this.onModelStop(model), ); events.on(InferenceEvent.OnInferenceStopped, () => - this.onInferenceStopped() + this.onInferenceStopped(), ); // Attempt to fetch nvidia info @@ -120,7 +120,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { } else { await fs.writeFileSync( engineFile, - JSON.stringify(this._engineSettings, null, 2) + JSON.stringify(this._engineSettings, null, 2), ); } } catch (err) { @@ -148,7 +148,7 @@ export default class JanInferenceNitroExtension extends InferenceExtension { this.getNitroProcesHealthIntervalId = setInterval( () => this.periodicallyGetNitroHealth(), - JanInferenceNitroExtension._intervalHealthCheck + JanInferenceNitroExtension._intervalHealthCheck, ); } diff --git a/extensions/inference-nitro-extension/src/node/index.ts b/extensions/inference-nitro-extension/src/node/index.ts index a75f33df2..77060e414 100644 --- a/extensions/inference-nitro-extension/src/node/index.ts +++ b/extensions/inference-nitro-extension/src/node/index.ts @@ -78,7 +78,7 @@ function stopModel(): Promise { * TODO: Should pass absolute of the model file instead of just the name - So we can modurize the module.ts to npm package */ async function runModel( - wrapper: ModelInitOptions + wrapper: ModelInitOptions, ): Promise { if (wrapper.model.engine !== InferenceEngine.nitro) { // Not a nitro model @@ -96,7 +96,7 @@ async function runModel( const ggufBinFile = files.find( (file) => file === path.basename(currentModelFile) || - file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT) + file.toLowerCase().includes(SUPPORTED_MODEL_FORMAT), ); if (!ggufBinFile) return Promise.reject("No GGUF model file found"); @@ -133,7 +133,6 @@ async function runModel( mmproj: path.join(modelFolderPath, wrapper.model.settings.mmproj), }), }; - console.log(currentSettings); return runNitroAndLoadModel(); } } @@ -192,10 +191,10 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const system_prompt = promptTemplate.substring(0, systemIndex); const user_prompt = promptTemplate.substring( systemIndex + systemMarker.length, - promptIndex + promptIndex, ); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length + promptIndex + promptMarker.length, ); // Return the split parts @@ -205,7 +204,7 @@ function promptTemplateConverter(promptTemplate: string): PromptTemplate { const promptIndex = promptTemplate.indexOf(promptMarker); const user_prompt = promptTemplate.substring(0, promptIndex); const ai_prompt = promptTemplate.substring( - promptIndex + promptMarker.length + promptIndex + promptMarker.length, ); // Return the split parts @@ -234,8 +233,8 @@ function loadLLMModel(settings: any): Promise { .then((res) => { log( `[NITRO]::Debug: Load model success with response ${JSON.stringify( - res - )}` + res, + )}`, ); return Promise.resolve(res); }) @@ -264,8 +263,8 @@ async function validateModelStatus(): Promise { }).then(async (res: Response) => { log( `[NITRO]::Debug: Validate model state success with response ${JSON.stringify( - res - )}` + res, + )}`, ); // If the response is OK, check model_loaded status. if (res.ok) { @@ -316,7 +315,7 @@ function spawnNitroProcess(): Promise { const args: string[] = ["1", LOCAL_HOST, PORT.toString()]; // Execute the binary log( - `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}` + `[NITRO]::Debug: Spawn nitro at path: ${executableOptions.executablePath}, and args: ${args}`, ); subprocess = spawn( executableOptions.executablePath, @@ -327,7 +326,7 @@ function spawnNitroProcess(): Promise { ...process.env, CUDA_VISIBLE_DEVICES: executableOptions.cudaVisibleDevices, }, - } + }, ); // Handle subprocess output diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts index 54572041d..44525b631 100644 --- a/extensions/inference-openai-extension/src/index.ts +++ b/extensions/inference-openai-extension/src/index.ts @@ -15,6 +15,7 @@ import { ThreadMessage, events, fs, + InferenceEngine, BaseExtension, MessageEvent, ModelEvent, @@ -57,7 +58,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { // Events subscription events.on(MessageEvent.OnMessageSent, (data) => - JanInferenceOpenAIExtension.handleMessageRequest(data, this) + JanInferenceOpenAIExtension.handleMessageRequest(data, this), ); events.on(ModelEvent.OnModelInit, (model: OpenAIModel) => { @@ -81,7 +82,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { try { const engineFile = join( JanInferenceOpenAIExtension._homeDir, - JanInferenceOpenAIExtension._engineMetadataFileName + JanInferenceOpenAIExtension._engineMetadataFileName, ); if (await fs.existsSync(engineFile)) { const engine = await fs.readFileSync(engineFile, "utf-8"); @@ -90,7 +91,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { } else { await fs.writeFileSync( engineFile, - JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2) + JSON.stringify(JanInferenceOpenAIExtension._engineSettings, null, 2), ); } } catch (err) { @@ -98,7 +99,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { } } private static async handleModelInit(model: OpenAIModel) { - if (model.engine !== "openai") { + if (model.engine !== InferenceEngine.openai) { return; } else { JanInferenceOpenAIExtension._currentModel = model; @@ -116,7 +117,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { } private static async handleInferenceStopped( - instance: JanInferenceOpenAIExtension + instance: JanInferenceOpenAIExtension, ) { instance.isCancelled = true; instance.controller?.abort(); @@ -130,7 +131,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { */ private static async handleMessageRequest( data: MessageRequest, - instance: JanInferenceOpenAIExtension + instance: JanInferenceOpenAIExtension, ) { if (data.model.engine !== "openai") { return; @@ -160,7 +161,7 @@ export default class JanInferenceOpenAIExtension extends BaseExtension { ...JanInferenceOpenAIExtension._currentModel, parameters: data.model.parameters, }, - instance.controller + instance.controller, ).subscribe({ next: (content) => { const messageContent: ThreadContent = { diff --git a/extensions/inference-openai-extension/tsconfig.json b/extensions/inference-openai-extension/tsconfig.json index b48175a16..7bfdd9009 100644 --- a/extensions/inference-openai-extension/tsconfig.json +++ b/extensions/inference-openai-extension/tsconfig.json @@ -3,13 +3,12 @@ "target": "es2016", "module": "ES6", "moduleResolution": "node", - "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, "skipLibCheck": true, - "rootDir": "./src" + "rootDir": "./src", }, - "include": ["./src"] + "include": ["./src"], } diff --git a/extensions/inference-triton-trtllm-extension/tsconfig.json b/extensions/inference-triton-trtllm-extension/tsconfig.json index b48175a16..7bfdd9009 100644 --- a/extensions/inference-triton-trtllm-extension/tsconfig.json +++ b/extensions/inference-triton-trtllm-extension/tsconfig.json @@ -3,13 +3,12 @@ "target": "es2016", "module": "ES6", "moduleResolution": "node", - "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": false, "skipLibCheck": true, - "rootDir": "./src" + "rootDir": "./src", }, - "include": ["./src"] + "include": ["./src"], } diff --git a/web/containers/CardSidebar/index.tsx b/web/containers/CardSidebar/index.tsx index bc5047497..c0dd19ba5 100644 --- a/web/containers/CardSidebar/index.tsx +++ b/web/containers/CardSidebar/index.tsx @@ -17,6 +17,7 @@ import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' interface Props { children: ReactNode + rightAction?: ReactNode title: string asChild?: boolean hideMoreVerticalAction?: boolean @@ -25,6 +26,7 @@ export default function CardSidebar({ children, title, asChild, + rightAction, hideMoreVerticalAction, }: Props) { const [show, setShow] = useState(true) @@ -53,27 +55,16 @@ export default function CardSidebar({
- {title} -
- {!asChild && ( - <> - {!hideMoreVerticalAction && ( -
setMore(!more)} - > - -
- )} - - )} +
+ {title} +
+
+ {rightAction && rightAction} + {!asChild && ( + <> + {!hideMoreVerticalAction && ( +
setMore(!more)} + > + +
+ )} + + )}
{more && ( diff --git a/web/containers/Checkbox/index.tsx b/web/containers/Checkbox/index.tsx index e8f916d98..a545771b6 100644 --- a/web/containers/Checkbox/index.tsx +++ b/web/containers/Checkbox/index.tsx @@ -9,54 +9,26 @@ import { TooltipTrigger, } from '@janhq/uikit' -import { useAtomValue, useSetAtom } from 'jotai' import { InfoIcon } from 'lucide-react' -import { useActiveModel } from '@/hooks/useActiveModel' -import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' - -import { getConfigurationsData } from '@/utils/componentSettings' -import { toSettingParams } from '@/utils/modelParam' - -import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' -import { - engineParamsUpdateAtom, - getActiveThreadIdAtom, - getActiveThreadModelParamsAtom, -} from '@/helpers/atoms/Thread.atom' - type Props = { name: string title: string + enabled?: boolean description: string checked: boolean + onValueChanged?: (e: string | number | boolean) => void } -const Checkbox: React.FC = ({ name, title, checked, description }) => { - const { updateModelParameter } = useUpdateModelParameters() - const threadId = useAtomValue(getActiveThreadIdAtom) - - const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) - - const modelSettingParams = toSettingParams(activeModelParams) - - const engineParams = getConfigurationsData(modelSettingParams) - - const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom) - - const serverEnabled = useAtomValue(serverEnabledAtom) - - const { stopModel } = useActiveModel() - +const Checkbox: React.FC = ({ + title, + checked, + enabled = true, + description, + onValueChanged, +}) => { const onCheckedChange = (checked: boolean) => { - if (!threadId) return - if (engineParams.some((x) => x.name.includes(name))) { - setEngineParamsUpdate(true) - stopModel() - } else { - setEngineParamsUpdate(false) - } - updateModelParameter(threadId, name, checked) + onValueChanged?.(checked) } return ( @@ -80,7 +52,7 @@ const Checkbox: React.FC = ({ name, title, checked, description }) => {
) diff --git a/web/containers/Layout/TopBar/index.tsx b/web/containers/Layout/TopBar/index.tsx index ac05e4e1a..ab67cb3b7 100644 --- a/web/containers/Layout/TopBar/index.tsx +++ b/web/containers/Layout/TopBar/index.tsx @@ -120,13 +120,13 @@ const TopBar = () => {
-
- {activeThread && ( + {activeThread && ( +
{showing && (
@@ -227,8 +227,8 @@ const TopBar = () => { />
- )} -
+
+ )} )} diff --git a/web/containers/Loader/GenerateResponse.tsx b/web/containers/Loader/GenerateResponse.tsx new file mode 100644 index 000000000..457c44987 --- /dev/null +++ b/web/containers/Loader/GenerateResponse.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useState } from 'react' + +export default function GenerateResponse() { + const [loader, setLoader] = useState(0) + + // This is fake loader please fix this when we have realtime percentage when load model + useEffect(() => { + if (loader === 24) { + setTimeout(() => { + setLoader(loader + 1) + }, 250) + } else if (loader === 50) { + setTimeout(() => { + setLoader(loader + 1) + }, 250) + } else if (loader === 78) { + setTimeout(() => { + setLoader(loader + 1) + }, 250) + } else if (loader === 85) { + setLoader(85) + } else { + setLoader(loader + 1) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [loader]) + + return ( +
+
+
+ Generating response... +
+
+ ) +} diff --git a/web/containers/ModelConfigInput/index.tsx b/web/containers/ModelConfigInput/index.tsx index e409fd424..d573a0bf9 100644 --- a/web/containers/ModelConfigInput/index.tsx +++ b/web/containers/ModelConfigInput/index.tsx @@ -7,65 +7,26 @@ import { TooltipTrigger, } from '@janhq/uikit' -import { useAtomValue, useSetAtom } from 'jotai' - import { InfoIcon } from 'lucide-react' -import { useActiveModel } from '@/hooks/useActiveModel' -import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' - -import { getConfigurationsData } from '@/utils/componentSettings' - -import { toSettingParams } from '@/utils/modelParam' - -import { serverEnabledAtom } from '@/helpers/atoms/LocalServer.atom' -import { - engineParamsUpdateAtom, - getActiveThreadIdAtom, - getActiveThreadModelParamsAtom, -} from '@/helpers/atoms/Thread.atom' - type Props = { title: string + enabled?: boolean name: string description: string placeholder: string value: string + onValueChanged?: (e: string | number | boolean) => void } const ModelConfigInput: React.FC = ({ title, - name, + enabled = true, value, description, placeholder, + onValueChanged, }) => { - const { updateModelParameter } = useUpdateModelParameters() - const threadId = useAtomValue(getActiveThreadIdAtom) - - const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) - - const modelSettingParams = toSettingParams(activeModelParams) - - const engineParams = getConfigurationsData(modelSettingParams) - - const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom) - - const { stopModel } = useActiveModel() - - const serverEnabled = useAtomValue(serverEnabledAtom) - - const onValueChanged = (e: React.ChangeEvent) => { - if (!threadId) return - if (engineParams.some((x) => x.name.includes(name))) { - setEngineParamsUpdate(true) - stopModel() - } else { - setEngineParamsUpdate(false) - } - updateModelParameter(threadId, name, e.target.value) - } - return (
@@ -86,9 +47,9 @@ const ModelConfigInput: React.FC = ({