mirror of
https://github.com/OpenBankProject/API-Explorer-II.git
synced 2026-02-06 10:47:04 +00:00
migrate to pinia store
This commit is contained in:
parent
43c7934e0c
commit
74a45b8c83
@ -10,47 +10,45 @@ import ChatMessage from './ChatMessage.vue';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { OpeyStreamContext, OpeyMessage, UserMessage, sendOpeyMessage, getobpConsent } from '@/obp/opey-functions';
|
||||
import { getCurrentUser } from '@/obp';
|
||||
import { useChat } from '@/stores/chat';
|
||||
|
||||
export default {
|
||||
setup () {
|
||||
return {
|
||||
Close,
|
||||
ElTop
|
||||
ElTop,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chatOpen: false,
|
||||
thread_id: uuidv4(),
|
||||
input: '',
|
||||
lastUserMessasgeFailed: false,
|
||||
userHasConsented: false,
|
||||
opeyContext: reactive({
|
||||
currentAssistantMessage: {
|
||||
id: '',
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
},
|
||||
messages: new Array<OpeyMessage>(),
|
||||
status: 'ready'
|
||||
} as OpeyStreamContext),
|
||||
chat: useChat(),
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ChatMessage,
|
||||
},
|
||||
async mounted() {
|
||||
this.chat = useChat()
|
||||
const isLoggedIn = await this.checkLoginStatus()
|
||||
console.log('Is logged in: ', isLoggedIn)
|
||||
if (isLoggedIn) {
|
||||
this.initiateConsentFlow()
|
||||
try {
|
||||
await this.chat.handleAuthentication()
|
||||
} catch (error) {
|
||||
console.error('Error in chat:', error);
|
||||
ElMessage.error('Failed to authenticate.')
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async toggleChat() {
|
||||
this.chatOpen = !this.chatOpen
|
||||
if (!this.userHasConsented) {
|
||||
await this.initiateConsentFlow()
|
||||
if (!this.chat.userIsAuthenticated) {
|
||||
await this.chat.handleAuthentication()
|
||||
}
|
||||
},
|
||||
async checkLoginStatus(): Promise<boolean> {
|
||||
@ -62,22 +60,7 @@ export default {
|
||||
return false
|
||||
}
|
||||
},
|
||||
async initiateConsentFlow() {
|
||||
// get consent for Opey from user
|
||||
const consentResponse = await getobpConsent()
|
||||
|
||||
if (consentResponse) {
|
||||
const consentId = consentResponse.consent_id
|
||||
if (consentId) {
|
||||
this.userHasConsented = true
|
||||
ElMessage.success('Consent granted. You can now chat with Opey.')
|
||||
} else {
|
||||
ElMessage.error('Failed to grant consent. Please try again.')
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('Failed to grant consent. Please try again.')
|
||||
}
|
||||
},
|
||||
|
||||
async onSubmit() {
|
||||
// Add user message to the messages array
|
||||
const userMessage: UserMessage = {
|
||||
@ -86,18 +69,18 @@ export default {
|
||||
content: this.input,
|
||||
isToolCallApproval: false,
|
||||
};
|
||||
this.opeyContext.messages.push(userMessage);
|
||||
this.chat.addMessage(userMessage);
|
||||
|
||||
// Create a placeholder for the assistant's response
|
||||
this.opeyContext.currentAssistantMessage = {
|
||||
this.chat.currentAssistantMessage = {
|
||||
id: uuidv4(),
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
};
|
||||
this.opeyContext.messages.push(this.opeyContext.currentAssistantMessage);
|
||||
this.chat.addMessage(this.chat.currentAssistantMessage);
|
||||
|
||||
// Set status to loading
|
||||
this.opeyContext.status = 'loading';
|
||||
this.chat.status = 'loading';
|
||||
|
||||
// Clear input field after sending
|
||||
this.input = '';
|
||||
@ -107,21 +90,21 @@ export default {
|
||||
|
||||
|
||||
try {
|
||||
await sendOpeyMessage(
|
||||
userMessage,
|
||||
this.thread_id,
|
||||
this.opeyContext
|
||||
await this.chat.stream({
|
||||
message: userMessage,
|
||||
}
|
||||
|
||||
)
|
||||
console.log('Opey Status: ', this.opeyContext.status)
|
||||
console.log('Opey Status: ', this.chat.status)
|
||||
} catch (error) {
|
||||
console.error('Error in chat:', error);
|
||||
// on error, remove the assistant message placeholder, as it will be empty.
|
||||
this.opeyContext.messages = this.opeyContext.messages.filter(m => m.id !== this.opeyContext.currentAssistantMessage.id);
|
||||
this.chat.removeMessage(this.chat.currentAssistantMessage.id);
|
||||
this.lastUserMessasgeFailed = true;
|
||||
this.opeyContext.messages[this.opeyContext.messages.length - 1].error = "Failed to send message. Please try again.";
|
||||
this.chat.messages[this.chat.messages.length - 1].error = "Failed to send message. Please try again.";
|
||||
|
||||
} finally {
|
||||
this.opeyContext.status = 'ready';
|
||||
this.chat.status = 'ready';
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -148,14 +131,14 @@ export default {
|
||||
<el-main>
|
||||
<div class="messages-container">
|
||||
<el-scrollbar>
|
||||
<ChatMessage v-for="message in opeyContext.messages" :key="message.id" :message="message" />
|
||||
<ChatMessage v-for="message in chat.messages" :key="message.id" :message="message" />
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div class="user-input-container">
|
||||
<div class="user-input">
|
||||
<textarea v-model="input" type="textarea" placeholder="Type your message..." :disabled="opeyContext.status !== 'ready'" @keypress.enter="onSubmit" />
|
||||
<textarea v-model="input" type="textarea" placeholder="Type your message..." :disabled="chat.status !== 'ready'" @keypress.enter="onSubmit" />
|
||||
</div>
|
||||
<el-button type="primary" @click="onSubmit" color="#253047" :icon="ElTop" circle></el-button>
|
||||
</div>
|
||||
|
||||
@ -65,11 +65,13 @@ import { getCacheStorageInfo } from './obp/common-functions'
|
||||
fallbackLocale: 'ES',
|
||||
messages
|
||||
})
|
||||
app.provide('i18n', i18n)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.provide('i18n', i18n)
|
||||
app.use(ElementPlus)
|
||||
app.use(i18n)
|
||||
app.use(createPinia())
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
9
src/models/ChatModel.ts
Normal file
9
src/models/ChatModel.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { OpeyMessage } from "@/models/MessageModel"
|
||||
|
||||
export interface Chat {
|
||||
messages: OpeyMessage[];
|
||||
currentAssistantMessage: OpeyMessage;
|
||||
status: 'ready' | 'streaming' | 'error' | 'loading';
|
||||
userIsAuthenticated: boolean;
|
||||
threadId: string;
|
||||
}
|
||||
18
src/models/MessageModel.ts
Normal file
18
src/models/MessageModel.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export interface OpeyMessage {
|
||||
id: string; // i.e. UUID4
|
||||
role: string;
|
||||
content: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface UserMessage extends OpeyMessage {
|
||||
isToolCallApproval: boolean;
|
||||
}
|
||||
|
||||
export interface AssistantMessage extends OpeyMessage {
|
||||
// Probably we will need some fields here for tool call/ tool call approval requests
|
||||
}
|
||||
|
||||
export interface ChatStreamInput {
|
||||
message: UserMessage;
|
||||
}
|
||||
@ -25,53 +25,148 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { OpeyMessage, ChatStreamInput } from '@/models/MessageModel'
|
||||
import { Chat } from '@/models/ChatModel'
|
||||
import { getobpConsent, processOpeyStream } from '@/obp/opey-functions'
|
||||
import { defineStore } from 'pinia'
|
||||
import { socket } from '@/socket'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
/**
|
||||
* Represents a Pinia store for managing chat messages and chatbot responses.
|
||||
*/
|
||||
export const useChatStore = defineStore('chat', {
|
||||
state: () => ({
|
||||
// Messages a list of messages in the OpenAI format
|
||||
chatMessages: [] as {role: string; content: string}[],
|
||||
// Tells us wether a response from the chatbot is currently being streamed or not
|
||||
isStreaming: false,
|
||||
// The partial message at a particular moment in time
|
||||
currentMessageSnapshot: "" as string,
|
||||
lastError: "" as string,
|
||||
waitingForResponse: false,
|
||||
}),
|
||||
actions: {
|
||||
bindEvents() {
|
||||
// TODO: Maybe we don't need to log this except for DEBUG, keep same for now
|
||||
socket.on("connect", () => {
|
||||
console.log("Connected to chatbot");
|
||||
})
|
||||
export const useChat = defineStore('chat', {
|
||||
|
||||
// When the assistant stream response starts, we set isStreaming to true
|
||||
socket.on('response stream start', (response) => {
|
||||
this.isStreaming = true;
|
||||
this.waitingForResponse = true;
|
||||
// We create a temporary blank assistant message for the ChatWidget to render and add text deltas to when they come in
|
||||
this.chatMessages.push({ role: 'assistant', content: " "})
|
||||
});
|
||||
|
||||
// Text deltas received from the assistant stream (they are like little snippets of the generated response)
|
||||
socket.on('response stream delta', (response) => {
|
||||
this.currentMessageSnapshot += response.assistant;
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
this.lastError = error.error;
|
||||
console.error(error.error);
|
||||
})
|
||||
|
||||
socket.on('response stream end', (response) => {
|
||||
this.isStreaming = false;
|
||||
this.chatMessages[this.chatMessages.length - 1].content = this.currentMessageSnapshot
|
||||
this.currentMessageSnapshot = ""
|
||||
});
|
||||
state: (): Chat => {
|
||||
return {
|
||||
messages: [] as OpeyMessage[],
|
||||
currentAssistantMessage: {
|
||||
content: '',
|
||||
role: 'assistant',
|
||||
id: '',
|
||||
} as OpeyMessage,
|
||||
status: 'ready' as 'ready' | 'streaming' | 'error' | 'loading',
|
||||
userIsAuthenticated: false,
|
||||
threadId: '',
|
||||
}
|
||||
},
|
||||
|
||||
getters: {
|
||||
|
||||
/**
|
||||
* Retrieves or creates a thread ID for the chat.
|
||||
*
|
||||
* @param store - The store object that holds the thread ID
|
||||
* @param threadId - Optional thread ID to set
|
||||
* @returns The current or newly created thread ID
|
||||
*
|
||||
* If a threadId is provided, it will be set in the store and returned.
|
||||
* This is useful if the you want to match the thread ID on the chatbot server side.
|
||||
*
|
||||
* If no threadId is provided and none exists in the store, a new UUID will be generated.
|
||||
* Otherwise, the existing threadId from the store is returned.
|
||||
*/
|
||||
getThreadId: (store) => {
|
||||
return (id?: string): string => {
|
||||
if (id) {
|
||||
if (!store.threadId) {
|
||||
store.threadId = id
|
||||
return store.threadId
|
||||
} else {
|
||||
console.warn('Cannot set thread ID on already instantiated store. Create a new store instead.')
|
||||
return store.threadId
|
||||
}
|
||||
}
|
||||
|
||||
if (!store.threadId) {
|
||||
store.threadId = uuidv4()
|
||||
return store.threadId
|
||||
} else {
|
||||
return store.threadId
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* Adds a message to the chat.
|
||||
*
|
||||
* Works in a reducer-like fashion, updating the message if it already exists.
|
||||
*
|
||||
* @param message - The message to add to the chat
|
||||
*/
|
||||
async addMessage(message: OpeyMessage): Promise<void> {
|
||||
|
||||
const existingMessage = this.messages.find(m => m.id === message.id);
|
||||
if (existingMessage) {
|
||||
// Update the existing message
|
||||
existingMessage.content = message.content;
|
||||
|
||||
} else {
|
||||
// Add the new message
|
||||
this.messages.push(message);
|
||||
}
|
||||
},
|
||||
|
||||
async removeMessage(messageId: string): Promise<void> {
|
||||
this.messages = this.messages.filter(m => m.id !== messageId);
|
||||
},
|
||||
|
||||
async handleAuthentication(): Promise<void> {
|
||||
// Handle authentication
|
||||
// get consent for Opey from user
|
||||
const consentResponse = await getobpConsent()
|
||||
|
||||
if (consentResponse) {
|
||||
const consentId = consentResponse.consent_id
|
||||
if (consentId) {
|
||||
this.userIsAuthenticated = true
|
||||
} else {
|
||||
throw new Error('Failed to grant consent. Please try again.')
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to grant consent. Please try again.')
|
||||
}
|
||||
},
|
||||
|
||||
async stream(input: ChatStreamInput): Promise<void> {
|
||||
// Handle stream
|
||||
try {
|
||||
const response = await fetch('/api/opey/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
thread_id: this.threadId,
|
||||
message: input.message.content,
|
||||
is_tool_call_approval: input.message.isToolCallApproval
|
||||
})
|
||||
})
|
||||
|
||||
const stream = response.body;
|
||||
if (!stream) {
|
||||
throw new Error('No stream returned from API')
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Error sending Opey message: ${response.statusText}`);
|
||||
}
|
||||
|
||||
|
||||
let context = {
|
||||
currentAssistantMessage: this.currentAssistantMessage,
|
||||
messages: this.messages,
|
||||
status: this.status
|
||||
};
|
||||
|
||||
await processOpeyStream(stream, context);
|
||||
} catch (error) {
|
||||
console.error('Error sending Opey message:', error);
|
||||
this.status = 'ready';
|
||||
throw new Error(`Error sending Opey message: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
36
src/test/chat.test.ts
Normal file
36
src/test/chat.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// Tesing the Pinia chat store in src/stores/chat.ts
|
||||
|
||||
import { useChat } from '@/stores/chat'
|
||||
import { beforeEach, describe, it, expect } from 'vitest'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
|
||||
describe('Chat Store', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
// Set the active Pinia store
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('should be able to create its own thread ID', () => {
|
||||
const chatStore = useChat()
|
||||
const threadId = chatStore.getThreadId()
|
||||
expect(threadId).toBeDefined()
|
||||
expect(threadId).not.toBe('')
|
||||
})
|
||||
|
||||
it('should accept a set thread ID and not change it later', () => {
|
||||
const chatStore = useChat()
|
||||
const threadId = chatStore.getThreadId('1234')
|
||||
expect(chatStore.threadId).toBe('1234')
|
||||
const newThreadId = chatStore.getThreadId()
|
||||
expect(newThreadId).toBe('1234')
|
||||
})
|
||||
|
||||
it('should not change the thread ID if it is already set', () => {
|
||||
const chatStore = useChat()
|
||||
const threadId = chatStore.getThreadId('1234')
|
||||
expect(chatStore.threadId).toBe('1234')
|
||||
const newThreadId = chatStore.getThreadId('5678')
|
||||
expect(newThreadId).toBe('1234')
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user