UNPKG

@websolutespa/payload-plugin-bowl-llm

Version:

LLM plugin for Bowl PayloadCms plugin

329 lines (328 loc) 12.2 kB
import { isObject } from '@websolutespa/bom-core'; import { textToChunksLastUnparsed } from './consts'; import { isChunkText } from './types'; export class MessageDecoder { decoder_ = new TextDecoder('utf-8'); lastUnparsedText_ = ''; set lastUnparsedText(lastUnparsedText) { // console.log('MessageService.lastUnparsedText.set', lastUnparsedText); this.lastUnparsedText_ = lastUnparsedText; } get lastUnparsedText() { return this.lastUnparsedText_; } constructor(){} bytesToChunks(byteArray) { let chunks = []; let text = ''; try { text = this.decoder_.decode(byteArray); chunks = this.textToChunks(text); } catch (error) { console.log('decodedChunk.error', error, text); } // console.log(chunks, text); return chunks; } textToChunks(text) { let chunks = []; let jsonText = ''; try { if (text.match(/(",|},|],)$/)) { let textToReplace = this.lastUnparsedText + text; if (textToReplace.match(/^(,)/)) { console.log('textToChunks.error', `incorrect line starting [${text}]`); textToReplace = textToReplace.substring(1); } const replacedText = textToReplace.replace(/(,$)/, ''); jsonText = `[${replacedText}]`; const jsonChunks = JSON.parse(jsonText); chunks = jsonChunks; this.lastUnparsedText = ''; } else { console.log('textToChunks.error', `incorrect line ending [${text}]`); this.lastUnparsedText += text; } } catch (error) { console.log('textToChunks.error', error); this.lastUnparsedText += text; } return chunks; } logChunks(chunks) { chunks.forEach((x)=>{ if (isObject(x)) { switch(x.type){ case 'error': console.error('LlmMessageService.error', x); break; case 'log': console.log('LlmMessageService.log', x); break; case 'header': console.log('LlmMessageService.header', x); break; case 'info': console.log('LlmMessageService.info', x); break; case 'end': console.log('LlmMessageService.end', x); break; default: } } }); } chunksToChunkItems(chunks, response) { const chunkItems = []; chunks.forEach((x)=>{ let lastMessage = chunkItems[chunkItems.length - 1]; if (isObject(x)) { switch(x.type){ case 'log': break; case 'error': chunkItems.push(x); break; case 'header': { const { type, ...rest } = x; if (response) { Object.assign(response, rest); } } break; case 'info': { if (response) { // !!! deprecated response.threadId = x.threadId || response.threadId; } } break; case 'end': break; default: chunkItems.push(x); } } else if (typeof x === 'string') { // console.warn('chunksToChunkItems typeof string'); if (!lastMessage) { lastMessage = { type: 'text', text: '' }; chunkItems.push(lastMessage); } if (isChunkText(lastMessage)) { const lastChunkText = lastMessage; if (chunkItems.indexOf(lastChunkText) > 0) { // secondary chunk lastChunkText.text += x; } else if (!lastChunkText.text.includes('\n')) { // uncompleted first chunk if (x.includes('\n')) { const lines = x.split('\n'); lines.forEach((line, i)=>{ const lastChar = lines.length > i + 1 ? '\n' : ''; if (i === 0) { lastChunkText.text += line + lastChar; } else if (line.length > 0) { chunkItems.push({ type: 'text', text: line + lastChar }); } }); } else { // uncompleted first chunk lastChunkText.text += x; } } else { // complete first chunk chunkItems.push({ type: 'text', text: x }); } } else { // new text chunk chunkItems.push({ type: 'text', text: x }); } } }); return chunkItems; } chunksToResponse(chunks) { const response = { chunks: [], threadId: '', messageId: '' }; response.chunks = this.chunksToChunkItems(chunks, response); return response; } decodeChunks(chunks) { const items = []; chunks.forEach((chunk)=>{ const decodedChunks = this.textToChunks(chunk); decodedChunks.forEach((decodedChunk)=>{ items.push(decodedChunk); }); }); return this.chunksToResponse(items); } } export class MessageService extends MessageDecoder { options; constructor(options){ super(); this.options = options; } get url() { return this.options.url; } lastMessage_; async sendMessage(request, onMessage, onEnd, onChunk) { // console.log('MessageServiceService.request', request); this.abort(); // console.log('MessageServiceService.request', request); const aborter = new AbortController(); const decodedChunks = []; this.lastMessage_ = { aborter, decodedChunks, onEnd }; const signal = aborter.signal; const url = this.options.url; let bytes = 0; try { const response = await fetch(url, { method: 'POST', body: JSON.stringify(request), headers: { 'Content-Type': 'application/json' }, signal }); if (response.body) { console.log('headers', Object.fromEntries(response.headers)); const threadId = response.headers.get('x-thread-id') || ''; const messageId = response.headers.get('x-msg-id') || ''; const header = { type: 'header', threadId, messageId }; const headerChunks = [ header ]; this.logChunks(headerChunks); decodedChunks.push(...headerChunks); if (typeof onChunk === 'function') { onChunk(headerChunks); } textToChunksLastUnparsed.text = ''; const reader = response.body.getReader(); this.lastMessage_.reader = reader; this.lastUnparsedText = ''; // console.log('lastUnparsedText.clear'); for await (const chunk of readChunks(reader)){ if (!signal.aborted) { bytes += chunk.length; const chunks = this.bytesToChunks(chunk); this.logChunks(chunks); decodedChunks.push(...chunks); const response = this.chunksToResponse(decodedChunks); if (typeof onMessage === 'function') { onMessage(response); } if (typeof onChunk === 'function') { onChunk(chunks); } } } if (!signal.aborted) { if (typeof onEnd === 'function') { const response = this.chunksToResponse(decodedChunks); onEnd(response); } } } } catch (error) { console.warn('MessageService.sendMessage.error', error); if (error === 'cancel') { if (typeof onEnd === 'function') { const response = this.chunksToResponse(decodedChunks); response.threadId = response.threadId || 'unknownThreadId'; onEnd(response); } return; } let errorMessage = typeof error === 'string' ? error : JSON.stringify(error, null, 2); if (error instanceof TypeError) { errorMessage = 'TypeError: Browser may not support async iteration'; } if (typeof onEnd === 'function') { const errorChunk = { type: 'error', error: errorMessage }; decodedChunks.push(errorChunk); const response = this.chunksToResponse(decodedChunks); onEnd(response); } // throw (error); } } abort() { const lastMessage = this.lastMessage_; if (lastMessage && lastMessage.aborter && !lastMessage.aborter.signal.aborted) { try { if (lastMessage.reader) { lastMessage.reader.cancel().catch((error)=>{ console.log('MessageService.abort.reader.error', error); }); } lastMessage.aborter.abort('cancel'); } catch (error) { console.log('MessageService.abort.error', error); } if (typeof lastMessage.onEnd === 'function') { const response = this.chunksToResponse(lastMessage.decodedChunks); lastMessage.onEnd(response); } } } } // readChunks() reads from the provided reader and yields the results into an async iterable function readChunks(reader) { return { async *[Symbol.asyncIterator] () { let readResult = await reader.read(); while(!readResult.done){ yield readResult.value; readResult = await reader.read(); } } }; } class Iterator { } Iterator.prototype[Symbol.asyncIterator] = async function*() { const reader = this.getReader(); try { while(true){ const { done, value } = await reader.read(); if (done) return; yield value; } } finally{ reader.releaseLock(); } }; //# sourceMappingURL=message.service.js.map