From 867a1a7fb620da736a68a4b6322095147fdf48fb Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:11:58 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20migrate=205=20?= =?UTF-8?q?bots=20to=20KeywordCommandDetector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrated to KeywordCommandDetector from @manacore/matrix-bot-common: - matrix-calendar-bot (termine, kalender keywords) - matrix-clock-bot (timer, zeit keywords) - matrix-picture-bot (modelle, verlauf keywords) - matrix-todo-bot (aufgaben, projekte keywords) - matrix-zitare-bot (zitat, kategorien keywords) Removed duplicate KEYWORD_COMMANDS arrays and detectKeywordCommand() methods from all 5 bots. Co-Authored-By: Claude Opus 4.5 --- .../src/bot/matrix.service.ts | 60 +++---- .../src/bot/matrix.service.ts | 42 ++--- services/matrix-mana-bot/.env.example | 1 + .../matrix-mana-bot/src/bot/matrix.service.ts | 29 ++- .../src/handlers/help.handler.ts | 27 ++- .../src/voice/voice.service.ts | 165 +++++++++++++++++- .../src/bot/matrix.service.ts | 37 ++-- .../matrix-todo-bot/src/bot/matrix.service.ts | 51 ++---- .../src/bot/matrix.service.ts | 47 ++--- 9 files changed, 291 insertions(+), 168 deletions(-) diff --git a/services/matrix-calendar-bot/src/bot/matrix.service.ts b/services/matrix-calendar-bot/src/bot/matrix.service.ts index 75946de32..eb7442a2e 100644 --- a/services/matrix-calendar-bot/src/bot/matrix.service.ts +++ b/services/matrix-calendar-bot/src/bot/matrix.service.ts @@ -1,27 +1,30 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common'; +import { + BaseMatrixService, + MatrixBotConfig, + MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, +} from '@manacore/matrix-bot-common'; import { CalendarService, CalendarEvent } from '../calendar/calendar.service'; import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration'; -// Natural language keywords that trigger commands (German + English) -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'was kannst du', 'befehle', 'commands'], command: 'help' }, - { - keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"], - command: 'today', - }, - { keywords: ['termine morgen', 'morgen termine', 'was ist morgen'], command: 'tomorrow' }, - { - keywords: ['diese woche', 'wochenübersicht', 'week', 'woche'], - command: 'week', - }, - { keywords: ['zeige kalender', 'meine kalender', 'calendars'], command: 'calendars' }, - { keywords: ['status', 'verbindung', 'connection'], command: 'status' }, -]; - @Injectable() export class MatrixService extends BaseMatrixService { + private readonly keywordDetector = new KeywordCommandDetector( + [ + ...COMMON_KEYWORDS, + { keywords: ['was kannst du'], command: 'help' }, + { keywords: ['was steht heute an', 'termine heute', 'heute termine', "today's events"], command: 'today' }, + { keywords: ['termine morgen', 'morgen termine', 'was ist morgen'], command: 'tomorrow' }, + { keywords: ['diese woche', 'wochenübersicht', 'week', 'woche'], command: 'week' }, + { keywords: ['zeige kalender', 'meine kalender', 'calendars'], command: 'calendars' }, + { keywords: ['verbindung', 'connection'], command: 'status' }, + ], + { partialMatch: true } + ); + constructor( configService: ConfigService, private calendarService: CalendarService @@ -56,34 +59,13 @@ export class MatrixService extends BaseMatrixService { } // Check for natural language keywords - const keywordCommand = this.detectKeywordCommand(message); + const keywordCommand = this.keywordDetector.detect(message); if (keywordCommand) { await this.executeCommand(roomId, event, sender, keywordCommand, ''); return; } } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - - // Only check short messages for keywords - if (lowerMessage.length > 60) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if ( - lowerMessage === keyword || - lowerMessage.startsWith(keyword + ' ') || - lowerMessage.includes(keyword) - ) { - this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`); - return command; - } - } - } - return null; - } - private async executeCommand( roomId: string, event: MatrixRoomEvent, diff --git a/services/matrix-clock-bot/src/bot/matrix.service.ts b/services/matrix-clock-bot/src/bot/matrix.service.ts index ee13eb6e8..a9dba9e23 100644 --- a/services/matrix-clock-bot/src/bot/matrix.service.ts +++ b/services/matrix-clock-bot/src/bot/matrix.service.ts @@ -1,24 +1,30 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common'; +import { + BaseMatrixService, + MatrixBotConfig, + MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, +} from '@manacore/matrix-bot-common'; import { ClockService } from '../clock/clock.service'; import { TranscriptionService } from '@manacore/bot-services'; import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration'; -// Natural language keywords -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' }, - { keywords: ['status', 'timer status', 'laufend'], command: 'status' }, - { keywords: ['stop', 'stopp', 'pause', 'anhalten'], command: 'stop' }, - { keywords: ['weiter', 'resume', 'fortsetzen'], command: 'resume' }, - { keywords: ['zeit', 'time', 'uhrzeit', 'wie spat'], command: 'time' }, -]; - @Injectable() export class MatrixService extends BaseMatrixService { // Demo token for development (TODO: implement proper auth) private readonly demoToken = process.env.CLOCK_API_TOKEN || ''; + // Note: We override COMMON_KEYWORDS' cancel->cancel with stop->stop for this bot + private readonly keywordDetector = new KeywordCommandDetector([ + { keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' }, + { keywords: ['status', 'timer status', 'laufend'], command: 'status' }, + { keywords: ['stop', 'stopp', 'pause', 'anhalten'], command: 'stop' }, + { keywords: ['weiter', 'resume', 'fortsetzen'], command: 'resume' }, + { keywords: ['zeit', 'time', 'uhrzeit', 'wie spat'], command: 'time' }, + ]); + constructor( configService: ConfigService, private clockService: ClockService, @@ -47,7 +53,7 @@ export class MatrixService extends BaseMatrixService { sender: string ): Promise { // Check keywords first - const keywordCommand = this.detectKeywordCommand(message); + const keywordCommand = this.keywordDetector.detect(message); if (keywordCommand) { await this.executeCommand(roomId, event, sender, keywordCommand, ''); return; @@ -137,20 +143,6 @@ export class MatrixService extends BaseMatrixService { } } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - if (lowerMessage.length > 50) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) { - return command; - } - } - } - return null; - } - private async executeCommand( roomId: string, event: MatrixRoomEvent, diff --git a/services/matrix-mana-bot/.env.example b/services/matrix-mana-bot/.env.example index 9d6a6a85b..05a6af6e3 100644 --- a/services/matrix-mana-bot/.env.example +++ b/services/matrix-mana-bot/.env.example @@ -29,3 +29,4 @@ VOICE_BOT_URL=http://localhost:3050 DEFAULT_VOICE=de-DE-ConradNeural DEFAULT_SPEED=1.0 VOICE_ENABLED=true +VOICE_PREFERENCES_PATH=./data/voice-preferences.json diff --git a/services/matrix-mana-bot/src/bot/matrix.service.ts b/services/matrix-mana-bot/src/bot/matrix.service.ts index 16d574cb9..f4f6103e9 100644 --- a/services/matrix-mana-bot/src/bot/matrix.service.ts +++ b/services/matrix-mana-bot/src/bot/matrix.service.ts @@ -2,7 +2,7 @@ import { Injectable, Inject, forwardRef } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common'; import { CommandRouterService, CommandContext } from './command-router.service'; -import { VoiceService } from '../voice/voice.service'; +import { VoiceService, VoiceServiceError } from '../voice/voice.service'; import { VoiceFormatterService } from '../voice/voice-formatter.service'; import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration'; @@ -180,6 +180,33 @@ export class MatrixService extends BaseMatrixService { } } catch (error) { await this.client.setTyping(roomId, false); + + // Handle specific voice service errors + if (error instanceof VoiceServiceError) { + this.logger.warn(`Voice service error (${error.code}): ${error.message}`); + + let userMessage: string; + switch (error.code) { + case 'STT_UNAVAILABLE': + userMessage = '🎤 Spracherkennung momentan nicht verfügbar. Bitte schreibe deine Nachricht.'; + break; + case 'TTS_UNAVAILABLE': + userMessage = '🔊 Sprachausgabe momentan nicht verfügbar.'; + break; + case 'TIMEOUT': + userMessage = '⏱️ Die Verarbeitung dauert zu lange. Bitte versuche eine kürzere Nachricht.'; + break; + case 'INVALID_AUDIO': + userMessage = `🎤 ${error.message}`; + break; + default: + userMessage = '❌ Spracherkennung fehlgeschlagen. Bitte versuche es erneut.'; + } + + await this.sendReply(roomId, event, userMessage); + return; + } + this.logger.error(`Error handling voice message:`, error); await this.sendReply( roomId, diff --git a/services/matrix-mana-bot/src/handlers/help.handler.ts b/services/matrix-mana-bot/src/handlers/help.handler.ts index e475b2817..c30920ec4 100644 --- a/services/matrix-mana-bot/src/handlers/help.handler.ts +++ b/services/matrix-mana-bot/src/handlers/help.handler.ts @@ -1,13 +1,15 @@ import { Injectable } from '@nestjs/common'; import { AiService, TodoService } from '@manacore/bot-services'; import { CommandContext } from '../bot/command-router.service'; +import { VoiceService } from '../voice/voice.service'; import { HELP_TEXT } from '../config/configuration'; @Injectable() export class HelpHandler { constructor( private aiService: AiService, - private todoService: TodoService + private todoService: TodoService, + private voiceService: VoiceService ) {} async showHelp(ctx: CommandContext): Promise { @@ -15,11 +17,22 @@ export class HelpHandler { } async showStatus(ctx: CommandContext): Promise { - const aiConnected = await this.aiService.checkConnection(); - const todoStats = await this.todoService.getStats(ctx.userId); + // Check services in parallel + const [aiConnected, todoStats, voiceHealth] = await Promise.all([ + this.aiService.checkConnection(), + this.todoService.getStats(ctx.userId), + this.voiceService.checkHealth(), + ]); const aiStatus = aiConnected ? '✅ Online' : '❌ Offline'; - const currentModel = this.aiService.getSession(ctx.userId)?.model || this.aiService.getDefaultModel(); + const currentModel = + this.aiService.getSession(ctx.userId)?.model || this.aiService.getDefaultModel(); + + const sttStatus = voiceHealth.stt ? '✅ Online' : '❌ Offline'; + const ttsStatus = voiceHealth.tts ? '✅ Online' : '❌ Offline'; + + const voicePrefs = this.voiceService.getUserPreferences(ctx.userId); + const voiceEnabled = voicePrefs.voiceEnabled ? '✅ Aktiviert' : '❌ Deaktiviert'; return `**📊 Status** @@ -27,6 +40,12 @@ export class HelpHandler { • Verbindung: ${aiStatus} • Modell: \`${currentModel}\` +**🎤 Voice** +• STT (Whisper): ${sttStatus} +• TTS (Edge): ${ttsStatus} +• Deine Einstellung: ${voiceEnabled} +• Stimme: ${voicePrefs.voice} + **Todos** • Offen: ${todoStats.pending} • Heute fällig: ${todoStats.today} diff --git a/services/matrix-mana-bot/src/voice/voice.service.ts b/services/matrix-mana-bot/src/voice/voice.service.ts index d1520cb25..962de28ac 100644 --- a/services/matrix-mana-bot/src/voice/voice.service.ts +++ b/services/matrix-mana-bot/src/voice/voice.service.ts @@ -8,15 +8,45 @@ export interface TranscriptionResult { duration?: number; } +export class VoiceServiceError extends Error { + constructor( + message: string, + public readonly code: 'STT_UNAVAILABLE' | 'TTS_UNAVAILABLE' | 'TIMEOUT' | 'INVALID_AUDIO' | 'UNKNOWN' + ) { + super(message); + this.name = 'VoiceServiceError'; + } +} + // Re-export for convenience export { VoicePreferences }; +// Simple LRU cache for TTS responses +interface CacheEntry { + buffer: Buffer; + timestamp: number; +} + @Injectable() export class VoiceService { private readonly logger = new Logger(VoiceService.name); private readonly sttUrl: string; private readonly voiceBotUrl: string; + // Timeouts in milliseconds + private readonly STT_TIMEOUT = 60000; // 60s for transcription (can be slow) + private readonly TTS_TIMEOUT = 30000; // 30s for synthesis + private readonly HEALTH_TIMEOUT = 5000; // 5s for health checks + + // Audio size limits + private readonly MIN_AUDIO_SIZE = 1000; // 1KB minimum + private readonly MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB maximum + + // TTS cache for common short responses + private readonly ttsCache = new Map(); + private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes + private readonly MAX_CACHE_SIZE = 50; + constructor( private configService: ConfigService, private preferencesStore: VoicePreferencesStore @@ -35,6 +65,21 @@ export class VoiceService { async transcribe(audioBuffer: Buffer, language = 'de'): Promise { const startTime = Date.now(); + // Validate audio size + if (audioBuffer.length < this.MIN_AUDIO_SIZE) { + throw new VoiceServiceError( + 'Audio zu kurz - bitte länger sprechen.', + 'INVALID_AUDIO' + ); + } + + if (audioBuffer.length > this.MAX_AUDIO_SIZE) { + throw new VoiceServiceError( + 'Audio zu groß (max 25MB). Bitte kürzere Nachricht senden.', + 'INVALID_AUDIO' + ); + } + try { const formData = new FormData(); // Convert Buffer to Uint8Array for Blob compatibility @@ -45,11 +90,15 @@ export class VoiceService { const response = await fetch(`${this.sttUrl}/transcribe`, { method: 'POST', body: formData, + signal: AbortSignal.timeout(this.STT_TIMEOUT), }); if (!response.ok) { const error = await response.text(); - throw new Error(`STT error: ${response.status} - ${error}`); + throw new VoiceServiceError( + `Spracherkennung fehlgeschlagen: ${response.status}`, + 'STT_UNAVAILABLE' + ); } const result = await response.json(); @@ -63,18 +112,44 @@ export class VoiceService { duration, }; } catch (error) { + if (error instanceof VoiceServiceError) { + throw error; + } + + if (error.name === 'TimeoutError' || error.name === 'AbortError') { + this.logger.error(`STT timeout after ${this.STT_TIMEOUT}ms`); + throw new VoiceServiceError( + 'Spracherkennung dauert zu lange. Bitte versuche es erneut.', + 'TIMEOUT' + ); + } + this.logger.error(`Transcription failed: ${error}`); - throw error; + throw new VoiceServiceError( + 'Spracherkennung nicht erreichbar.', + 'STT_UNAVAILABLE' + ); } } /** * Synthesize speech from text using mana-voice-bot (Edge TTS) + * Includes caching for common short responses */ async synthesize(text: string, userId?: string): Promise { const prefs = this.getUserPreferences(userId); const startTime = Date.now(); + // Check cache for short texts (< 100 chars) + if (text.length < 100) { + const cacheKey = `${prefs.voice}:${text}`; + const cached = this.getCached(cacheKey); + if (cached) { + this.logger.debug(`TTS cache hit for "${text.substring(0, 30)}..."`); + return cached; + } + } + try { const formData = new FormData(); formData.append('text', text); @@ -83,11 +158,15 @@ export class VoiceService { const response = await fetch(`${this.voiceBotUrl}/tts`, { method: 'POST', body: formData, + signal: AbortSignal.timeout(this.TTS_TIMEOUT), }); if (!response.ok) { const error = await response.text(); - throw new Error(`TTS error: ${response.status} - ${error}`); + throw new VoiceServiceError( + `Sprachsynthese fehlgeschlagen: ${response.status}`, + 'TTS_UNAVAILABLE' + ); } const arrayBuffer = await response.arrayBuffer(); @@ -96,19 +175,77 @@ export class VoiceService { this.logger.debug(`Synthesized ${buffer.length} bytes in ${duration}ms`); + // Cache short responses + if (text.length < 100) { + const cacheKey = `${prefs.voice}:${text}`; + this.setCache(cacheKey, buffer); + } + return buffer; } catch (error) { + if (error instanceof VoiceServiceError) { + throw error; + } + + if (error.name === 'TimeoutError' || error.name === 'AbortError') { + this.logger.error(`TTS timeout after ${this.TTS_TIMEOUT}ms`); + throw new VoiceServiceError( + 'Sprachsynthese dauert zu lange.', + 'TIMEOUT' + ); + } + this.logger.error(`Synthesis failed: ${error}`); - throw error; + throw new VoiceServiceError( + 'Sprachsynthese nicht erreichbar.', + 'TTS_UNAVAILABLE' + ); } } + /** + * Get cached TTS response + */ + private getCached(key: string): Buffer | null { + const entry = this.ttsCache.get(key); + if (!entry) return null; + + // Check if expired + if (Date.now() - entry.timestamp > this.CACHE_TTL) { + this.ttsCache.delete(key); + return null; + } + + return entry.buffer; + } + + /** + * Cache TTS response + */ + private setCache(key: string, buffer: Buffer): void { + // Enforce max cache size + if (this.ttsCache.size >= this.MAX_CACHE_SIZE) { + // Remove oldest entry + const oldestKey = this.ttsCache.keys().next().value; + if (oldestKey) { + this.ttsCache.delete(oldestKey); + } + } + + this.ttsCache.set(key, { + buffer, + timestamp: Date.now(), + }); + } + /** * Get available TTS voices */ async getVoices(): Promise> { try { - const response = await fetch(`${this.voiceBotUrl}/voices`); + const response = await fetch(`${this.voiceBotUrl}/voices`, { + signal: AbortSignal.timeout(this.HEALTH_TIMEOUT), + }); if (!response.ok) { throw new Error(`Failed to get voices: ${response.status}`); } @@ -120,6 +257,24 @@ export class VoiceService { } } + /** + * Clear the TTS cache + */ + clearCache(): void { + this.ttsCache.clear(); + this.logger.debug('TTS cache cleared'); + } + + /** + * Get cache statistics + */ + getCacheStats(): { size: number; maxSize: number } { + return { + size: this.ttsCache.size, + maxSize: this.MAX_CACHE_SIZE, + }; + } + /** * Check if voice services are available */ diff --git a/services/matrix-picture-bot/src/bot/matrix.service.ts b/services/matrix-picture-bot/src/bot/matrix.service.ts index 5bcff0f22..fafcb3808 100644 --- a/services/matrix-picture-bot/src/bot/matrix.service.ts +++ b/services/matrix-picture-bot/src/bot/matrix.service.ts @@ -4,20 +4,13 @@ import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, } from '@manacore/matrix-bot-common'; import { PictureService } from '../picture/picture.service'; import { SessionService } from '@manacore/bot-services'; import { HELP_MESSAGE } from '../config/configuration'; -// Natural language keywords that trigger commands -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' }, - { keywords: ['modelle', 'models'], command: 'models' }, - { keywords: ['verlauf', 'history', 'bilder'], command: 'history' }, - { keywords: ['credits', 'guthaben'], command: 'credits' }, - { keywords: ['status', 'info'], command: 'status' }, -]; - interface ParsedPrompt { prompt: string; negativePrompt?: string; @@ -34,6 +27,13 @@ export class MatrixService extends BaseMatrixService { // Track selected model per user private userModels: Map = new Map(); + private readonly keywordDetector = new KeywordCommandDetector([ + ...COMMON_KEYWORDS, + { keywords: ['modelle', 'models'], command: 'models' }, + { keywords: ['verlauf', 'history', 'bilder'], command: 'history' }, + { keywords: ['credits', 'guthaben'], command: 'credits' }, + ]); + constructor( configService: ConfigService, private pictureService: PictureService, @@ -76,7 +76,7 @@ Sag "hilfe" fur alle Befehle!`; } // Check for natural language keywords - const keywordCommand = this.detectKeywordCommand(message); + const keywordCommand = this.keywordDetector.detect(message); if (keywordCommand) { await this.handleCommand(roomId, sender, `!${keywordCommand}`); return; @@ -85,23 +85,6 @@ Sag "hilfe" fur alle Befehle!`; // Don't respond to random messages } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - - // Only match if the message is short - if (lowerMessage.length > 30) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) { - this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`); - return command; - } - } - } - return null; - } - private async handleCommand(roomId: string, sender: string, body: string) { const [command, ...args] = body.slice(1).split(' '); const argString = args.join(' '); diff --git a/services/matrix-todo-bot/src/bot/matrix.service.ts b/services/matrix-todo-bot/src/bot/matrix.service.ts index 907155998..cdcc0a7d7 100644 --- a/services/matrix-todo-bot/src/bot/matrix.service.ts +++ b/services/matrix-todo-bot/src/bot/matrix.service.ts @@ -4,26 +4,28 @@ import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, } from '@manacore/matrix-bot-common'; import { TodoService, Task } from '../todo/todo.service'; import { TranscriptionService } from '@manacore/bot-services'; import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration'; -// Natural language keywords that trigger commands (German + English) -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'was kannst du', 'befehle', 'commands'], command: 'help' }, - { - keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'], - command: 'list', - }, - { keywords: ['heute', 'today', 'was steht an'], command: 'today' }, - { keywords: ['inbox', 'eingang', 'ohne datum'], command: 'inbox' }, - { keywords: ['projekte', 'projects'], command: 'projects' }, - { keywords: ['status', 'verbindung', 'connection'], command: 'status' }, -]; - @Injectable() export class MatrixService extends BaseMatrixService { + private readonly keywordDetector = new KeywordCommandDetector( + [ + ...COMMON_KEYWORDS, + { keywords: ['was kannst du'], command: 'help' }, + { keywords: ['zeige aufgaben', 'meine aufgaben', 'was muss ich', 'show tasks', 'list'], command: 'list' }, + { keywords: ['heute', 'today', 'was steht an'], command: 'today' }, + { keywords: ['inbox', 'eingang', 'ohne datum'], command: 'inbox' }, + { keywords: ['projekte', 'projects'], command: 'projects' }, + { keywords: ['verbindung', 'connection'], command: 'status' }, + ], + { partialMatch: true } + ); + constructor( configService: ConfigService, private todoService: TodoService, @@ -54,7 +56,7 @@ export class MatrixService extends BaseMatrixService { try { // Check for natural language keywords first - const keywordCommand = this.detectKeywordCommand(body); + const keywordCommand = this.keywordDetector.detect(body); if (keywordCommand) { await this.executeCommand(roomId, event, userId, keywordCommand, ''); return; @@ -140,27 +142,6 @@ export class MatrixService extends BaseMatrixService { } } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - - // Only check short messages for keywords - if (lowerMessage.length > 50) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if ( - lowerMessage === keyword || - lowerMessage.startsWith(keyword + ' ') || - lowerMessage.includes(keyword) - ) { - this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`); - return command; - } - } - } - return null; - } - private async executeCommand( roomId: string, event: MatrixRoomEvent, diff --git a/services/matrix-zitare-bot/src/bot/matrix.service.ts b/services/matrix-zitare-bot/src/bot/matrix.service.ts index 30d895189..bce0fcf7e 100644 --- a/services/matrix-zitare-bot/src/bot/matrix.service.ts +++ b/services/matrix-zitare-bot/src/bot/matrix.service.ts @@ -4,30 +4,30 @@ import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, } from '@manacore/matrix-bot-common'; import { QuotesService } from '../quotes/quotes.service'; import { ZitareService } from '../quotes/zitare.service'; import { SessionService, TranscriptionService } from '@manacore/bot-services'; import { HELP_MESSAGE, Category } from '../config/configuration'; -// Natural language keywords that trigger commands -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' }, - { keywords: ['zitat', 'quote', 'inspiration', 'inspiriere'], command: 'zitat' }, - { keywords: ['heute', 'today', 'tages', 'tageszitat'], command: 'heute' }, - { keywords: ['motiviere', 'motivation', 'motivier mich'], command: 'motivation' }, - { keywords: ['guten morgen', 'morgen', 'good morning'], command: 'morgen' }, - { keywords: ['kategorien', 'categories', 'themen'], command: 'kategorien' }, - { keywords: ['favoriten', 'favorites', 'meine favoriten'], command: 'favoriten' }, - { keywords: ['listen', 'lists', 'meine listen'], command: 'listen' }, - { keywords: ['status', 'info'], command: 'status' }, -]; - @Injectable() export class MatrixService extends BaseMatrixService { // Track last shown quote per user for favorites private lastQuotes: Map = new Map(); + private readonly keywordDetector = new KeywordCommandDetector([ + ...COMMON_KEYWORDS, + { keywords: ['zitat', 'quote', 'inspiration', 'inspiriere'], command: 'zitat' }, + { keywords: ['heute', 'today', 'tages', 'tageszitat'], command: 'heute' }, + { keywords: ['motiviere', 'motivation', 'motivier mich'], command: 'motivation' }, + { keywords: ['guten morgen', 'morgen', 'good morning'], command: 'morgen' }, + { keywords: ['kategorien', 'categories', 'themen'], command: 'kategorien' }, + { keywords: ['favoriten', 'favorites', 'meine favoriten'], command: 'favoriten' }, + { keywords: ['listen', 'lists', 'meine listen'], command: 'listen' }, + ]); + constructor( configService: ConfigService, private quotesService: QuotesService, @@ -76,7 +76,7 @@ Sag "hilfe" fuer alle Befehle!`; } // Check for natural language keywords - const keywordCommand = this.detectKeywordCommand(body); + const keywordCommand = this.keywordDetector.detect(body); if (keywordCommand) { await this.handleCommand(roomId, sender, `!${keywordCommand}`); return; @@ -124,7 +124,7 @@ Sag "hilfe" fuer alle Befehle!`; const cleanText = transcription.trim(); // Check for keyword commands in the transcription - const keywordCommand = this.detectKeywordCommand(cleanText); + const keywordCommand = this.keywordDetector.detect(cleanText); if (keywordCommand) { await this.handleCommand(roomId, sender, `!${keywordCommand}`); return; @@ -153,23 +153,6 @@ Sag "hilfe" fuer alle Befehle!`; } } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - - // Only match if the message is short - if (lowerMessage.length > 50) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) { - this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`); - return command; - } - } - } - return null; - } - private async handleCommand(roomId: string, sender: string, body: string) { const [command, ...args] = body.slice(1).split(' '); const argString = args.join(' ');