Skip to content

Commit

Permalink
refactor: update thread and message according to new spec
Browse files Browse the repository at this point in the history
Signed-off-by: James <[email protected]>
  • Loading branch information
James committed Nov 24, 2023
1 parent c0fb6ec commit 5dfc501
Show file tree
Hide file tree
Showing 28 changed files with 878 additions and 509 deletions.
10 changes: 10 additions & 0 deletions core/src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ const rmdir: (path: string) => Promise<any> = (path) =>
const deleteFile: (path: string) => Promise<any> = (path) =>
window.coreAPI?.deleteFile(path) ?? window.electronAPI?.deleteFile(path);

/**
* Appends data to a file at the specified path.
* @param path path to the file
* @param data data to append
*/
const appendFile: (path: string, data: string) => Promise<any> = (path, data) =>
window.coreAPI?.appendFile(path, data) ??
window.electronAPI?.appendFile(path, data);

export const fs = {
isDirectory,
getUserSpace,
Expand All @@ -71,4 +80,5 @@ export const fs = {
mkdir,
rmdir,
deleteFile,
appendFile,
};
28 changes: 15 additions & 13 deletions core/src/plugins/conversational.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
import { Thread } from "../index";
import { Thread, ThreadMessage } from "../index";
import { JanPlugin } from "../plugin";

/**
* Abstract class for conversational plugins.
* Abstract class for Thread plugins.
* @abstract
* @extends JanPlugin
*/
export abstract class ConversationalPlugin extends JanPlugin {
/**
* Returns a list of conversations.
* Returns a list of thread.
* @abstract
* @returns {Promise<any[]>} A promise that resolves to an array of conversations.
* @returns {Promise<Thread[]>} A promise that resolves to an array of threads.
*/
abstract getConversations(): Promise<any[]>;
abstract getThreads(): Promise<Thread[]>;

/**
* Saves a conversation.
* Saves a thread.
* @abstract
* @param {Thread} conversation - The conversation to save.
* @returns {Promise<void>} A promise that resolves when the conversation is saved.
* @param {Thread} thread - The thread to save.
* @returns {Promise<void>} A promise that resolves when the thread is saved.
*/
abstract saveConversation(conversation: Thread): Promise<void>;
abstract saveThread(thread: Thread): Promise<void>;

/**
* Deletes a conversation.
* Deletes a thread.
* @abstract
* @param {string} conversationId - The ID of the conversation to delete.
* @returns {Promise<void>} A promise that resolves when the conversation is deleted.
* @param {string} threadId - The ID of the thread to delete.
* @returns {Promise<void>} A promise that resolves when the thread is deleted.
*/
abstract deleteConversation(conversationId: string): Promise<void>;
abstract deleteThread(threadId: string): Promise<void>;

abstract addNewMessage(message: ThreadMessage): Promise<void>;
}
72 changes: 53 additions & 19 deletions core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,15 @@ export type ChatCompletionMessage = {
*/
export type MessageRequest = {
id?: string;

/** The thread id of the message request. **/
threadId?: string;
threadId: string;

/**
* The assistant id of the message request.
*/
assistantId?: string;

/** Messages for constructing a chat completion request **/
messages?: ChatCompletionMessage[];
};
Expand All @@ -57,17 +64,39 @@ export enum MessageStatus {
*/
export type ThreadMessage = {
/** Unique identifier for the message, generated by default using the ULID method. **/
id?: string;
id: string;
object: string;
/** Thread id, default is a ulid. **/
threadId?: string;
thread_id: string;
/** The role of the author of this message. **/
role?: ChatCompletionRole;
assistant_id?: string;
// TODO: comment
role: ChatCompletionRole;
/** The content of this message. **/
content?: string;
content: ThreadContent[];
/** The status of this message. **/
status: MessageStatus;
/** The timestamp indicating when this message was created, represented in ISO 8601 format. **/
createdAt?: string;
created: number;

updated: number;

metadata?: Record<string, unknown>;
};

export enum ContentType {
Text = "text",
Image = "image",
}

export type ThreadContent = {
type: ContentType;
text: ContentValue;
};

export type ContentValue = {
value: string;
annotations: string[];
};

/**
Expand All @@ -77,22 +106,27 @@ export type ThreadMessage = {
export interface Thread {
/** Unique identifier for the thread, generated by default using the ULID method. **/
id: string;
/** The summary of this thread. **/
summary?: string;
/** The messages of this thread. **/
messages: ThreadMessage[];
// TODO: comment
object: string;
/** The title of this thread. **/
title: string;
assistants: Assistant[];
// if the thread has been init will full assistant info
isFinishInit: boolean;
/** The timestamp indicating when this thread was created, represented in ISO 8601 format. **/
createdAt?: string;
created: number;
/** The timestamp indicating when this thread was updated, represented in ISO 8601 format. **/
updatedAt?: string;

/**
* @deprecated This field is deprecated and should not be used.
* Read from model file instead.
*/
modelId?: string;
updated: number;
metadata?: Record<string, unknown>;
}

export type ThreadState = {
hasMore: boolean;
waitingForResponse: boolean;
error?: Error;
lastMessage?: string;
};

/**
* Model type defines the shape of a model object.
* @stored
Expand Down Expand Up @@ -242,5 +276,5 @@ export type Assistant = {
instructions: string;
tools: any;
file_ids: string[];
metadata: Map<string, string>;
metadata?: Record<string, unknown>;
};
19 changes: 19 additions & 0 deletions electron/handlers/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,23 @@ export function handleFsIPCs() {

return result
})

/**
* Appends data to a file in the user data directory.
* @param event - The event object.
* @param path - The path of the file to append to.
* @param data - The data to append to the file.
* @returns A promise that resolves when the file has been written.
*/
ipcMain.handle('appendFile', async (_event, path: string, data: string) => {
return new Promise((resolve, reject) => {
fs.appendFile(join(userSpacePath, path), data, 'utf8', (err) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
})
}
7 changes: 6 additions & 1 deletion electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
* @property {Function} readFile - Reads the file at the given path.
* @property {Function} writeFile - Writes the given data to the file at the given path.
* @property {Function} listFiles - Lists the files in the directory at the given path.
* @property {Function} appendFile - Appends the given data to the file at the given path.
* @property {Function} mkdir - Creates a directory at the given path.
* @property {Function} rmdir - Removes a directory at the given path recursively.
* @property {Function} installRemotePlugin - Installs the remote plugin with the given name.
Expand Down Expand Up @@ -88,7 +89,8 @@ contextBridge.exposeInMainWorld('electronAPI', {

deleteFile: (filePath: string) => ipcRenderer.invoke('deleteFile', filePath),

isDirectory: (filePath: string) => ipcRenderer.invoke('isDirectory', filePath),
isDirectory: (filePath: string) =>
ipcRenderer.invoke('isDirectory', filePath),

getUserSpace: () => ipcRenderer.invoke('getUserSpace'),

Expand All @@ -99,6 +101,9 @@ contextBridge.exposeInMainWorld('electronAPI', {

listFiles: (path: string) => ipcRenderer.invoke('listFiles', path),

appendFile: (path: string, data: string) =>
ipcRenderer.invoke('appendFile', path, data),

mkdir: (path: string) => ipcRenderer.invoke('mkdir', path),

rmdir: (path: string) => ipcRenderer.invoke('rmdir', path),
Expand Down
101 changes: 63 additions & 38 deletions plugins/conversational-json/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { PluginType, fs } from '@janhq/core'
import { ConversationalPlugin } from '@janhq/core/lib/plugins'
import { Thread } from '@janhq/core/lib/types'
import { Thread, ThreadMessage } from '@janhq/core/lib/types'
import { join } from 'path'

/**
* JSONConversationalPlugin is a ConversationalPlugin implementation that provides
* functionality for managing conversations.
* functionality for managing threads.
*/
export default class JSONConversationalPlugin implements ConversationalPlugin {
private static readonly _homeDir = 'threads'
private static readonly _threadInfoFileName = 'thread.json'
private static readonly _threadMessagesFileName = 'messages.jsonl'

/**
* Returns the type of the plugin.
Expand All @@ -35,12 +37,12 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
/**
* Returns a Promise that resolves to an array of Conversation objects.
*/
async getConversations(): Promise<Thread[]> {
async getThreads(): Promise<Thread[]> {
try {
const convoIds = await this.getConversationDocs()
const threadIds = await this.getThreadDocs()

const promises = convoIds.map((conversationId) => {
return this.readConvo(conversationId)
const promises = threadIds.map((threadId) => {
return this.readThread(threadId)
})
const promiseResults = await Promise.allSettled(promises)
const convos = promiseResults
Expand All @@ -51,10 +53,9 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
})
.filter((convo) => convo != null)
convos.sort(
(a, b) =>
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime()
)
console.debug('getConversations: ', JSON.stringify(convos, null, 2))
console.debug('getThreads', JSON.stringify(convos, null, 2))
return convos
} catch (error) {
console.error(error)
Expand All @@ -63,55 +64,79 @@ export default class JSONConversationalPlugin implements ConversationalPlugin {
}

/**
* Saves a Conversation object to a Markdown file.
* @param conversation The Conversation object to save.
* Saves a Thread object to a json file.
* @param thread The Thread object to save.
*/
saveConversation(conversation: Thread): Promise<void> {
return fs
.mkdir(`${JSONConversationalPlugin._homeDir}/${conversation.id}`)
.then(() =>
fs.writeFile(
join(
JSONConversationalPlugin._homeDir,
conversation.id,
`${conversation.id}.json`
),
JSON.stringify(conversation, null, 2)
)
async saveThread(thread: Thread): Promise<void> {
try {
const threadDirPath = join(JSONConversationalPlugin._homeDir, thread.id)
const threadJsonPath = join(
threadDirPath,
JSONConversationalPlugin._threadInfoFileName
)
await fs.mkdir(threadDirPath)
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
Promise.resolve()
} catch (err) {
Promise.reject(err)
}
}

/**
* Deletes a conversation with the specified ID.
* @param conversationId The ID of the conversation to delete.
* Delete a thread with the specified ID.
* @param threadId The ID of the thread to delete.
*/
deleteConversation(conversationId: string): Promise<void> {
return fs.rmdir(
join(JSONConversationalPlugin._homeDir, `${conversationId}`)
)
deleteThread(threadId: string): Promise<void> {
return fs.rmdir(join(JSONConversationalPlugin._homeDir, `${threadId}`))
}

async addNewMessage(message: ThreadMessage): Promise<void> {
// check if message file exist
// if not, create file and write message
// if exist, append message
console.log('addNewMessage', message)
try {
const threadDirPath = join(
JSONConversationalPlugin._homeDir,
message.thread_id
)
const threadMessagePath = join(
threadDirPath,
JSONConversationalPlugin._threadMessagesFileName
)
await fs.mkdir(threadDirPath)
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
Promise.resolve()
} catch (err) {
Promise.reject(err)
}
}

/**
* A promise builder for reading a conversation from a file.
* @param convoId the conversation id we are reading from.
* @returns data of the conversation
* A promise builder for reading a thread from a file.
* @param threadId the thread id we are reading from.
* @returns data of the thread
*/
private async readConvo(convoId: string): Promise<any> {
private async readThread(threadId: string): Promise<any> {
return fs.readFile(
join(JSONConversationalPlugin._homeDir, convoId, `${convoId}.json`)
join(
JSONConversationalPlugin._homeDir,
threadId,
JSONConversationalPlugin._threadInfoFileName
)
)
}

/**
* Returns a Promise that resolves to an array of conversation IDs.
* The conversation IDs are the names of the Markdown files in the "conversations" directory.
* Returns a Promise that resolves to an array of thread IDs.
* The thread IDs are the names of the Markdown files in the "threads" directory.
* @private
*/
private async getConversationDocs(): Promise<string[]> {
private async getThreadDocs(): Promise<string[]> {
return fs
.listFiles(JSONConversationalPlugin._homeDir)
.then((files: string[]) => {
return Promise.all(files.filter((file) => file.startsWith('jan-')))
return Promise.all(files.filter((file) => file.startsWith('jan_')))
})
}
}
Loading

0 comments on commit 5dfc501

Please sign in to comment.