@websolutespa/payload-plugin-bowl-llm
Version:
LLM plugin for Bowl PayloadCms plugin
329 lines (328 loc) • 12.2 kB
JavaScript
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