From 712136a5e9bf4abd044e19db752dbe2c80e9160e Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Wed, 24 Jun 2026 10:53:07 +0200 Subject: [PATCH] mcp --- .../src/assistant/assistant.service.spec.ts | 102 ++++++++++++++++++ .../src/assistant/assistant.service.ts | 78 ++++++++++++-- 2 files changed, 170 insertions(+), 10 deletions(-) diff --git a/listify-api/src/assistant/assistant.service.spec.ts b/listify-api/src/assistant/assistant.service.spec.ts index 1acb3be..aad48e7 100644 --- a/listify-api/src/assistant/assistant.service.spec.ts +++ b/listify-api/src/assistant/assistant.service.spec.ts @@ -40,6 +40,7 @@ describe('AssistantService', () => { listLists: jest.fn(), addItem: jest.fn(), }; + listsService.listLists.mockResolvedValue([]); service = new AssistantService( chatLogsRepository as never, listRealtimeService as never, @@ -570,6 +571,107 @@ describe('AssistantService', () => { ); }); + it('treats unknown list tool results as created lists for creation requests', async () => { + const createdList = { + id: 'list-1', + ownerId: 'user-1', + accessRole: 'owner', + name: 'Einkauf', + kind: 'shopping', + items: [], + collaborators: [], + createdAt: '2026-06-12T00:00:00.000Z', + updatedAt: '2026-06-12T00:00:00.000Z', + }; + const providerResponse = { + choices: [ + { + messages: [ + { + role: 'tool', + content: [ + { + type: 'text', + text: JSON.stringify({ list: createdList }), + }, + ], + }, + { + role: 'assistant', + content: 'Erledigt.', + }, + ], + }, + ], + }; + mockMistralResponse(providerResponse); + + const result = await service.chat('user-1', { + messages: [{ role: 'user', content: 'Erstelle eine Einkaufsliste' }], + }); + + expect(result.actions).toEqual([ + { + type: 'list.created', + listId: 'list-1', + list: createdList, + }, + ]); + expect(result.message.content).toBe('Erledigt.'); + expect(listRealtimeService.publishSnapshot).toHaveBeenCalledWith( + 'user-1', + createdList, + ); + }); + + it('detects lists created through the connector when the provider omits tool details', async () => { + const createdList = { + id: 'list-1', + ownerId: 'user-1', + accessRole: 'owner', + name: 'Einkauf', + kind: 'shopping', + items: [], + collaborators: [], + createdAt: '2026-06-12T00:00:00.000Z', + updatedAt: '2026-06-12T00:00:00.000Z', + }; + listsService.listLists + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([createdList]); + mockMistralResponse({ + choices: [ + { + message: { + content: 'Ich habe die Liste angelegt.', + }, + }, + ], + }); + + const result = await service.chat('user-1', { + messages: [{ role: 'user', content: 'Erstelle eine Einkaufsliste' }], + }); + + expect(result).toEqual({ + message: { + role: 'assistant', + content: 'Ich habe die Liste angelegt.', + }, + actions: [ + { + type: 'list.created', + listId: 'list-1', + list: createdList, + }, + ], + }); + expect(listRealtimeService.publishSnapshot).toHaveBeenCalledWith( + 'user-1', + createdList, + ); + }); + it('returns a clear message when list creation did not use the connector', async () => { mockMistralResponse({ choices: [ diff --git a/listify-api/src/assistant/assistant.service.ts b/listify-api/src/assistant/assistant.service.ts index 498ddb2..8f3023f 100644 --- a/listify-api/src/assistant/assistant.service.ts +++ b/listify-api/src/assistant/assistant.service.ts @@ -106,6 +106,13 @@ export class AssistantService { ): Promise { const messages = this.normalizeMessages(request.messages); const context = this.normalizeContext(request.context); + const latestUserMessage = this.latestUserMessage(messages); + const isCreationRequest = this.isListCreationRequest( + latestUserMessage?.content ?? '', + ); + const listIdsBeforeMistral = isCreationRequest + ? await this.listIdsForUser(userId) + : null; const localResponse = await this.tryHandleLocalListQuery(userId, messages); if (localResponse) { @@ -123,18 +130,24 @@ export class AssistantService { } const response = await this.callMistralAgent(userId, messages, context); - const actions = this.extractActions(response); + let actions = this.extractActions(response, { + assumeCreatedListForUnknownToolResult: isCreationRequest, + }); + actions = await this.addDetectedCreatedLists( + userId, + actions, + listIdsBeforeMistral, + ); const content = this.extractAssistantContent(response) ?? this.createActionContent(actions); - const latestUserMessage = this.latestUserMessage(messages); actions.forEach((action) => { this.listRealtimeService.publishSnapshot(userId, action.list); }); if ( - this.isListCreationRequest(latestUserMessage?.content ?? '') && + isCreationRequest && !actions.some((action) => action.type === 'list.created') ) { return { @@ -175,6 +188,40 @@ export class AssistantService { }; } + private async listIdsForUser(userId: string): Promise> { + const lists = await this.listsService.listLists(userId); + return new Set(lists.map((list) => list.id)); + } + + private async addDetectedCreatedLists( + userId: string, + actions: AssistantAction[], + listIdsBeforeMistral: Set | null, + ): Promise { + if (!listIdsBeforeMistral) { + return actions; + } + + const listsAfterMistral = await this.listsService.listLists(userId); + const existingCreatedIds = new Set( + actions + .filter((action) => action.type === 'list.created') + .map((action) => action.listId), + ); + const detectedActions = listsAfterMistral + .filter((list) => !listIdsBeforeMistral.has(list.id)) + .filter((list) => !existingCreatedIds.has(list.id)) + .map( + (list): AssistantAction => ({ + type: 'list.created', + listId: list.id, + list, + }), + ); + + return this.uniqueActions([...actions, ...detectedActions]); + } + async listChatLogs(userId: string): Promise { const logs = await this.chatLogsRepository.find({ where: { userId }, @@ -578,7 +625,10 @@ export class AssistantService { return null; } - private extractActions(responsePayload: unknown): AssistantAction[] { + private extractActions( + responsePayload: unknown, + options: { assumeCreatedListForUnknownToolResult?: boolean } = {}, + ): AssistantAction[] { if (!responsePayload || typeof responsePayload !== 'object') { return []; } @@ -634,12 +684,20 @@ export class AssistantService { continue; } - actions.push({ - type: 'list.item_added', - listId: list.id, - itemTitle: this.lastItemTitle(list), - list, - }); + if (options.assumeCreatedListForUnknownToolResult) { + actions.push({ + type: 'list.created', + listId: list.id, + list, + }); + } else { + actions.push({ + type: 'list.item_added', + listId: list.id, + itemTitle: this.lastItemTitle(list), + list, + }); + } } }