NamH 84fb5ef346
fix(Thread): #1042 allow create new thread by clicking Use in Jan Hub (#1103)
Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
2023-12-19 17:06:57 +07:00

218 lines
6.0 KiB
TypeScript

import { ExtensionType, fs } from '@janhq/core'
import { ConversationalExtension } from '@janhq/core'
import { Thread, ThreadMessage } from '@janhq/core'
import { join } from 'path'
/**
* JSONConversationalExtension is a ConversationalExtension implementation that provides
* functionality for managing threads.
*/
export default class JSONConversationalExtension
implements ConversationalExtension
{
private static readonly _homeDir = 'threads'
private static readonly _threadInfoFileName = 'thread.json'
private static readonly _threadMessagesFileName = 'messages.jsonl'
/**
* Returns the type of the extension.
*/
type(): ExtensionType {
return ExtensionType.Conversational
}
/**
* Called when the extension is loaded.
*/
onLoad() {
fs.mkdir(JSONConversationalExtension._homeDir)
console.debug('JSONConversationalExtension loaded')
}
/**
* Called when the extension is unloaded.
*/
onUnload() {
console.debug('JSONConversationalExtension unloaded')
}
/**
* Returns a Promise that resolves to an array of Conversation objects.
*/
async getThreads(): Promise<Thread[]> {
try {
const threadDirs = await this.getValidThreadDirs()
const promises = threadDirs.map((dirName) => this.readThread(dirName))
const promiseResults = await Promise.allSettled(promises)
const convos = promiseResults
.map((result) => {
if (result.status === 'fulfilled') {
return JSON.parse(result.value) as Thread
}
})
.filter((convo) => convo != null)
convos.sort(
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime()
)
return convos
} catch (error) {
console.error(error)
return []
}
}
/**
* Saves a Thread object to a json file.
* @param thread The Thread object to save.
*/
async saveThread(thread: Thread): Promise<void> {
try {
const threadDirPath = join(
JSONConversationalExtension._homeDir,
thread.id
)
const threadJsonPath = join(
threadDirPath,
JSONConversationalExtension._threadInfoFileName
)
await fs.mkdir(threadDirPath)
await fs.writeFile(threadJsonPath, JSON.stringify(thread, null, 2))
Promise.resolve()
} catch (err) {
Promise.reject(err)
}
}
/**
* Delete a thread with the specified ID.
* @param threadId The ID of the thread to delete.
*/
deleteThread(threadId: string): Promise<void> {
return fs.rmdir(join(JSONConversationalExtension._homeDir, `${threadId}`))
}
async addNewMessage(message: ThreadMessage): Promise<void> {
try {
const threadDirPath = join(
JSONConversationalExtension._homeDir,
message.thread_id
)
const threadMessagePath = join(
threadDirPath,
JSONConversationalExtension._threadMessagesFileName
)
await fs.mkdir(threadDirPath)
await fs.appendFile(threadMessagePath, JSON.stringify(message) + '\n')
Promise.resolve()
} catch (err) {
Promise.reject(err)
}
}
async writeMessages(
threadId: string,
messages: ThreadMessage[]
): Promise<void> {
try {
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
const threadMessagePath = join(
threadDirPath,
JSONConversationalExtension._threadMessagesFileName
)
await fs.mkdir(threadDirPath)
await fs.writeFile(
threadMessagePath,
messages.map((msg) => JSON.stringify(msg)).join('\n') +
(messages.length ? '\n' : '')
)
Promise.resolve()
} catch (err) {
Promise.reject(err)
}
}
/**
* A promise builder for reading a thread from a file.
* @param threadDirName the thread dir we are reading from.
* @returns data of the thread
*/
private async readThread(threadDirName: string): Promise<any> {
return fs.readFile(
join(
JSONConversationalExtension._homeDir,
threadDirName,
JSONConversationalExtension._threadInfoFileName
)
)
}
/**
* Returns a Promise that resolves to an array of thread directories.
* @private
*/
private async getValidThreadDirs(): Promise<string[]> {
const fileInsideThread: string[] = await fs.listFiles(
JSONConversationalExtension._homeDir
)
const threadDirs: string[] = []
for (let i = 0; i < fileInsideThread.length; i++) {
const path = join(
JSONConversationalExtension._homeDir,
fileInsideThread[i]
)
const isDirectory = await fs.isDirectory(path)
if (!isDirectory) {
console.debug(`Ignore ${path} because it is not a directory`)
continue
}
const isHavingThreadInfo = (await fs.listFiles(path)).includes(
JSONConversationalExtension._threadInfoFileName
)
if (!isHavingThreadInfo) {
console.debug(`Ignore ${path} because it does not have thread info`)
continue
}
threadDirs.push(fileInsideThread[i])
}
return threadDirs
}
async getAllMessages(threadId: string): Promise<ThreadMessage[]> {
try {
const threadDirPath = join(JSONConversationalExtension._homeDir, threadId)
const isDir = await fs.isDirectory(threadDirPath)
if (!isDir) {
throw Error(`${threadDirPath} is not directory`)
}
const files: string[] = await fs.listFiles(threadDirPath)
if (
!files.includes(JSONConversationalExtension._threadMessagesFileName)
) {
throw Error(`${threadDirPath} not contains message file`)
}
const messageFilePath = join(
threadDirPath,
JSONConversationalExtension._threadMessagesFileName
)
const result = await fs.readLineByLine(messageFilePath)
const messages: ThreadMessage[] = []
result.forEach((line: string) => {
messages.push(JSON.parse(line) as ThreadMessage)
})
return messages
} catch (err) {
console.error(err)
return []
}
}
}