This commit is contained in:
Bastian Wagner
2026-06-24 10:53:07 +02:00
parent cbf1451255
commit 712136a5e9
2 changed files with 170 additions and 10 deletions

View File

@@ -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: [

View File

@@ -106,6 +106,13 @@ export class AssistantService {
): Promise<AssistantChatResponse> {
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<Set<string>> {
const lists = await this.listsService.listLists(userId);
return new Set(lists.map((list) => list.id));
}
private async addDetectedCreatedLists(
userId: string,
actions: AssistantAction[],
listIdsBeforeMistral: Set<string> | null,
): Promise<AssistantAction[]> {
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<AssistantChatLog[]> {
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,
});
}
}
}