This commit is contained in:
Bastian Wagner
2026-06-12 15:07:16 +02:00
parent a921095f3a
commit 6642575ea9
3 changed files with 192 additions and 5 deletions

View File

@@ -6,8 +6,11 @@ import {
import { InjectRepository } from '@nestjs/typeorm';
import { randomUUID } from 'crypto';
import { Repository } from 'typeorm';
import { UserList } from '../list-templates/list-template.types';
import { ListRealtimeService } from '../lists/list-realtime.service';
import { AssistantChatLogEntity } from './assistant-chat-log.entity';
import {
AssistantAction,
AssistantChatMessage,
AssistantChatRequest,
AssistantChatResponse,
@@ -21,6 +24,18 @@ interface MistralAgentCompletionResponse {
messages?: Array<{
role?: string;
content?: string | null | unknown[];
tool_call_id?: string;
tool_calls?: Array<{
id?: string;
function?: {
name?: string;
};
}>;
metadata?: {
mcp_meta?: {
structuredContent?: unknown;
};
};
}>;
}>;
}
@@ -32,6 +47,7 @@ export class AssistantService {
constructor(
@InjectRepository(AssistantChatLogEntity)
private readonly chatLogsRepository: Repository<AssistantChatLogEntity>,
private readonly listRealtimeService: ListRealtimeService,
) {}
async chat(
@@ -41,6 +57,11 @@ export class AssistantService {
const messages = this.normalizeMessages(request.messages);
const response = await this.callMistralAgent(userId, messages);
const content = this.extractAssistantContent(response);
const actions = this.extractActions(response);
actions.forEach((action) => {
this.listRealtimeService.publishSnapshot(userId, action.list);
});
if (!content) {
throw new ServiceUnavailableException('Mistral response was empty.');
@@ -51,7 +72,7 @@ export class AssistantService {
role: 'assistant',
content,
},
actions: [],
actions,
};
}
@@ -177,6 +198,116 @@ export class AssistantService {
return null;
}
private extractActions(responsePayload: unknown): AssistantAction[] {
if (!responsePayload || typeof responsePayload !== 'object') {
return [];
}
const actions: AssistantAction[] = [];
const response = responsePayload as MistralAgentCompletionResponse;
for (const choice of response.choices ?? []) {
const toolNamesById = new Map<string, string>();
for (const message of choice.messages ?? []) {
for (const toolCall of message.tool_calls ?? []) {
if (toolCall.id && toolCall.function?.name) {
toolNamesById.set(toolCall.id, toolCall.function.name);
}
}
}
for (const message of choice.messages ?? []) {
if (message.role !== 'tool') {
continue;
}
const structuredContent = message.metadata?.mcp_meta?.structuredContent;
const list = this.listFromStructuredContent(structuredContent);
if (!list) {
continue;
}
const toolName = message.tool_call_id
? toolNamesById.get(message.tool_call_id)
: undefined;
if (toolName?.includes('create_list')) {
actions.push({
type: 'list.created',
listId: list.id,
list,
});
continue;
}
if (toolName?.includes('add_list_item')) {
actions.push({
type: 'list.item_added',
listId: list.id,
itemTitle: this.lastItemTitle(list),
list,
});
continue;
}
actions.push({
type: 'list.item_added',
listId: list.id,
itemTitle: this.lastItemTitle(list),
list,
});
}
}
return this.uniqueActions(actions);
}
private listFromStructuredContent(value: unknown): UserList | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
const structuredContent = value as { list?: unknown };
const list = structuredContent.list;
if (!list || typeof list !== 'object' || Array.isArray(list)) {
return null;
}
const candidate = list as Partial<UserList>;
if (
typeof candidate.id !== 'string' ||
typeof candidate.name !== 'string' ||
!Array.isArray(candidate.items)
) {
return null;
}
return candidate as UserList;
}
private lastItemTitle(list: UserList): string {
return list.items.at(-1)?.title ?? 'Eintrag';
}
private uniqueActions(actions: AssistantAction[]): AssistantAction[] {
const seen = new Set<string>();
return actions.filter((action) => {
const key = `${action.type}:${action.listId}:${action.type === 'list.item_added' ? action.itemTitle : ''}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
private async recordChatLog(input: {
userId: string;
agentId: string;