mcp
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user