mcp
This commit is contained in:
@@ -12,6 +12,9 @@ describe('AssistantService', () => {
|
||||
let listRealtimeService: {
|
||||
publishSnapshot: jest.Mock;
|
||||
};
|
||||
let listsService: {
|
||||
listLists: jest.Mock;
|
||||
};
|
||||
let service: AssistantService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -25,9 +28,13 @@ describe('AssistantService', () => {
|
||||
listRealtimeService = {
|
||||
publishSnapshot: jest.fn(),
|
||||
};
|
||||
listsService = {
|
||||
listLists: jest.fn(),
|
||||
};
|
||||
service = new AssistantService(
|
||||
chatLogsRepository as never,
|
||||
listRealtimeService as never,
|
||||
listsService as never,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -114,6 +121,82 @@ describe('AssistantService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('answers open list read requests locally without calling Mistral', async () => {
|
||||
listsService.listLists.mockResolvedValue([
|
||||
{
|
||||
id: 'list-1',
|
||||
ownerId: 'user-1',
|
||||
accessRole: 'owner',
|
||||
name: 'Einkauf',
|
||||
kind: 'shopping',
|
||||
items: [
|
||||
{
|
||||
id: 'item-1',
|
||||
title: 'Milch',
|
||||
required: true,
|
||||
checked: false,
|
||||
position: 0,
|
||||
createdAt: '2026-06-12T00:00:00.000Z',
|
||||
updatedAt: '2026-06-12T00:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'item-2',
|
||||
title: 'Brot',
|
||||
required: true,
|
||||
checked: true,
|
||||
position: 1,
|
||||
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',
|
||||
},
|
||||
{
|
||||
id: 'list-2',
|
||||
ownerId: 'user-1',
|
||||
accessRole: 'owner',
|
||||
name: 'Fertig',
|
||||
kind: 'todo',
|
||||
items: [
|
||||
{
|
||||
id: 'item-3',
|
||||
title: 'Done',
|
||||
required: true,
|
||||
checked: true,
|
||||
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',
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await service.chat('user-1', {
|
||||
messages: [{ role: 'user', content: 'Zeige mir meine offenen Listen' }],
|
||||
});
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
expect(result.message.content).toContain('Du hast 1 offene Liste');
|
||||
expect(result.message.content).toContain('**Einkauf**');
|
||||
expect(result.message.content).toContain('Milch');
|
||||
expect(result.message.content).not.toContain('Fertig');
|
||||
expect(chatLogsRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
userId: 'user-1',
|
||||
provider: 'listify',
|
||||
endpoint: 'local:listLists',
|
||||
agentId: null,
|
||||
statusCode: 200,
|
||||
assistantContent: expect.stringContaining('Einkauf'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a normalized list context system message when context is present', async () => {
|
||||
const providerResponse = {
|
||||
choices: [
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
UserListItem,
|
||||
} from '../list-templates/list-template.types';
|
||||
import { ListRealtimeService } from '../lists/list-realtime.service';
|
||||
import { ListsService } from '../lists/lists.service';
|
||||
import { AssistantChatLogEntity } from './assistant-chat-log.entity';
|
||||
import {
|
||||
AssistantAction,
|
||||
@@ -93,6 +94,7 @@ export class AssistantService {
|
||||
@InjectRepository(AssistantChatLogEntity)
|
||||
private readonly chatLogsRepository: Repository<AssistantChatLogEntity>,
|
||||
private readonly listRealtimeService: ListRealtimeService,
|
||||
private readonly listsService: ListsService,
|
||||
) {}
|
||||
|
||||
async chat(
|
||||
@@ -101,6 +103,12 @@ export class AssistantService {
|
||||
): Promise<AssistantChatResponse> {
|
||||
const messages = this.normalizeMessages(request.messages);
|
||||
const context = this.normalizeContext(request.context);
|
||||
const localResponse = await this.tryHandleLocalListQuery(userId, messages);
|
||||
|
||||
if (localResponse) {
|
||||
return localResponse;
|
||||
}
|
||||
|
||||
const response = await this.callMistralAgent(userId, messages, context);
|
||||
const content = this.extractAssistantContent(response);
|
||||
const actions = this.extractActions(response);
|
||||
@@ -122,6 +130,106 @@ export class AssistantService {
|
||||
};
|
||||
}
|
||||
|
||||
private async tryHandleLocalListQuery(
|
||||
userId: string,
|
||||
messages: AssistantChatMessage[],
|
||||
): Promise<AssistantChatResponse | null> {
|
||||
const latestUserMessage = [...messages]
|
||||
.reverse()
|
||||
.find((message) => message.role === 'user');
|
||||
const content = latestUserMessage?.content.toLowerCase() ?? '';
|
||||
|
||||
if (!this.isListReadRequest(content)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
const lists = await this.listsService.listLists(userId);
|
||||
const wantsOpenLists = /\boffen\w*\b/.test(content);
|
||||
const visibleLists = wantsOpenLists
|
||||
? lists.filter((list) => !this.isCompletedList(list))
|
||||
: lists;
|
||||
const assistantContent = this.formatListsAnswer(visibleLists, wantsOpenLists);
|
||||
|
||||
await this.recordChatLog({
|
||||
userId,
|
||||
provider: 'listify',
|
||||
endpoint: 'local:listLists',
|
||||
agentId: null,
|
||||
requestPayload: {
|
||||
intent: wantsOpenLists ? 'list.open_lists' : 'list.list_lists',
|
||||
latestUserMessage: latestUserMessage?.content,
|
||||
},
|
||||
responsePayload: { lists: visibleLists },
|
||||
statusCode: 200,
|
||||
durationMs: Date.now() - startedAt,
|
||||
assistantContent,
|
||||
errorMessage: null,
|
||||
});
|
||||
|
||||
return {
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: assistantContent,
|
||||
},
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
|
||||
private isListReadRequest(content: string): boolean {
|
||||
const asksForLists = /\b(listen|liste)\b/.test(content);
|
||||
const asksToRead =
|
||||
/\b(zeig|zeige|anzeigen|abrufen|auflisten|welche|was|habe|gib|nenn)\w*\b/.test(
|
||||
content,
|
||||
);
|
||||
const asksForOpenLists = /\boffen\w*\b/.test(content) && asksForLists;
|
||||
const asksForOwnLists = /\bmeine listen\b/.test(content);
|
||||
|
||||
return asksToRead && (asksForOpenLists || asksForOwnLists);
|
||||
}
|
||||
|
||||
private isCompletedList(list: UserList): boolean {
|
||||
return (
|
||||
list.items.length > 0 && list.items.every((item) => item.checked === true)
|
||||
);
|
||||
}
|
||||
|
||||
private formatListsAnswer(lists: UserList[], openOnly: boolean): string {
|
||||
if (lists.length === 0) {
|
||||
return openOnly
|
||||
? 'Du hast aktuell keine offenen Listen.'
|
||||
: 'Du hast aktuell keine Listen.';
|
||||
}
|
||||
|
||||
const heading = openOnly
|
||||
? `Du hast ${lists.length} offene ${lists.length === 1 ? 'Liste' : 'Listen'}:`
|
||||
: `Du hast ${lists.length} ${lists.length === 1 ? 'Liste' : 'Listen'}:`;
|
||||
const lines = [heading, ''];
|
||||
|
||||
for (const list of lists) {
|
||||
const checkedCount = list.items.filter((item) => item.checked).length;
|
||||
const openItems = list.items.filter((item) => !item.checked);
|
||||
const ownerSuffix =
|
||||
list.accessRole === 'collaborator'
|
||||
? `, geteilt von ${list.ownerName || list.ownerEmail || 'Owner'}`
|
||||
: '';
|
||||
|
||||
lines.push(
|
||||
`- **${list.name}** (${checkedCount}/${list.items.length} erledigt${ownerSuffix})`,
|
||||
);
|
||||
|
||||
for (const item of openItems.slice(0, 5)) {
|
||||
lines.push(` - ${item.title}`);
|
||||
}
|
||||
|
||||
if (openItems.length > 5) {
|
||||
lines.push(` - ${openItems.length - 5} weitere offene Punkte`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
private async callMistralAgent(
|
||||
userId: string,
|
||||
messages: AssistantChatMessage[],
|
||||
@@ -359,7 +467,9 @@ export class AssistantService {
|
||||
|
||||
private async recordChatLog(input: {
|
||||
userId: string;
|
||||
agentId: string;
|
||||
provider?: string;
|
||||
endpoint?: string;
|
||||
agentId: string | null;
|
||||
requestPayload: Record<string, unknown>;
|
||||
responsePayload: unknown;
|
||||
statusCode: number | null;
|
||||
@@ -371,8 +481,8 @@ export class AssistantService {
|
||||
this.chatLogsRepository.create({
|
||||
id: randomUUID(),
|
||||
userId: input.userId,
|
||||
provider: 'mistral',
|
||||
endpoint: this.endpoint,
|
||||
provider: input.provider ?? 'mistral',
|
||||
endpoint: input.endpoint ?? this.endpoint,
|
||||
agentId: input.agentId,
|
||||
statusCode: input.statusCode,
|
||||
durationMs: input.durationMs,
|
||||
|
||||
Reference in New Issue
Block a user