feat: chat stream messages
This commit is contained in:
1 parent
bc71fa4d59
commit
5d2f7609f4
5 files changed
+131
-10
No files matched your search
@@ -107,7 +107,9 @@ class ChatMixin:
|
|||||||
now = int(datetime.now().timestamp())
|
now = int(datetime.now().timestamp())
|
||||||
|
|
||||||
if isinstance(content, Stream):
|
if isinstance(content, Stream):
|
||||||
initial_content = "..."
|
initial_format = format
|
||||||
|
format = format and "steam-" + format or "stream"
|
||||||
|
initial_content = str(content.id)
|
||||||
else:
|
else:
|
||||||
initial_content = content
|
initial_content = content
|
||||||
|
|
||||||
@@ -169,9 +171,11 @@ class ChatMixin:
|
|||||||
notify_users()
|
notify_users()
|
||||||
|
|
||||||
def chunk_fn (chunk):
|
def chunk_fn (chunk):
|
||||||
message.content = content.content + "..."
|
pass
|
||||||
async def end_fn ():
|
async def end_fn ():
|
||||||
|
message.format = initial_format
|
||||||
message.content = content.content
|
message.content = content.content
|
||||||
|
|
||||||
if db.is_instance(message):
|
if db.is_instance(message):
|
||||||
await message.save()
|
await message.save()
|
||||||
await notify_agents()
|
await notify_agents()
|
||||||
@@ -228,6 +232,7 @@ class ChatMixin:
|
|||||||
|
|
||||||
def notify_message_to_user(self, user_id, message, update=False):
|
def notify_message_to_user(self, user_id, message, update=False):
|
||||||
if user_id in self.user_connections:
|
if user_id in self.user_connections:
|
||||||
|
# TODO: Server's responsibility, and Message.to_json responsibility as well
|
||||||
user_message = {
|
user_message = {
|
||||||
'action': 'chat-message',
|
'action': 'chat-message',
|
||||||
'id': message.id,
|
'id': message.id,
|
||||||
@@ -235,6 +240,8 @@ class ChatMixin:
|
|||||||
'time': message.time,
|
'time': message.time,
|
||||||
'sender': message.sender,
|
'sender': message.sender,
|
||||||
'kind': message.kind,
|
'kind': message.kind,
|
||||||
|
'subkind': message.subkind,
|
||||||
|
'format': message.format,
|
||||||
'content': message.content,
|
'content': message.content,
|
||||||
'threadId': message.thread_id,
|
'threadId': message.thread_id,
|
||||||
'update': update,
|
'update': update,
|
||||||
|
|||||||
+26
-2
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import * as ui from './ui';
|
import * as ui from './ui';
|
||||||
|
import Stream from './stream';
|
||||||
|
|
||||||
export class ChatInfo {
|
export class ChatInfo {
|
||||||
constructor({ id, name }) {
|
constructor({ id, name }) {
|
||||||
@@ -42,6 +43,16 @@ function createDeferred() {
|
|||||||
return {promise, resolve, reject};
|
return {promise, resolve, reject};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createDeferredStream(callback) {
|
||||||
|
let resolve, reject;
|
||||||
|
const promise = new Promise((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
let deferred = {promise, resolve, reject};
|
||||||
|
return {promise, resolve (value) { callback(value, deferred) }, reject}
|
||||||
|
}
|
||||||
|
|
||||||
function initState () {
|
function initState () {
|
||||||
backendUrl = window.location.origin;
|
backendUrl = window.location.origin;
|
||||||
const tk = localStorage.getItem('loginToken');
|
const tk = localStorage.getItem('loginToken');
|
||||||
@@ -126,12 +137,12 @@ export async function newToken () {
|
|||||||
|
|
||||||
// --- Communication ---
|
// --- Communication ---
|
||||||
|
|
||||||
export function sendAction(action, body = {}) {
|
export function sendAction(action, body = {}, callback = null) {
|
||||||
if (socket?.readyState !== WebSocket.OPEN) {
|
if (socket?.readyState !== WebSocket.OPEN) {
|
||||||
return Promise.reject(new Error("WebSocket is not open. Cannot send action."));
|
return Promise.reject(new Error("WebSocket is not open. Cannot send action."));
|
||||||
}
|
}
|
||||||
const nonce = nextNonce++;
|
const nonce = nextNonce++;
|
||||||
const deferred = createDeferred();
|
const deferred = callback ? createDeferredStream(callback) : createDeferred();
|
||||||
|
|
||||||
pendingActions[nonce] = deferred;
|
pendingActions[nonce] = deferred;
|
||||||
deferred.promise.finally(() => {
|
deferred.promise.finally(() => {
|
||||||
@@ -320,6 +331,19 @@ export async function getAgent(agentId) {
|
|||||||
return allAgents.find(it => it.id == agentId);
|
return allAgents.find(it => it.id == agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function readStream(streamId) {
|
||||||
|
let stream = new Stream(streamId);
|
||||||
|
sendAction("read-stream", {streamId}, (packet, deferred) => {
|
||||||
|
if (packet.eof) {
|
||||||
|
stream.close();
|
||||||
|
deferred.resolve();
|
||||||
|
} else if (packet.data != null) {
|
||||||
|
stream.feed(packet.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
export function invalidateCache({ chats=false, agents=false } = {}) {
|
export function invalidateCache({ chats=false, agents=false } = {}) {
|
||||||
if (chats) cache.chats = null;
|
if (chats) cache.chats = null;
|
||||||
if (agents) cache.agents = null;
|
if (agents) cache.agents = null;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
|
import * as backend from '../backend';
|
||||||
marked.use({
|
marked.use({
|
||||||
renderer: {
|
renderer: {
|
||||||
code (code, infostring, escaped) {
|
code (code, infostring, escaped) {
|
||||||
@@ -36,6 +38,31 @@ function chatEntryClass(msg) {
|
|||||||
return ['info'];
|
return ['info'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const format = ref(null);
|
||||||
|
const content = ref("");
|
||||||
|
|
||||||
|
let currentStreamId = null;
|
||||||
|
|
||||||
|
watch(() => [props.message.format, props.message.content], ([newFormat, newContent]) => {
|
||||||
|
// TODO: if format.startsWith("stream:")
|
||||||
|
if (newFormat == "stream") {
|
||||||
|
format.value = null;
|
||||||
|
currentStreamId = parseInt(newContent);
|
||||||
|
|
||||||
|
let stream = backend.readStream(currentStreamId);
|
||||||
|
stream.on('data', (chunk) => {
|
||||||
|
if (stream.id == currentStreamId) {
|
||||||
|
content.value = stream.partialContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
format.value = newFormat;
|
||||||
|
content.value = newContent;
|
||||||
|
currentStreamId = null;
|
||||||
|
}
|
||||||
|
}, {immediate: true})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -43,18 +70,18 @@ function chatEntryClass(msg) {
|
|||||||
<template v-if="message.type == 'message' || message.type == null">
|
<template v-if="message.type == 'message' || message.type == null">
|
||||||
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
|
<div v-if="message.sender != 'user'" class="sender">{{message.sender}}</div>
|
||||||
|
|
||||||
<div v-if="message.format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div>
|
<div v-if="format == 'markdown'" v-html="marked.parse(message.content)" class="content"></div>
|
||||||
<div v-else class="content">{{message.content}}</div>
|
<div v-else class="content">{{content}}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="message.type == 'note'">
|
<template v-if="message.type == 'note'">
|
||||||
<div class="sender">{{message.sender}}</div>
|
<div class="sender">{{message.sender}}</div>
|
||||||
<div class="content">{{message.content}}</div>
|
<div class="content">{{content}}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="message.type == 'log'">
|
<template v-if="message.type == 'log'">
|
||||||
<div class="sender">Log ({{message.sender}})</div>
|
<div class="sender">Log ({{message.sender}})</div>
|
||||||
<div class="content">{{message.content}}</div>
|
<div class="content">{{content}}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="message.type == 'thread-header'">
|
<template v-if="message.type == 'thread-header'">
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export default class Stream {
|
||||||
|
constructor (id) {
|
||||||
|
this.id = id;
|
||||||
|
this.partialContent = "";
|
||||||
|
this.eof = false;
|
||||||
|
|
||||||
|
this.listeners = {
|
||||||
|
data: [],
|
||||||
|
end: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let onResponse = (msg) => {
|
||||||
|
if (this.eof) return;
|
||||||
|
|
||||||
|
if (msg.eof) {
|
||||||
|
this.eof = true;
|
||||||
|
this.handle('end');
|
||||||
|
} else {
|
||||||
|
this.partialContent += msg.data;
|
||||||
|
this.handle('data', msg.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.contentPromise = new Promise((resolve, reject) => {
|
||||||
|
this.handle("end", () => {
|
||||||
|
resolve(this.partialContent);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
feed (chunk) {
|
||||||
|
if (this.eof) return;
|
||||||
|
this.partialContent += chunk;
|
||||||
|
this.handle('data', chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
close () {
|
||||||
|
this.eof = true;
|
||||||
|
this.handle('end', this.partialContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
getContent () {
|
||||||
|
return this.contentPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle (event, ...args) {
|
||||||
|
for (let fn of this.listeners[event]) {
|
||||||
|
fn(...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on (event, callback) {
|
||||||
|
this.listeners[event].push(callback);
|
||||||
|
|
||||||
|
if (event == "data" && this.partialContent) {
|
||||||
|
callback(this.partialContent);
|
||||||
|
}
|
||||||
|
if (event == "end" && this.eof) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,7 @@
|
|||||||
for (let entry of messages.value) {
|
for (let entry of messages.value) {
|
||||||
if (entry.id == it.id) {
|
if (entry.id == it.id) {
|
||||||
entry.type = it.kind;
|
entry.type = it.kind;
|
||||||
|
entry.format = it.format;
|
||||||
entry.content = it.content;
|
entry.content = it.content;
|
||||||
entry.threadId = it.threadId;
|
entry.threadId = it.threadId;
|
||||||
return;
|
return;
|
||||||
@@ -108,6 +109,7 @@
|
|||||||
addMessage({
|
addMessage({
|
||||||
id: it.id,
|
id: it.id,
|
||||||
type: it.kind,
|
type: it.kind,
|
||||||
|
format: it.format,
|
||||||
sender: it.sender,
|
sender: it.sender,
|
||||||
content: it.content,
|
content: it.content,
|
||||||
threadId: it.threadId,
|
threadId: it.threadId,
|
||||||
@@ -163,7 +165,6 @@
|
|||||||
}
|
}
|
||||||
runListener.addRunId(event.runId);
|
runListener.addRunId(event.runId);
|
||||||
messageRunMap.value[event.messageId] = event.runId;
|
messageRunMap.value[event.messageId] = event.runId;
|
||||||
ui.alert("Chat Run Message: " + JSON.stringify(event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRunUpdate (newRun) {
|
function onRunUpdate (newRun) {
|
||||||
@@ -335,7 +336,7 @@
|
|||||||
|
|
||||||
async function startNewThread () {
|
async function startNewThread () {
|
||||||
const res = await backend.sendAction("create-chat-thread", {chatId});
|
const res = await backend.sendAction("create-chat-thread", {chatId});
|
||||||
console.log("Starting a nedeleteChatw thread", res);
|
console.log("Starting a new thread", res);
|
||||||
if (res.threadId != null) {
|
if (res.threadId != null) {
|
||||||
currentThreadId.value = res.threadId;
|
currentThreadId.value = res.threadId;
|
||||||
expandThread(res.threadId, true);
|
expandThread(res.threadId, true);
|
||||||
|
|||||||
Reference in new issue
Block a user