This commit is contained in:
Bastian Wagner
2026-06-23 15:46:57 +02:00
parent e543a2f4a1
commit 35613eddb6
2 changed files with 255 additions and 17 deletions

View File

@@ -120,13 +120,29 @@ export class AssistantService {
}
const response = await this.callMistralAgent(userId, messages, context);
const content = this.extractAssistantContent(response);
const actions = this.extractActions(response);
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 ?? '') &&
!actions.some((action) => action.type === 'list.created')
) {
return {
message: {
role: 'assistant',
content:
'Ich konnte die Liste nicht anlegen, weil der Listify-Connector keine Liste erstellt hat.',
},
actions: [],
};
}
if (!content) {
throw new ServiceUnavailableException('Mistral response was empty.');
}
@@ -144,9 +160,7 @@ export class AssistantService {
userId: string,
messages: AssistantChatMessage[],
): Promise<AssistantChatResponse | null> {
const latestUserMessage = [...messages]
.reverse()
.find((message) => message.role === 'user');
const latestUserMessage = this.latestUserMessage(messages);
const content = latestUserMessage?.content.toLowerCase() ?? '';
if (!this.isListReadRequest(content)) {
@@ -195,9 +209,7 @@ export class AssistantService {
return null;
}
const latestUserMessage = [...messages]
.reverse()
.find((message) => message.role === 'user');
const latestUserMessage = this.latestUserMessage(messages);
const itemTitle = this.extractItemTitleToAdd(latestUserMessage?.content ?? '');
if (!itemTitle) {
@@ -300,6 +312,29 @@ export class AssistantService {
return asksToRead && (asksForOpenLists || asksForOwnLists);
}
private isListCreationRequest(content: string): boolean {
const normalized = this.normalizeIntentText(content);
const mentionsList =
/\b(list|liste|listen|packliste|einkaufsliste|todo|aufgabenliste)\b/.test(
normalized,
);
const asksToCreate =
/\b(erstell|erstelle|erstellt|erzeugen|generier|generiere|mach|mache|bau|baue)\w*\b/.test(
normalized,
) || /\b(?:leg|lege)\s+.*\b(?:an|neu)\b/.test(normalized);
return mentionsList && asksToCreate;
}
private normalizeIntentText(content: string): string {
return content
.normalize('NFD')
.replace(/\p{Diacritic}/gu, '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
private isCompletedList(list: UserList): boolean {
return (
list.items.length > 0 && list.items.every((item) => item.checked === true)
@@ -364,7 +399,14 @@ export class AssistantService {
messages: [
...messages,
...(contextMessage ? [contextMessage] : []),
{ role: 'system', content: 'benutze immer den listify connector' },
{
role: 'system',
content: [
'Benutze fuer Listify-Daten immer den listify Connector.',
'Wenn der User eine Liste anlegen will, musst du create_list verwenden und darfst keinen JSON-Entwurf als Antwort ausgeben.',
'Wenn der Connector keine Liste erstellt, sage kurz, dass die Liste nicht angelegt werden konnte.',
].join(' '),
},
],
tools: [
{
@@ -445,7 +487,7 @@ export class AssistantService {
const response = responsePayload as MistralAgentCompletionResponse;
const directContent = response.choices?.[0]?.message?.content?.trim();
if (directContent) {
if (directContent && !this.looksLikeJson(directContent)) {
return directContent;
}
@@ -455,10 +497,9 @@ export class AssistantService {
.reverse();
for (const message of assistantMessages) {
const content =
typeof message.content === 'string' ? message.content.trim() : '';
const content = this.contentToText(message.content).trim();
if (content) {
if (content && !this.looksLikeJson(content)) {
return content;
}
}
@@ -492,7 +533,9 @@ export class AssistantService {
}
const structuredContent = message.metadata?.mcp_meta?.structuredContent;
const list = this.listFromStructuredContent(structuredContent);
const list =
this.listFromStructuredContent(structuredContent) ??
this.listFromToolContent(message.content);
if (!list) {
continue;
@@ -558,6 +601,78 @@ export class AssistantService {
return candidate as UserList;
}
private listFromToolContent(value: unknown): UserList | null {
for (const text of this.contentTextParts(value)) {
try {
const parsed = JSON.parse(text) as unknown;
const list = this.listFromStructuredContent(parsed);
if (list) {
return list;
}
} catch {
continue;
}
}
return null;
}
private contentToText(value: unknown): string {
return this.contentTextParts(value).join('\n');
}
private contentTextParts(value: unknown): string[] {
if (typeof value === 'string') {
return [value];
}
if (!Array.isArray(value)) {
return [];
}
return value
.map((part) => {
if (typeof part === 'string') {
return part;
}
if (!part || typeof part !== 'object' || Array.isArray(part)) {
return null;
}
const candidate = part as { text?: unknown };
return typeof candidate.text === 'string' ? candidate.text : null;
})
.filter((part): part is string => part !== null);
}
private looksLikeJson(content: string): boolean {
const trimmed = content.trim();
return (
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'))
);
}
private createActionContent(actions: AssistantAction[]): string | null {
const createdList = actions.find((action) => action.type === 'list.created');
if (createdList) {
return `Ich habe die Liste **${createdList.list.name}** angelegt.`;
}
const addedItem = actions.find((action) => action.type === 'list.item_added');
if (addedItem) {
return `Ich habe **${addedItem.itemTitle}** zu **${addedItem.list.name}** hinzugefuegt.`;
}
return null;
}
private lastItemTitle(list: UserList): string {
return list.items.at(-1)?.title ?? 'Eintrag';
}
@@ -606,6 +721,12 @@ export class AssistantService {
);
}
private latestUserMessage(
messages: AssistantChatMessage[],
): AssistantChatMessage | undefined {
return [...messages].reverse().find((message) => message.role === 'user');
}
private normalizeMessages(
messages: AssistantChatMessage[] | undefined,
): AssistantChatMessage[] {