diff --git a/.gitignore b/.gitignore index 3cddf31c..26b31b38 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,5 @@ next-env.d.ts # app output directories /gptscripts /threads + +bin/ diff --git a/actions/knowledge/knowledge.ts b/actions/knowledge/knowledge.ts new file mode 100644 index 00000000..9a29ce60 --- /dev/null +++ b/actions/knowledge/knowledge.ts @@ -0,0 +1,23 @@ +'use server'; +import path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execPromise = promisify(exec); + +export async function ingest( + workspace: string, + token: string | undefined, + datasetID?: string | null +): Promise { + if (!datasetID) { + throw new Error('Dataset ID is required'); + } + const dir = path.join(workspace, 'knowledge'); + const knowledgeBinaryPath = process.env.KNOWLEDGE_BIN; + await execPromise( + `${knowledgeBinaryPath} ingest --dataset ${datasetID} ${dir.replace(/ /g, '\\ ')}`, + { env: { ...process.env, GPTSCRIPT_GATEWAY_API_KEY: token } } + ); + return; +} diff --git a/actions/threads.tsx b/actions/threads.tsx index ef72e35e..28b51488 100644 --- a/actions/threads.tsx +++ b/actions/threads.tsx @@ -1,6 +1,6 @@ 'use server'; -import { GATEWAY_URL, THREADS_DIR, WORKSPACE_DIR } from '@/config/env'; +import { THREADS_DIR, WORKSPACE_DIR } from '@/config/env'; import { gpt } from '@/config/env'; import fs from 'fs/promises'; import path from 'path'; @@ -24,15 +24,6 @@ export type ThreadMeta = { workspace: string; }; -export async function init() { - const threadsDir = THREADS_DIR(); - try { - await fs.access(threadsDir); - } catch (error) { - await fs.mkdir(threadsDir, { recursive: true }); - } -} - export async function getThreads() { const threads: Thread[] = []; const threadsDir = THREADS_DIR(); @@ -40,6 +31,7 @@ export async function getThreads() { let threadDirs: void | string[] = []; try { threadDirs = await fs.readdir(threadsDir); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return []; } @@ -49,18 +41,19 @@ export async function getThreads() { for (const threadDir of threadDirs) { const threadPath = path.join(threadsDir, threadDir); const files = await fs.readdir(threadPath); + const stateFile = path.join(threadPath, STATE_FILE); const thread: Thread = {} as Thread; if (files.includes(STATE_FILE)) { - const state = await fs.readFile( - path.join(threadPath, STATE_FILE), - 'utf-8' - ); + const state = await fs.readFile(stateFile, 'utf-8'); thread.state = state; } if (files.includes(META_FILE)) { const meta = await fs.readFile(path.join(threadPath, META_FILE), 'utf-8'); + const stateStats = await fs.stat(stateFile); + const lastModified = stateStats.mtime; thread.meta = JSON.parse(meta) as ThreadMeta; + thread.meta.updated = lastModified; } else { continue; } @@ -96,9 +89,8 @@ export async function generateThreadName( export async function createThread( script: string, - firstMessage?: string, - scriptId?: string, - workspace?: string + threadName?: string, + scriptId?: string ): Promise { const threadsDir = THREADS_DIR(); @@ -106,13 +98,18 @@ export async function createThread( const id = Math.random().toString(36).substring(7); const threadPath = path.join(threadsDir, id); await fs.mkdir(threadPath, { recursive: true }); + const workspace = path.join(threadPath, 'workspace'); + await fs.mkdir(workspace, { recursive: true }); + if (!threadName) { + threadName = await newThreadName(); + } const threadMeta = { - name: await newThreadName(), + name: threadName, description: '', created: new Date(), updated: new Date(), - workspace: workspace ?? WORKSPACE_DIR(), + workspace: workspace, id, scriptId: scriptId || '', script, @@ -125,11 +122,6 @@ export async function createThread( ); await fs.writeFile(path.join(threadPath, STATE_FILE), ''); - if (firstMessage) { - const generatedThreadName = await generateThreadName(firstMessage); - await renameThread(id, generatedThreadName); - } - return { state: threadState, meta: threadMeta, diff --git a/actions/upload.tsx b/actions/upload.tsx index db73d404..0886ac9a 100644 --- a/actions/upload.tsx +++ b/actions/upload.tsx @@ -5,7 +5,14 @@ import path from 'node:path'; import { revalidatePath } from 'next/cache'; import { Dirent } from 'fs'; -export async function uploadFile(workspace: string, formData: FormData) { +export async function uploadFile( + workspace: string, + formData: FormData, + isKnowledge?: boolean +) { + if (isKnowledge) { + workspace = path.join(workspace, 'knowledge'); + } const file = formData.get('file') as File; await fs.mkdir(workspace, { recursive: true }); diff --git a/app/edit/page.tsx b/app/edit/page.tsx index ad3c339a..0249d0a1 100644 --- a/app/edit/page.tsx +++ b/app/edit/page.tsx @@ -29,7 +29,7 @@ function EditFile() {
( useSearchParams().get('file') ?? 'github.com/gptscript-ai/ui-assistant' ); - const [thread, _setThread] = useState( - useSearchParams().get('thread') ?? '' - ); const [scriptId, _scriptId] = useState( useSearchParams().get('id') ?? '' ); + const { setCurrent } = useContext(NavContext); useEffect(() => setCurrent('/'), []); @@ -24,8 +22,8 @@ function RunFile() { return (
= ({ tool }) => {
diff --git a/components/script.tsx b/components/script.tsx index e3c4153c..e48a2394 100644 --- a/components/script.tsx +++ b/components/script.tsx @@ -1,21 +1,16 @@ 'use client'; -import { useContext, useCallback, useEffect, useState, useRef } from 'react'; +import { useContext, useEffect, useState, useRef, useCallback } from 'react'; import Messages, { MessageType } from '@/components/script/messages'; import ChatBar from '@/components/script/chatBar'; import ToolForm from '@/components/script/form'; import Loading from '@/components/loading'; import { Button } from '@nextui-org/react'; import { getWorkspaceDir } from '@/actions/workspace'; -import { - createThread, - getThreads, - generateThreadName, - renameThread, -} from '@/actions/threads'; import { getGatewayUrl } from '@/actions/gateway'; import { ScriptContext } from '@/contexts/script'; import AssistantNotFound from '@/components/assistant-not-found'; +import { generateThreadName, renameThread } from '@/actions/threads'; interface ScriptProps { className?: string; @@ -27,7 +22,6 @@ interface ScriptProps { const Script: React.FC = ({ className, messagesHeight = 'h-full', - enableThreads, showAssistantName, }) => { const inputRef = useRef(null); @@ -45,16 +39,12 @@ const Script: React.FC = ({ messages, setMessages, thread, - setThreads, - setSelectedThreadId, socket, connected, running, notFound, restartScript, - scriptId, fetchThreads, - workspace, } = useContext(ScriptContext); useEffect(() => { @@ -91,48 +81,28 @@ const Script: React.FC = ({ })); }; + const hasNoUserMessages = useCallback( + () => messages.filter((m) => m.type === MessageType.User).length === 0, + [messages] + ); + const handleMessageSent = async (message: string) => { if (!socket || !connected) return; - let threadId = ''; - if ( - hasNoUserMessages() && - enableThreads && - !thread && - setThreads && - setSelectedThreadId - ) { - const newThread = await createThread( - script, - message, - scriptId, - workspace - ); - threadId = newThread?.meta?.id; - setThreads(await getThreads()); - setSelectedThreadId(threadId); - } - setMessages((prevMessages) => [ ...prevMessages, { type: MessageType.User, message }, ]); - socket.emit('userMessage', message, threadId); - if (hasNoUserMessages() && thread) { renameThread(thread, await generateThreadName(message)); fetchThreads(); } + socket.emit('userMessage', message, thread); }; - const hasNoUserMessages = useCallback( - () => messages.filter((m) => m.type === MessageType.User).length === 0, - [messages] - ); - return (
- {(connected && running) || (showForm && hasParams) ? ( + {connected || (showForm && hasParams) ? ( <>
= ({ {tool.chat ? 'Start chat' : 'Run script'} ) : ( - + )}
diff --git a/components/script/chatBar.tsx b/components/script/chatBar.tsx index 8b6d71ea..1ca8e214 100644 --- a/components/script/chatBar.tsx +++ b/components/script/chatBar.tsx @@ -2,6 +2,7 @@ import React, { useState, useContext, useEffect } from 'react'; import { IoMdSend } from 'react-icons/io'; +import { Spinner } from '@nextui-org/react'; import { FaBackward } from 'react-icons/fa'; import { Button, Textarea } from '@nextui-org/react'; import Commands from '@/components/script/chatBar/commands'; @@ -76,7 +77,10 @@ const ChatBar = ({ disabled = false, onMessageSent }: ChatBarProps) => { radius="full" className="text-lg" color="primary" - onPress={() => setCommandsOpen(true)} + onPress={() => { + if (disabled) return; + setCommandsOpen(true); + }} onBlur={() => setTimeout(() => setCommandsOpen(false), 300)} // super hacky but it does work />
@@ -128,15 +132,21 @@ const ChatBar = ({ disabled = false, onMessageSent }: ChatBarProps) => { onPress={interrupt} /> ) : ( -
); diff --git a/components/script/chatBar/commands.tsx b/components/script/chatBar/commands.tsx index 6b9aa3db..a05c81af 100644 --- a/components/script/chatBar/commands.tsx +++ b/components/script/chatBar/commands.tsx @@ -2,6 +2,7 @@ import { Card, Listbox, ListboxItem } from '@nextui-org/react'; import React, { useEffect, useContext } from 'react'; import { GoAlert, + GoCheckCircleFill, GoInbox, GoIssueReopened, GoPaperclip, @@ -11,10 +12,10 @@ import { PiToolbox } from 'react-icons/pi'; import { ScriptContext } from '@/contexts/script'; import Upload from '@/components/script/chatBar/upload'; import ToolCatalog from '@/components/script/chatBar/toolCatalog'; -import { Message, MessageType } from '@/components/script/messages'; +import { MessageType } from '@/components/script/messages'; import { useFilePicker } from 'use-file-picker'; import { uploadFile } from '@/actions/upload'; -import { getWorkspaceDir } from '@/actions/workspace'; +import { ingest } from '@/actions/knowledge/knowledge'; /* note(tylerslaton): @@ -33,6 +34,8 @@ import { getWorkspaceDir } from '@/actions/workspace'; to the previous command in the list. This is a bit hacky but it works for now. */ +const gatewayTool = 'github.com/gptscript-ai/knowledge@v0.4.10-gateway.1'; + const options = [ { title: 'Restart Chat', @@ -62,6 +65,18 @@ const options = [ }, ]; +function getCookie(name: string): string { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + const value = parts?.pop()?.split(';').shift(); + if (value) { + return decodeURIComponent(value); + } + } + return ''; +} + interface CommandsProps { text: string; setText: (text: string) => void; @@ -81,8 +96,16 @@ export default function Commands({ React.useState(options); const [uploadOpen, setUploadOpen] = React.useState(false); const [toolCatalogOpen, setToolCatalogOpen] = React.useState(false); - const { restartScript, socket, setMessages, tools, tool, setTool } = - useContext(ScriptContext); + const { + restartScript, + socket, + setMessages, + tools, + tool, + setTool, + workspace, + selectedThreadId, + } = useContext(ScriptContext); const { openFilePicker, filesContent, loading, plainFiles } = useFilePicker( {} ); @@ -102,54 +125,52 @@ export default function Commands({ }, [text]); useEffect(() => { - if (loading) return; - if (!filesContent.length) return; - - const addedMessages = [] as Message[]; - for (const file of plainFiles) { - const formData = new FormData(); - formData.append('file', file); + const uploadKnowledge = async () => { + if (loading) return; + if (!filesContent.length) return; - getWorkspaceDir().then((workspace) => - uploadFile(workspace, formData) - .then(() => { - addedMessages.push({ + try { + for (const file of plainFiles) { + const formData = new FormData(); + formData.append('file', file); + setMessages((prev) => [ + ...prev, + { type: MessageType.Alert, icon: , - message: `Added knowledge ${file.name}`, - }); - }) - .catch((error) => { - addedMessages.push({ - type: MessageType.Alert, - icon: , - message: `Error adding knowledge ${file.name}: ${error}`, - }); - }) - .finally(() => { - setMessages((prev) => [...prev, ...addedMessages]); - if ( - !tool || - tool.tools?.includes( - 'github.com/gptscript-ai/knowledge@v0.4.7-gateway' - ) - ) - return; - setTool((prev) => ({ - ...prev, - tools: [ - ...(prev.tools || []), - 'github.com/gptscript-ai/knowledge@v0.4.7-gateway', - ], - })); - socket?.emit( - 'addTool', - 'github.com/gptscript-ai/knowledge@v0.4.7-gateway' - ); - }) - ); - } - }, [filesContent, loading, tool]); + message: `Uploading knowledge ${file.name}`, + }, + ]); + await uploadFile(workspace, formData, true); + } + await ingest(workspace, getCookie('gateway_token'), selectedThreadId); + setMessages((prev) => [ + ...prev, + { + type: MessageType.Alert, + icon: , + message: `Successfully uploaded knowledge ${plainFiles.map((f) => f.name).join(', ')}`, + }, + ]); + if (!tool || tool.tools?.includes(gatewayTool)) return; + setTool((prev) => ({ + ...prev, + tools: [...(prev.tools || []), gatewayTool], + })); + socket?.emit('addTool', gatewayTool); + } catch (e) { + setMessages((prev) => [ + ...prev, + { + type: MessageType.Alert, + icon: , + message: `Error uploading knowledge ${filesContent.map((f) => f.name).join(', ')}: ${e}`, + }, + ]); + } + }; + uploadKnowledge(); + }, [filesContent, loading]); const handleSelect = (value: string) => { switch (value) { diff --git a/components/threads/new.tsx b/components/threads/new.tsx index dc182bcc..c4107f68 100644 --- a/components/threads/new.tsx +++ b/components/threads/new.tsx @@ -12,6 +12,7 @@ import { useEffect, useState, useContext } from 'react'; import { ScriptContext } from '@/contexts/script'; import { GoPlus } from 'react-icons/go'; import { getScripts, Script } from '@/actions/me/scripts'; +import { setWorkspaceDir } from '@/actions/workspace'; interface NewThreadProps { className?: string; @@ -21,7 +22,7 @@ const NewThread = ({ className }: NewThreadProps) => { const [scripts, setScripts] = useState([]); const [isOpen, setIsOpen] = useState(false); const [_loading, setLoading] = useState(true); - const { setThread, setSelectedThreadId, setScript, setThreads, scriptId } = + const { setThread, setSelectedThreadId, setScript, setThreads, setScriptId } = useContext(ScriptContext); const fetchScripts = async () => { @@ -35,12 +36,14 @@ const NewThread = ({ className }: NewThreadProps) => { fetchScripts(); }, [isOpen]); - const handleCreateThread = (script: string) => { - createThread(script, '', scriptId).then((newThread) => { + const handleCreateThread = (script: string, id?: string) => { + createThread(script, '', id).then((newThread) => { + setScriptId(id); setThreads((threads: Thread[]) => [newThread, ...threads]); setScript(script); setThread(newThread.meta.id); setSelectedThreadId(newThread.meta.id); + setWorkspaceDir(newThread.meta.workspace); setLoading(false); }); }; @@ -71,7 +74,7 @@ const NewThread = ({ className }: NewThreadProps) => { className="py-2 truncate max-w-[200px]" content={script.displayName} onClick={() => { - handleCreateThread(script.publicURL!); + handleCreateThread(script.publicURL!, script.id?.toString()); setIsOpen(false); }} > diff --git a/contexts/script.tsx b/contexts/script.tsx index 5fc024ec..b6d2bd78 100644 --- a/contexts/script.tsx +++ b/contexts/script.tsx @@ -1,9 +1,9 @@ -import { createContext, useState, useEffect, useCallback } from 'react'; +import { createContext, useState, useEffect, useCallback, useRef } from 'react'; import useChatSocket from '@/components/script/useChatSocket'; import { Message } from '@/components/script/messages'; import { Block, Tool, ToolDef } from '@gptscript-ai/gptscript'; import { Socket } from 'socket.io-client'; -import { getThreads, getThread, Thread } from '@/actions/threads'; +import { getThreads, getThread, Thread, createThread } from '@/actions/threads'; import { getScript, getScriptContent } from '@/actions/me/scripts'; import { rootTool } from '@/actions/gptscript'; import debounce from 'lodash/debounce'; @@ -13,8 +13,8 @@ interface ScriptContextProps { children: React.ReactNode; initialScript: string; initialSubTool?: string; - initialThread: string; initialScriptId?: string; + enableThread?: boolean; } interface ScriptContextState { @@ -65,15 +65,17 @@ interface ScriptContextState { restartScript: () => void; } +const defaultScriptName = `Tildy`; + const ScriptContext = createContext( {} as ScriptContextState ); const ScriptContextProvider: React.FC = ({ children, initialScript, - initialThread, initialSubTool, initialScriptId, + enableThread, }) => { const [script, setScript] = useState(initialScript); const [workspace, setWorkspace] = useState(''); @@ -86,7 +88,7 @@ const ScriptContextProvider: React.FC = ({ const [hasParams, setHasParams] = useState(false); const [isEmpty, setIsEmpty] = useState(false); const [notFound, setNotFound] = useState(false); - const [thread, setThread] = useState(initialThread); + const [thread, setThread] = useState(''); const [threads, setThreads] = useState([]); const [selectedThreadId, setSelectedThreadId] = useState(null); const [initialFetch, setInitialFetch] = useState(false); @@ -108,11 +110,11 @@ const ScriptContextProvider: React.FC = ({ setForceRun, } = useChatSocket(isEmpty); const [scriptDisplayName, setScriptDisplayName] = useState(''); + const threadInitialized = useRef(false); // need to initialize the workspace from the env variable with serves // as the default. useEffect(() => { - fetchThreads(); getWorkspaceDir().then((workspace) => { setWorkspace(workspace); }); @@ -137,13 +139,48 @@ const ScriptContextProvider: React.FC = ({ setNotFound(true); return; } - setScriptDisplayName('Tildy'); + setScriptDisplayName(defaultScriptName); setNotFound(false); setTool(await rootTool(content)); setInitialFetch(true); }); } - }, [script, scriptId]); + }, [script, scriptId, thread]); + + useEffect(() => { + if (!enableThread || thread || threadInitialized.current) { + return; + } + threadInitialized.current = true; + const createAndSetThread = async () => { + try { + const threads = await getThreads(); + if ((initialScript && initialScriptId) || threads.length === 0) { + // if both threads and scriptId are set, then always create a new thread + const newThread = await createThread( + script, + scriptDisplayName ?? defaultScriptName, + scriptId + ); + const threadId = newThread?.meta?.id; + setThread(threadId); + setThreads(await getThreads()); + setSelectedThreadId(threadId); + setWorkspace(newThread.meta.workspace); + } else if (threads.length > 0) { + setThreads(threads); + const latestThread = threads[0]; + setThread(latestThread.meta.id); + setSelectedThreadId(latestThread.meta.id); + setScriptId(latestThread.meta.scriptId); + } + } catch (e) { + threadInitialized.current = false; + console.error(e); + } + }; + createAndSetThread(); + }, [thread, threads, enableThread, scriptDisplayName]); useEffect(() => { setHasParams( @@ -170,7 +207,14 @@ const ScriptContextProvider: React.FC = ({ useEffect(() => { setIsEmpty(!tool.instructions); - if (hasRun || !socket || !connected || !initialFetch) return; + if ( + hasRun || + !socket || + !connected || + !initialFetch || + (enableThread && !threadInitialized.current) + ) + return; if ( !tool.arguments?.properties || Object.keys(tool.arguments.properties).length === 0 @@ -185,7 +229,7 @@ const ScriptContextProvider: React.FC = ({ ); setHasRun(true); } - }, [tool, connected, script, scriptContent, formValues, workspace, thread]); + }, [tool, connected, script, scriptContent, formValues, workspace]); useEffect(() => { if (forceRun && socket && connected) { diff --git a/electron/build.mjs b/electron/build.mjs index 468e11bb..838cee16 100644 --- a/electron/build.mjs +++ b/electron/build.mjs @@ -24,6 +24,7 @@ const options = { '!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}', '!**/{appveyor.yml,.travis.yml,circle.yml}', '!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}', + 'bin/*', ], mac: { hardenedRuntime: true, @@ -72,6 +73,13 @@ const options = { releaseType: 'release', vPrefixedTagName: true, }, + extraResources: [ + { + from: 'bin/', + to: 'bin', + filter: ['**/*'], + }, + ], }; function go() { diff --git a/electron/config.mjs b/electron/config.mjs index 89182247..6ec5e0eb 100644 --- a/electron/config.mjs +++ b/electron/config.mjs @@ -27,6 +27,14 @@ const gptscriptBin = 'bin', `gptscript${process.platform === 'win32' ? '.exe' : ''}` ); +const knowledgeBin = + process.env.KNOWLEDGE_BIN || process.env.NODE_ENV === 'production' + ? join( + process.resourcesPath, + 'bin', + 'knowledge' + (process.platform === 'win32' ? '.exe' : '') + ) + : join(process.cwd(), 'bin', 'knowledge'); const gatewayUrl = process.env.GPTSCRIPT_GATEWAY_URL || 'https://gateway-api.gptscript.ai'; @@ -51,7 +59,10 @@ Object.assign(log.transports.file, { const timestamp = Math.floor(Date.now() / 1000); try { - renameSync(filePath, join(info.dir, `${info.name}.${timestamp}${info.ext}`)); + renameSync( + filePath, + join(info.dir, `${info.name}.${timestamp}${info.ext}`) + ); } catch (e) { console.warn('failed to rotate log file', e); } @@ -79,4 +90,5 @@ export const config = { port, gptscriptBin, gatewayUrl, + knowledgeBin, }; diff --git a/electron/main.mjs b/electron/main.mjs index 38e80983..68a71326 100644 --- a/electron/main.mjs +++ b/electron/main.mjs @@ -31,6 +31,7 @@ async function startServer() { process.env.WORKSPACE_DIR = config.workspaceDir; process.env.GPTSCRIPT_GATEWAY_URL = config.gatewayUrl; process.env.GPTSCRIPT_OPENAPI_REVAMP = 'true'; + process.env.KNOWLEDGE_BIN = config.knowledgeBin; try { const url = await startAppServer({ diff --git a/package-lock.json b/package-lock.json index c6139c1e..96a1b028 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "acorn", "version": "v0.10.0-rc3", + "hasInstallScript": true, "dependencies": { "@gptscript-ai/gptscript": "github:gptscript-ai/node-gptscript#de914911013bf9da02046906b88a5613dce508e1", "@monaco-editor/react": "^4.6.0", diff --git a/package.json b/package.json index 45ca3776..ad4dd90e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "dev:electron": "electron .", "lint": "next lint", "format": "prettier --write .", - "start": "cross-env NODE_ENV='production' node server.mjs" + "start": "cross-env NODE_ENV='production' node server.mjs", + "postinstall": "node scripts/install-binary.mjs" }, "main": "electron/main.mjs", "dependencies": { diff --git a/scripts/install-binary.mjs b/scripts/install-binary.mjs new file mode 100644 index 00000000..90c815d0 --- /dev/null +++ b/scripts/install-binary.mjs @@ -0,0 +1,142 @@ +#!/usr/bin/env node + +'use strict'; + +import { DownloaderHelper } from 'node-downloader-helper'; +import fs from 'fs'; +import path from 'path'; +import AdmZip from 'adm-zip'; +import tar from 'tar'; +import util from 'util'; +import child_process from 'child_process'; + +const exec = util.promisify(child_process.exec); + +async function downloadAndExtract(url, saveDirectory) { + const dlh = new DownloaderHelper(url, saveDirectory); + + return new Promise((resolve, reject) => { + dlh.on('end', () => { + const downloadedFilePath = path.join(dlh.getDownloadPath()); + const newFilePath = path.join(saveDirectory, knowledgeBinaryName); + fs.rename(downloadedFilePath, newFilePath, (err) => { + if (err) { + return reject(err); + } + fs.chmod(newFilePath, 0o755, (_err) => { + if (url.endsWith('.zip')) { + const zip = new AdmZip(downloadedFilePath); + zip.extractAllTo(saveDirectory, true); + fs.unlinkSync(downloadedFilePath); + } else if (url.endsWith('.tar.gz')) { + tar + .x({ + file: downloadedFilePath, + cwd: saveDirectory, + }) + .then(() => { + fs.unlinkSync(downloadedFilePath); // Delete the tar.gz file after extraction + }) + .catch((error) => reject(error)); + } + resolve(); + }); + }); + }); + dlh.on('error', (error) => reject(error)); + dlh.on('progress.throttled', (downloadEvents) => { + const percentageComplete = + downloadEvents.progress < 100 + ? downloadEvents.progress.toFixed(2) + : 100; + console.info(`downloaded: ${percentageComplete}%`); + }); + + dlh.start(); + }); +} + +async function versions_match() { + try { + const command = path.join(outputDir, knowledgeBinaryName) + ' version'; + const { stdout } = await exec(command); + return stdout.toString().includes(gptscript_info.version); + } catch (err) { + console.error('Error checking gptscript version:', err); + return false; + } +} + +const platform = process.platform; +let arch = process.arch; +if (process.platform === 'darwin') { + arch = 'amd64'; +} else if (process.arch === 'x64') { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + arch = 'amd64'; +} + +let knowledgeBinaryName = 'knowledge'; +if (process.platform === 'win32') { + knowledgeBinaryName = 'knowledge.exe'; +} + +const gptscript_info = { + name: 'gptscript', + url: 'https://github.com/gptscript-ai/knowledge/releases/download/', + version: 'v0.4.10-gateway', +}; + +const pltfm = { + win32: 'windows', + linux: 'linux', + darwin: 'darwin', +}[platform]; + +const suffix = { + win32: '.exe', + linux: '', + darwin: '', +}[platform]; + +const url = `${gptscript_info.url}${gptscript_info.version}/knowledge-${pltfm}-${arch}${suffix}`; + +const outputDir = path.resolve('bin'); + +const fileExist = (path) => { + try { + fs.accessSync(path); + return true; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (err) { + return false; + } +}; + +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + console.info(`${outputDir} directory was created`); +} + +async function needToInstall() { + if (fileExist(path.join(outputDir, knowledgeBinaryName))) { + console.log('knowledge is installed...'); + const versions = await versions_match(); + if (versions) { + console.log('gptscript version is up to date...exiting'); + process.exit(0); + } + } +} + +await needToInstall(); +if (process.env.KNOWLEDGE_SKIP_INSTALL_BINARY === 'true') { + console.info('Skipping binary download'); + process.exit(0); +} +console.log(`Downloading and extracting knowledge binary from ${url}...`); +try { + await downloadAndExtract(url, outputDir); +} catch (error) { + console.error('Error downloading and extracting:', error); +} diff --git a/server/app.mjs b/server/app.mjs index 92946c47..8e358fb2 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -140,6 +140,10 @@ const mount = async ( workspace: scriptWorkspace, prompt: true, confirm: true, + env: [ + ...Object.entries(process.env).map(([key, value]) => `${key}=${value}`), + 'GPTSCRIPT_THREAD_ID=' + threadID, + ], }; if (tool) opts.subTool = tool; @@ -148,23 +152,28 @@ const mount = async ( let statePath = ''; if (threadID) statePath = path.join(THREADS_DIR, threadID, STATE_FILE); try { - state = JSON.parse(fs.readFileSync(statePath, 'utf8')); - if (state && state.chatState) { - opts.chatState = state.chatState; - // also load the tools defined the states so that when running a thread that has tools added in state, we don't lose them - for (let block of script) { - if (block.type === 'tool') { - if (!block.tools) block.tools = []; - block.tools = [ - ...new Set([...(block.tools || []), ...(state.tools || [])]), - ]; - break; + if (fs.existsSync(statePath)) { + const stateString = fs.readFileSync(statePath, 'utf8'); + if (stateString) { + state = JSON.parse(fs.readFileSync(statePath, 'utf8')); + if (state && state.chatState) { + opts.chatState = state.chatState; + // also load the tools defined the states so that when running a thread that has tools added in state, we don't lose them + for (let block of script) { + if (block.type === 'tool') { + if (!block.tools) block.tools = []; + block.tools = [ + ...new Set([...(block.tools || []), ...(state.tools || [])]), + ]; + break; + } + } + socket.emit('loaded', { + messages: state.messages, + tools: state.tools || [], + }); } } - socket.emit('loaded', { - messages: state.messages, - tools: state.tools || [], - }); } } catch (e) { console.error('Error loading state:', e);