Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@ next-env.d.ts
# app output directories
/gptscripts
/threads

bin/
23 changes: 23 additions & 0 deletions actions/knowledge/knowledge.ts
Original file line number Diff line number Diff line change
@@ -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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we run knowledge ingest command on server side to ingest file on fly.

workspace: string,
token: string | undefined,
datasetID?: string | null
): Promise<void> {
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;
}
40 changes: 16 additions & 24 deletions actions/threads.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,22 +24,14 @@ 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();

let threadDirs: void | string[] = [];
try {
threadDirs = await fs.readdir(threadsDir);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return [];
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -96,23 +89,27 @@ export async function generateThreadName(

export async function createThread(
script: string,
firstMessage?: string,
scriptId?: string,
workspace?: string
threadName?: string,
scriptId?: string
): Promise<Thread> {
const threadsDir = THREADS_DIR();

// will probably want something else for this
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,
Expand All @@ -125,11 +122,6 @@ export async function createThread(
);
await fs.writeFile(path.join(threadPath, STATE_FILE), '');

if (firstMessage) {
const generatedThreadName = await generateThreadName(firstMessage);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, it was a requirement to have the LLM name the thread based on the first message. Has that requirement changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I changed it because the thread will always be created at first and it is kind of weird that the name will be changed later after user type the message. But yeah I will confirm with craig.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will change this, thanks for pointting out

await renameThread(id, generatedThreadName);
}

return {
state: threadState,
meta: threadMeta,
Expand Down
9 changes: 8 additions & 1 deletion actions/upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down
2 changes: 1 addition & 1 deletion app/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function EditFile() {
<ScriptContextProvider
initialScript={file}
initialScriptId={scriptId}
initialThread=""
enableThread={false}
>
<EditContextProvider scriptPath={file} initialScriptId={scriptId}>
<div
Expand Down
6 changes: 2 additions & 4 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,19 @@ function RunFile() {
const [script, _setScript] = useState<string>(
useSearchParams().get('file') ?? 'github.com/gptscript-ai/ui-assistant'
);
const [thread, _setThread] = useState<string>(
useSearchParams().get('thread') ?? ''
);
const [scriptId, _scriptId] = useState<string>(
useSearchParams().get('id') ?? ''
);

const { setCurrent } = useContext(NavContext);

useEffect(() => setCurrent('/'), []);

return (
<ScriptContextProvider
initialScript={script}
initialThread={thread}
initialScriptId={scriptId}
enableThread={true}
>
<section className="absolute left-0 top-[50px]">
<div
Expand Down
2 changes: 1 addition & 1 deletion components/edit/configure/customTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ const CustomTool: React.FC<ConfigureProps> = ({ tool }) => {
<div className="h-full overflow-y-auto">
<ScriptContextProvider
initialScript={scriptPath}
initialThread=""
enableThread={false}
initialSubTool={customTool.name}
initialScriptId={`${scriptId}`}
>
Expand Down
50 changes: 10 additions & 40 deletions components/script.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -27,7 +22,6 @@ interface ScriptProps {
const Script: React.FC<ScriptProps> = ({
className,
messagesHeight = 'h-full',
enableThreads,
showAssistantName,
}) => {
const inputRef = useRef<HTMLInputElement>(null);
Expand All @@ -45,16 +39,12 @@ const Script: React.FC<ScriptProps> = ({
messages,
setMessages,
thread,
setThreads,
setSelectedThreadId,
socket,
connected,
running,
notFound,
restartScript,
scriptId,
fetchThreads,
workspace,
} = useContext(ScriptContext);

useEffect(() => {
Expand Down Expand Up @@ -91,48 +81,28 @@ const Script: React.FC<ScriptProps> = ({
}));
};

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 (
<div className={`h-full w-full ${className}`}>
{(connected && running) || (showForm && hasParams) ? (
{connected || (showForm && hasParams) ? (
<>
<div
id="small-message"
Expand Down Expand Up @@ -168,7 +138,7 @@ const Script: React.FC<ScriptProps> = ({
{tool.chat ? 'Start chat' : 'Run script'}
</Button>
) : (
<ChatBar onMessageSent={handleMessageSent} />
<ChatBar disabled={!running} onMessageSent={handleMessageSent} />
)}
</div>
</>
Expand Down
30 changes: 20 additions & 10 deletions components/script/chatBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
/>
<div className="w-full relative">
Expand Down Expand Up @@ -128,15 +132,21 @@ const ChatBar = ({ disabled = false, onMessageSent }: ChatBarProps) => {
onPress={interrupt}
/>
) : (
<Button
startContent={<IoMdSend />}
isDisabled={disabled}
isIconOnly
radius="full"
className="text-lg"
color="primary"
onPress={handleSend}
/>
<>
{disabled ? (
<Spinner />
) : (
<Button
startContent={<IoMdSend />}
isIconOnly
isDisabled={!inputValue}
radius="full"
className="text-lg"
color="primary"
onPress={handleSend}
/>
)}
</>
)}
</div>
);
Expand Down
Loading