refactor to use pinia store for conversation state

The chat widget endpoint is now using stores/chat.ts to manage the conversation state. The connection state is managed by stores/connection.ts.
OpeyController /chat proxy has been removed and the socket connection is managed by the store instead.
OpeyController /token proxy is used to get a jwt token from the server when the chat widget is loaded.
The token is used to authenticate the socket connection.
This commit is contained in:
nemo 2024-08-09 17:56:54 +01:00
parent c58ed1fe14
commit baa978e44f
5 changed files with 201 additions and 108 deletions

View File

@ -39,80 +39,11 @@ import superagent from 'superagent'
@Service()
@Controller('/opey')
export class OpeyController {
private obpExplorerHome = process.env.VITE_OBP_API_EXPLORER_HOST
private chatBotUrl = process.env.VITE_CHATBOT_URL
private opeySecret = process.env.VITE_OPEY_SECRET
constructor(
private obpClientService: OBPClientService,
private oauthInjectedService: OauthInjectedService
) {}
@Post('/chat')
async chat(
@Session() session: any,
@Req() request: Request,
@Res() response: Response
): Response {
try {
const oauthService = this.oauthInjectedService
const consumer = oauthService.getConsumer()
// Get current user
const oauthConfig = session['clientConfig']
const version = this.obpClientService.getOBPVersion()
const currentUser = await this.obpClientService.get(`/obp/${version}/users/current`, oauthConfig)
const currentResponseKeys = Object.keys(currentUser)
// If current user is logged in, issue JWT signed with private key
if (currentResponseKeys.includes('user_id')) {
// sign
const jwtToken = this.generateJWT(currentUser.user_id, currentUser.username, session)
// Establish websocket connection
const ws = new WebSocket(`${this.chatBotUrl.replace('http', 'ws')}/chat`, {
headers: {
'Authorization': `Bearer ${jwtToken}`
}
});
// Send request with jwt token
ws.on('open', () => {
// Send request data
ws.send(JSON.stringify({
session_id: request.body.session_id,
message: request.body.message,
obp_api_host: request.body.obp_api_host,
}));
});
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
// Handle incoming message
console.log("Message delta: ", message);
response.write(JSON.stringify(message));
});
ws.on('close', () => {
response.end();
});
ws.on('error', (error) => {
console.error("WebSocket error: ", error);
response.status(500).json({ error: 'WebSocket Error' });
});
request.on('close', () => {
ws.close();
});
return response;
} else {
return response.status(400).json({ message: 'User not logged in, Authentication required' });
}
} catch (error) {
console.error("Error in chat endpoint: ", error);
return response.status(500).json({ error: 'Internal Server Error' });
}
}
@Post('/token')
async getToken(
@Session() session: any,
@ -120,8 +51,6 @@ export class OpeyController {
@Res() response: Response
): Response {
try {
const oauthService = this.oauthInjectedService
const consumer = oauthService.getConsumer()
// Get current user
const oauthConfig = session['clientConfig']
const version = this.obpClientService.getOBPVersion()

View File

@ -30,12 +30,14 @@
import MarkdownIt from "markdown-it";
import 'prismjs/themes/prism.css'; // Choose a theme you like
import { v4 as uuidv4 } from 'uuid';
import axios from 'axios';
import { inject, onMounted, ref } from 'vue';
import { inject } from 'vue';
import { obpApiHostKey } from '@/obp/keys';
import { getCurrentUser } from '../obp';
import { getOpeyJWT } from '@/obp/common-functions'
import { storeToRefs } from "pinia";
import { socket } from '@/socket';
import { Check, Close } from '@element-plus/icons-vue'
import { useConnectionStore } from '@/stores/connection';
import { useChatStore } from '@/stores/chat';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-javascript';
@ -47,11 +49,23 @@
import 'prismjs/themes/prism-okaidia.css';
export default {
setup() {
const chatStore = useChatStore();
const connectionStore = useConnectionStore();
socket.off()
chatStore.bindEvents();
connectionStore.bindEvents();
const { isStreaming, chatMessages, currentMessageSnapshot } = storeToRefs(chatStore);
return {isStreaming, chatMessages, currentMessageSnapshot, chatStore, connectionStore}
},
data() {
return {
isOpen: false,
userInput: '',
messages: [],
sessionId: uuidv4(),
isLoading: false,
obpApiHost: null,
@ -83,20 +97,52 @@
}
},
async establishWebSocketConnection() {
// Get the Opey JWT token
let token = ''
try {
token = await getOpeyJWT()
} catch (error) {
console.log(error)
token = ''
}
socket.auth = { token };
// Establish the WebSocket connection
console.log('Establishing WebSocket connection');
socket.connect();
this.connectionStore.connect(token)
},
async sendMessage() {
if (this.userInput.trim()) {
const newMessage = { role: 'user', content: this.userInput };
this.messages.push(newMessage);
this.chatMessages.push(newMessage);
this.userInput = '';
this.isLoading = true;
this.currentMessage = "",
// Send the user message to the backend and get the response
console.log('Sending message:', newMessage.content);
socket.emit('chat', {
session_id: this.sessionId,
message: newMessage.content,
obp_api_host: this.obpApiHost
});
socket.on('response stream start', (response) => {
this.isLoading = false;
});
/*
socket.on('response stream delta', (response) => {
this.isLoading = false;
console.log('Response:', response);
this.currentMessage += response.assistant;
});
*/
/*
try {
const response = await axios.post('/api/opey/chat', {
@ -112,10 +158,10 @@
console.log(`Response: ${response.status}`);
throw new Error("We're having trouble connecting you to Opey right now...");
}
this.messages.push({ role: 'assistant', content: response.data.reply });
this.chatMessages.push({ role: 'assistant', content: response.data.reply });
} catch (error) {
console.error('Error:', error);
this.messages.push({ role: 'error', content: "We're having trouble connecting you to Opey right now..."})
this.chatMessages.push({ role: 'error', content: "We're having trouble connecting you to Opey right now..."})
} finally {
this.isLoading = false;
}
@ -214,10 +260,13 @@
<img alt="Powered by OpenAI" src="@/assets/powered-by-openai-badge-outlined-on-dark.svg" height="32">
</div>
<div v-if="this.isLoggedIn" class="chat-messages" ref="messages">
<div v-for="(message, index) in messages" :key="index" :class="['chat-message', message.role]">
<div v-for="(message, index) in chatMessages" :key="index" :class="['chat-message', message.role]">
<div v-if="message.role=='error'">
<el-icon><Warning /></el-icon> <div v-html="renderMarkdown(message.content)"></div>
</div>
<div v-else-if="(this.isStreaming)&&(index === this.chatMessages.length -1)">
<div v-html="renderMarkdown(this.currentMessageSnapshot)"></div>
</div>
<div v-else>
<div v-html="renderMarkdown(message.content)"></div>
</div>

View File

@ -1,39 +1,39 @@
import { reactive } from "vue";
/*
* Open Bank Project - API Explorer II
* Copyright (C) 2023-2024, TESOBE GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Email: contact@tesobe.com
* TESOBE GmbH
* Osloerstrasse 16/17
* Berlin 13359, Germany
*
* This product includes software developed at
* TESOBE (http://www.tesobe.com/)
*
*/
import { io } from "socket.io-client";
import axios from 'axios';
import { getOpeyJWT } from './obp/common-functions'
export const state = reactive({
connected: false,
});
// "undefined" means the URL will be computed from the `window.location` object
const URL = import.meta.env.VITE_CHATBOT_URL
const token = await getOpeyJWT()
export const socket = io(
URL,
{
extraHeaders: {
Authorization: `Bearer ${token}`
}
autoConnect: false,
}
);
socket.on("connect", () => {
console.log("Websocket connection established");
state.connected = true;
});
socket.on('open', () => {
console.log('WebSocket connection established, authenticating...');
});
socket.on("disconnect", () => {
state.connected = false;
});
socket.on("message", (message) => {
console.log(message);
});

63
src/stores/chat.ts Normal file
View File

@ -0,0 +1,63 @@
/*
* Open Bank Project - API Explorer II
* Copyright (C) 2023-2024, TESOBE GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Email: contact@tesobe.com
* TESOBE GmbH
* Osloerstrasse 16/17
* Berlin 13359, Germany
*
* This product includes software developed at
* TESOBE (http://www.tesobe.com/)
*
*/
import { defineStore } from 'pinia'
import { socket } from '@/socket'
export const useChatStore = defineStore('chat', {
state: () => ({
chatMessages: [] as {role: string; content: string}[],
isStreaming: false,
currentMessageSnapshot: "" as string,
waitingForResponse: false,
}),
actions: {
bindEvents() {
socket.on("connect", () => {
console.log("Connected to chatbot");
})
socket.on('response stream start', (response) => {
this.isStreaming = true;
this.waitingForResponse = true;
this.chatMessages.push({ role: 'assistant', content: " "})
});
socket.on('response stream delta', (response) => {
this.currentMessageSnapshot += response.assistant;
});
socket.on('response stream end', (response) => {
this.isStreaming = false;
console.log(this.chatMessages[this.chatMessages.length - 1].content)
this.chatMessages[this.chatMessages.length - 1].content = this.currentMessageSnapshot
this.currentMessageSnapshot = ""
console.log(this.chatMessages)
});
}
}
})

52
src/stores/connection.ts Normal file
View File

@ -0,0 +1,52 @@
/*
* Open Bank Project - API Explorer II
* Copyright (C) 2023-2024, TESOBE GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Email: contact@tesobe.com
* TESOBE GmbH
* Osloerstrasse 16/17
* Berlin 13359, Germany
*
* This product includes software developed at
* TESOBE (http://www.tesobe.com/)
*
*/
import { defineStore } from "pinia";
import { socket } from "@/socket";
export const useConnectionStore = defineStore("connection", {
state: () => ({
isConnected: false,
}),
actions: {
bindEvents() {
socket.on("connect", () => {
this.isConnected = true;
});
socket.on("disconnect", () => {
this.isConnected = false;
});
},
connect(token: string) {
socket.auth = { token };
socket.connect();
}
},
});