diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 9c367cd..4f15f52 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -25,7 +25,7 @@ * */ -import type { OpeyMessage, ChatStreamInput, RawOpeyMessage, OpeyToolCall, AssistantMessage } from '@/models/MessageModel' +import type { OpeyMessage, ChatStreamInput, RawOpeyMessage, OpeyToolCall, AssistantMessage, UserMessage } from '@/models/MessageModel' import type { Chat } from '@/models/ChatModel' import { getobpConsent } from '@/obp/common-functions' import { defineStore } from 'pinia' @@ -102,7 +102,10 @@ export const useChat = defineStore('chat', { for (const message of allMessages) { if (message.role === 'assistant') { const assistantMessage = message as AssistantMessage - return assistantMessage.toolCalls.find(tc => tc.toolCall.id === toolCallId) + const toolCallMatch = assistantMessage.toolCalls.find(tc => tc.toolCall.id === toolCallId) + if (toolCallMatch) { + return toolCallMatch + } } } @@ -118,7 +121,7 @@ export const useChat = defineStore('chat', { * * @param message - The message to add to the chat */ - async addMessage(message: OpeyMessage): Promise { + async addMessage(message: AssistantMessage | UserMessage): Promise { const existingMessage = this.messages.find(m => m.id === message.id); if (existingMessage) { @@ -273,7 +276,7 @@ export const useChat = defineStore('chat', { if (data.type === 'tool') { const toolCallId = content.tool_call_id; if (!toolCallId) { - throw new Error('Tool call ID not found in tool message'); + throw new Error('Tool call ID not found'); } console.log("Tool Message: ", toolCallId) @@ -281,7 +284,7 @@ export const useChat = defineStore('chat', { // get the tool call that the message refers to const toolMessage = this.getToolCallById(toolCallId); if (!toolMessage) { - throw new Error('Tool call for ID not found in messages'); + throw new Error('Tool call for this ID not found in messages'); } // Update the tool message with the content diff --git a/src/test/chat.test.ts b/src/test/chat.test.ts index d7fa40a..4edb91a 100644 --- a/src/test/chat.test.ts +++ b/src/test/chat.test.ts @@ -1,5 +1,5 @@ // Tesing the Pinia chat store in src/stores/chat.ts -import type { OpeyMessage, ToolMessage } from '@/models/MessageModel' +import type { AssistantMessage, OpeyMessage, OpeyToolCall, } from '@/models/MessageModel' import { ToolCall } from '@langchain/core/messages' import { useChat } from '@/stores/chat' import { beforeEach, describe, it, expect, vi } from 'vitest' @@ -176,45 +176,7 @@ describe('Chat Store _proccessOpeyStream', () => { .toThrow('Stream closed by server') }) - it('should be able a chunk with type: message and a tool call in the body', async () => { - // create a mock stream - const mockStream = new ReadableStream({ - start(controller) { - controller.enqueue(new TextEncoder().encode(`data: {"type": "message", "content": {"type": "ai", "content": "", "tool_calls": [{"name": "retrieve_glossary", "args": {"question": "hre"}, "id": "call_XsmUpPIeS81l9MYpieBZtr4w", "type": "tool_call"}], "tool_approval_request": false, "tool_call_id": null, "run_id": "d0c2bcbe-62f7-464b-8564-bf9263939fe1", "original": {"type": "ai", "data": {"content": "", "additional_kwargs": {"tool_calls": [{"index": 0, "id": "call_XsmUpPIeS81l9MYpieBZtr4w", "function": {"arguments": "{\\"question\\":\\"hre\\"}", "name": "retrieve_glossary"}, "type": "function"}]}, "response_metadata": {"finish_reason": "tool_calls", "model_name": "gpt-4o-2024-08-06", "system_fingerprint": "fp_eb9dce56a8"}, "type": "ai", "name": null, "id": "run-5bb065b9-440d-4678-bbdb-cd6de94a78d3", "example": false, "tool_calls": [{"name": "retrieve_glossary", "args": {"question": "hre"}, "id": "call_XsmUpPIeS81l9MYpieBZtr4w", "type": "tool_call"}], "invalid_tool_calls": [], "usage_metadata": null}}}}\n`)); - controller.close(); - }, - }); - - // mock the fetch function - global.fetch = vi.fn(() => - Promise.resolve(new Response(mockStream, { - headers: { 'content-type': 'text/event-stream' }, - status: 200, - })) - ); - - await chatStore._processOpeyStream(mockStream) - - expect(chatStore.messages).toHaveLength(1) - - const toolMessage: ToolMessage = chatStore.messages[0] as ToolMessage - - expect(toolMessage.awaitingApproval).toBe(false) - expect(toolMessage.toolCall).toBeTypeOf('object') - expect(toolMessage.pending).toBe(true) - // Instead of checking instance directly, verify the object has the expected properties - expect(toolMessage.toolCall).toEqual(expect.objectContaining({ - name: 'retrieve_glossary', - args: expect.objectContaining({ - question: 'hre' - }), - id: 'call_XsmUpPIeS81l9MYpieBZtr4w', - type: 'tool_call' - })) - expect(toolMessage.content).toBe('') - }) - - it('should associate tool call with current assistant message', async () => { + it('should be able to handle a chunk with type: message and a tool call in the body', async () => { // create a mock stream const mockStream = new ReadableStream({ start(controller) { @@ -235,16 +197,27 @@ describe('Chat Store _proccessOpeyStream', () => { id: '123', role: 'assistant', content: '', - } + toolCalls: [] + } await chatStore._processOpeyStream(mockStream) - expect(chatStore.messages).toHaveLength(1) - const toolMessage: ToolMessage = chatStore.messages[0] as ToolMessage + for (const toolCall of chatStore.currentAssistantMessage.toolCalls) { + expect(toolCall.status).toBe('pending') + expect(toolCall.toolCall).toBeDefined() + expect(toolCall.toolCall).toEqual(expect.objectContaining({ + id: 'call_XsmUpPIeS81l9MYpieBZtr4w', + type: 'tool_call', + args: expect.objectContaining({ + question: 'hre' + }), + name: 'retrieve_glossary' + + })) + } - expect(chatStore.currentAssistantMessage.toolCalls).toBeDefined() - expect(chatStore.currentAssistantMessage.toolCalls).toContain(toolMessage.toolCall) + }) it('should throw an error when the chunk is not valid json', async () => { @@ -308,6 +281,7 @@ describe('Chat Store _proccessOpeyStream', () => { id: '456', role: 'assistant', content: '', + toolCalls: [] } // Push assistant message to the messages list as this is what we do in the ChatWidget to visualise token streaming @@ -322,6 +296,8 @@ describe('Chat Store _proccessOpeyStream', () => { id: '456', role: 'assistant', content: mockAsisstantMessage, + loading: false, + toolCalls: [] }) @@ -357,6 +333,7 @@ describe('Chat Store _proccessOpeyStream', () => { id: '456', role: 'assistant', content: '', + toolCalls: [] } // Push assistant message to the messages list as this is what we do in the ChatWidget to visualise token streaming @@ -369,4 +346,134 @@ describe('Chat Store _proccessOpeyStream', () => { } expect(hasUniqueValues(chatStore.messages)).toBe(true) }) +}) + +describe('getToolCallById', () => { + let chatStore: ReturnType + + beforeEach(() => { + // Set the active Pinia store + setActivePinia(createPinia()) + chatStore = useChat() + }) + + it('should return the correct tool call', () => { + const toolCall: OpeyToolCall = { + status: 'pending', + toolCall: { + id: '123', + type: 'tool_call', + name: 'test', + args: { + question: 'test' + } + } + } + + chatStore.currentAssistantMessage = { + id: '456', + role: 'assistant', + content: '', + toolCalls: [toolCall] + } + + const result = chatStore.getToolCallById('123') + + expect(result).toEqual(toolCall) + }) + + it('should return undefined if the tool call is not found', () => { + const toolCall: OpeyToolCall = { + status: 'pending', + toolCall: { + id: '123', + type: 'tool_call', + name: 'test', + args: { + question: 'test' + } + } + } + + chatStore.currentAssistantMessage = { + id: '456', + role: 'assistant', + content: '', + toolCalls: [toolCall] + } + + const result = chatStore.getToolCallById('45asdfasdf6') + + expect(result).toBeUndefined() + }) + + it('should be able to get a tool call buried in the messages list', () => { + const toolCall: OpeyToolCall = { + status: 'pending', + toolCall: { + id: '151255', + type: 'tool_call', + name: 'test', + args: { + question: 'test' + } + } + } + + chatStore.addMessage({ + id: '123', + role: 'user', + content: 'hello', + isToolCallApproval: false + }) + + chatStore.addMessage({ + id: '420yy3', + role: 'assistant', + content: 'test', + toolCalls: [toolCall] + }) + + const result = chatStore.getToolCallById('151255') + + expect(result).toEqual(toolCall) + }) + + it('should be able to get a tool call buried DEEP in the messages list', () => { + + let toolCall: OpeyToolCall + + for (let i=0; i<6; i++) { + + chatStore.addMessage({ + id: `user_${i}`, + role: 'user', + content: 'hello', + isToolCallApproval: false + }) + + toolCall = { + status: 'pending', + toolCall: { + id: `tool_${i}`, + type: 'tool_call', + name: 'test', + args: { + question: 'test' + } + } + } + + chatStore.addMessage({ + id: `assistant_${i}`, + role: 'assistant', + content: 'test', + toolCalls: [toolCall] + }) + + } + + const result = chatStore.getToolCallById('tool_3') + expect(result).toBeDefined() + }) }) \ No newline at end of file diff --git a/vitest.config.js b/vitest.config.js index c7974d9..576f1e2 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -33,7 +33,7 @@ export default defineConfig({ environment: 'happy-dom', // Simulates a browser environment exclude:[ ...configDefaults.exclude, - //'**/backend-tests/*' + '**/integration/*' ], pool: "vmThreads", deps: {