mcp
This commit is contained in:
@@ -14,6 +14,7 @@ describe('AssistantService', () => {
|
|||||||
};
|
};
|
||||||
let listsService: {
|
let listsService: {
|
||||||
listLists: jest.Mock;
|
listLists: jest.Mock;
|
||||||
|
addItem: jest.Mock;
|
||||||
};
|
};
|
||||||
let service: AssistantService;
|
let service: AssistantService;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ describe('AssistantService', () => {
|
|||||||
};
|
};
|
||||||
listsService = {
|
listsService = {
|
||||||
listLists: jest.fn(),
|
listLists: jest.fn(),
|
||||||
|
addItem: jest.fn(),
|
||||||
};
|
};
|
||||||
service = new AssistantService(
|
service = new AssistantService(
|
||||||
chatLogsRepository as never,
|
chatLogsRepository as never,
|
||||||
@@ -210,7 +212,7 @@ describe('AssistantService', () => {
|
|||||||
mockMistralResponse(providerResponse);
|
mockMistralResponse(providerResponse);
|
||||||
|
|
||||||
await service.chat('user-1', {
|
await service.chat('user-1', {
|
||||||
messages: [{ role: 'user', content: 'Fuege hier Brot hinzu' }],
|
messages: [{ role: 'user', content: 'Was ist hier wichtig?' }],
|
||||||
context: {
|
context: {
|
||||||
page: 'list_detail',
|
page: 'list_detail',
|
||||||
route: '/lists/list-1',
|
route: '/lists/list-1',
|
||||||
@@ -283,6 +285,84 @@ describe('AssistantService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds list items locally when the user writes onto the current list', async () => {
|
||||||
|
const updatedList = {
|
||||||
|
id: 'list-1',
|
||||||
|
ownerId: 'user-1',
|
||||||
|
accessRole: 'owner',
|
||||||
|
name: 'Einkauf',
|
||||||
|
kind: 'shopping',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'item-1',
|
||||||
|
title: 'Brot',
|
||||||
|
required: true,
|
||||||
|
checked: false,
|
||||||
|
position: 0,
|
||||||
|
createdAt: '2026-06-12T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-12T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
collaborators: [],
|
||||||
|
createdAt: '2026-06-12T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-06-12T00:00:00.000Z',
|
||||||
|
};
|
||||||
|
listsService.addItem.mockResolvedValue(updatedList);
|
||||||
|
|
||||||
|
const result = await service.chat('user-1', {
|
||||||
|
messages: [{ role: 'user', content: 'Fuege hier Brot hinzu' }],
|
||||||
|
context: {
|
||||||
|
page: 'list_detail',
|
||||||
|
route: '/lists/list-1',
|
||||||
|
list: {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled();
|
||||||
|
expect(listsService.addItem).toHaveBeenCalledWith('user-1', 'list-1', {
|
||||||
|
title: 'Brot',
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Ich habe **Brot** zu **Einkauf** hinzugefuegt.',
|
||||||
|
},
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'list.item_added',
|
||||||
|
listId: 'list-1',
|
||||||
|
itemTitle: 'Brot',
|
||||||
|
list: updatedList,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(listRealtimeService.publishSnapshot).toHaveBeenCalledWith(
|
||||||
|
'user-1',
|
||||||
|
updatedList,
|
||||||
|
);
|
||||||
|
expect(chatLogsRepository.save).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
provider: 'listify',
|
||||||
|
endpoint: 'local:addItem',
|
||||||
|
requestPayload: expect.objectContaining({
|
||||||
|
intent: 'list.add_item',
|
||||||
|
listId: 'list-1',
|
||||||
|
itemTitle: 'Brot',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('logs full failed provider responses before throwing', async () => {
|
it('logs full failed provider responses before throwing', async () => {
|
||||||
const providerResponse = {
|
const providerResponse = {
|
||||||
message: 'connector failed',
|
message: 'connector failed',
|
||||||
|
|||||||
@@ -109,6 +109,16 @@ export class AssistantService {
|
|||||||
return localResponse;
|
return localResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const localMutationResponse = await this.tryHandleLocalListMutation(
|
||||||
|
userId,
|
||||||
|
messages,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localMutationResponse) {
|
||||||
|
return localMutationResponse;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await this.callMistralAgent(userId, messages, context);
|
const response = await this.callMistralAgent(userId, messages, context);
|
||||||
const content = this.extractAssistantContent(response);
|
const content = this.extractAssistantContent(response);
|
||||||
const actions = this.extractActions(response);
|
const actions = this.extractActions(response);
|
||||||
@@ -176,6 +186,108 @@ export class AssistantService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async tryHandleLocalListMutation(
|
||||||
|
userId: string,
|
||||||
|
messages: AssistantChatMessage[],
|
||||||
|
context: NormalizedAssistantPageContext | null,
|
||||||
|
): Promise<AssistantChatResponse | null> {
|
||||||
|
if (context?.page !== 'list_detail') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestUserMessage = [...messages]
|
||||||
|
.reverse()
|
||||||
|
.find((message) => message.role === 'user');
|
||||||
|
const itemTitle = this.extractItemTitleToAdd(latestUserMessage?.content ?? '');
|
||||||
|
|
||||||
|
if (!itemTitle) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const list = await this.listsService.addItem(userId, context.list.id, {
|
||||||
|
title: itemTitle,
|
||||||
|
});
|
||||||
|
const assistantContent = `Ich habe **${itemTitle}** zu **${list.name}** hinzugefuegt.`;
|
||||||
|
const action: AssistantAction = {
|
||||||
|
type: 'list.item_added',
|
||||||
|
listId: list.id,
|
||||||
|
itemTitle,
|
||||||
|
list,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.listRealtimeService.publishSnapshot(userId, list);
|
||||||
|
|
||||||
|
await this.recordChatLog({
|
||||||
|
userId,
|
||||||
|
provider: 'listify',
|
||||||
|
endpoint: 'local:addItem',
|
||||||
|
agentId: null,
|
||||||
|
requestPayload: {
|
||||||
|
intent: 'list.add_item',
|
||||||
|
latestUserMessage: latestUserMessage?.content,
|
||||||
|
listId: context.list.id,
|
||||||
|
itemTitle,
|
||||||
|
},
|
||||||
|
responsePayload: { list },
|
||||||
|
statusCode: 200,
|
||||||
|
durationMs: Date.now() - startedAt,
|
||||||
|
assistantContent,
|
||||||
|
errorMessage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: assistantContent,
|
||||||
|
},
|
||||||
|
actions: [action],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractItemTitleToAdd(content: string): string | null {
|
||||||
|
const compacted = content.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
if (!compacted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = [
|
||||||
|
/\b(?:fuege|füge)\s+(.+?)\s+(?:hinzu|dazu|drauf|auf die liste|auf diese liste)\b/i,
|
||||||
|
/\b(?:schreib|schreibe|notier|notiere|pack|setze|setz)\s+(.+?)(?:\s+(?:drauf|dazu|hinzu|auf die liste|auf diese liste))?$/i,
|
||||||
|
/\b(.+?)\s+(?:draufschreiben|aufschreiben|hinzufuegen|hinzufügen)\b/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = compacted.match(pattern);
|
||||||
|
const title = this.cleanExtractedItemTitle(match?.[1]);
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanExtractedItemTitle(value: string | undefined): string | null {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = value
|
||||||
|
.replace(/^(?:bitte|noch|auch|mal|hier)\s+/i, '')
|
||||||
|
.replace(/\s+(?:bitte|noch|auch|mal)$/i, '')
|
||||||
|
.replace(/^["'`]+|["'`.,!?]+$/g, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!cleaned || cleaned.length > 220) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
private isListReadRequest(content: string): boolean {
|
private isListReadRequest(content: string): boolean {
|
||||||
const asksForLists = /\b(listen|liste)\b/.test(content);
|
const asksForLists = /\b(listen|liste)\b/.test(content);
|
||||||
const asksToRead =
|
const asksToRead =
|
||||||
|
|||||||
Reference in New Issue
Block a user