fix tool messages not being found by ID

This commit is contained in:
Nemo Godebski-Pedersen 2025-03-26 19:03:26 +00:00
parent b192085aa3
commit 69c629c564
3 changed files with 161 additions and 51 deletions

View File

@ -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<void> {
async addMessage(message: AssistantMessage | UserMessage): Promise<void> {
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

View File

@ -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<Uint8Array>({
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<Uint8Array>({
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
@ -370,3 +347,133 @@ describe('Chat Store _proccessOpeyStream', () => {
expect(hasUniqueValues(chatStore.messages)).toBe(true)
})
})
describe('getToolCallById', () => {
let chatStore: ReturnType<typeof useChat>
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()
})
})

View File

@ -33,7 +33,7 @@ export default defineConfig({
environment: 'happy-dom', // Simulates a browser environment
exclude:[
...configDefaults.exclude,
//'**/backend-tests/*'
'**/integration/*'
],
pool: "vmThreads",
deps: {