feat(matrix-ollama-bot): add natural language commands and welcome messages

- Add keyword detection for German/English commands (hilfe, modelle, status)
- Send welcome message when users join the room
- Send bot introduction when invited to new rooms
- Add !pin command to pin help message
- Auto-pin help when joining new rooms
- Update help text with simpler command overview

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-28 14:56:32 +01:00
parent e4145324b2
commit ee630158c5

View file

@ -24,6 +24,14 @@ const NON_CHAT_MODELS = ['deepseek-r1:1.5b'];
// Models that support vision/image input
const VISION_MODELS = ['llava', 'llava:7b', 'llava:13b', 'bakllava', 'moondream'];
// 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: ['modelle', 'models', 'welche modelle', 'liste modelle'], command: 'models' },
{ keywords: ['status', 'verbindung', 'connection', 'online'], command: 'status' },
{ keywords: ['lösche verlauf', 'clear', 'neustart', 'reset', 'vergiss alles'], command: 'clear' },
];
@Injectable()
export class MatrixService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(MatrixService.name);
@ -59,8 +67,20 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
// Create Matrix client
this.client = new MatrixClient(homeserverUrl!, accessToken, storage);
// Auto-join rooms when invited
AutojoinRoomsMixin.setupOnClient(this.client);
// Auto-join rooms when invited and send welcome
this.client.on('room.invite', async (roomId: string) => {
this.logger.log(`Invited to room ${roomId}, joining...`);
await this.client.joinRoom(roomId);
// Wait a bit for the join to complete, then send intro and pin help
setTimeout(async () => {
try {
await this.sendBotIntroduction(roomId);
} catch (error) {
this.logger.error(`Failed to send introduction to ${roomId}:`, error);
}
}, 2000);
});
// Get bot's user ID
this.botUserId = await this.client.getUserId();
@ -69,11 +89,110 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
// Setup message handler
this.client.on('room.message', this.handleRoomMessage.bind(this));
// Setup room join handler for welcome message
this.client.on('room.join', this.handleRoomJoin.bind(this));
// Start the client
await this.client.start();
this.logger.log('Matrix bot started successfully');
}
private async handleRoomJoin(roomId: string, event: any) {
// Only send welcome when someone else joins (not the bot itself)
if (event.state_key === this.botUserId) return;
if (!this.isRoomAllowed(roomId)) return;
this.logger.log(`User ${event.state_key} joined room ${roomId}`);
// Send welcome message
await this.sendWelcomeMessage(roomId, event.state_key);
}
private async sendWelcomeMessage(roomId: string, userId: string) {
const welcomeText = `👋 **Willkommen im Mana Chat, ${this.extractUsername(userId)}!**
Ich bin **Manai**, deine lokale KI-Assistentin (100% DSGVO-konform).
**So nutzt du mich:**
Schreib einfach eine Nachricht - ich antworte!
Sag "hilfe" oder "modelle" für mehr Infos
Oder nutze Befehle wie \`!help\`
**Quick Start:**
"Was ist TypeScript?" Ich erkläre es dir
"modelle" Zeigt verfügbare KI-Modelle
\`!all Erkläre Recursion\` → Vergleicht alle Modelle
Viel Spaß! 🚀`;
await this.sendMessage(roomId, welcomeText);
}
private extractUsername(userId: string): string {
// Extract username from @user:server.com format
const match = userId.match(/@([^:]+)/);
return match ? match[1] : userId;
}
private async sendBotIntroduction(roomId: string) {
const introText = `🤖 **Hallo! Ich bin Manai, eure lokale KI-Assistentin.**
Alle Daten bleiben auf diesem Server - 100% DSGVO-konform!
**Quick Start:**
Schreibt einfach eine Nachricht
Sagt "hilfe" für alle Befehle
Sagt "modelle" um KI-Modelle zu sehen
Ich pinne jetzt die Hilfe für euch an! 📌`;
await this.sendMessage(roomId, introText);
// Pin the help message
await this.pinHelpMessage(roomId);
}
private async pinHelpMessage(roomId: string) {
try {
// Send the help message and get its event ID
const helpContent = this.getHelpContent();
const htmlBody = this.markdownToHtml(helpContent);
const eventId = await this.client.sendMessage(roomId, {
msgtype: 'm.text',
body: helpContent,
format: 'org.matrix.custom.html',
formatted_body: htmlBody,
});
// Pin the message
await this.client.sendStateEvent(roomId, 'm.room.pinned_events', '', {
pinned: [eventId],
});
this.logger.log(`Pinned help message in room ${roomId}`);
} catch (error) {
this.logger.error(`Failed to pin help message in ${roomId}:`, error);
}
}
private getHelpContent(): string {
return `📌 **Manai - Befehls-Übersicht**
**Einfach sagen:**
"hilfe" - Diese Übersicht
"modelle" - Verfügbare KI-Modelle
"status" - Bot-Status
"lösche verlauf" - Chat zurücksetzen
**Power-User (mit !):**
\`!model [name]\` - Modell wechseln
\`!all [frage]\` - Alle Modelle vergleichen
\`!vision [frage]\` - Bild analysieren
**Nutzung:** Einfach schreiben und ich antworte!`;
}
async onModuleDestroy() {
if (this.client) {
await this.client.stop();
@ -137,16 +256,40 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
this.logger.log(`Message from ${event.sender} in ${roomId}: ${body.substring(0, 50)}...`);
// Handle commands
// Handle commands with ! prefix
if (body.startsWith('!')) {
await this.handleCommand(roomId, event.sender, body);
return;
}
// Check for natural language keywords
const keywordCommand = this.detectKeywordCommand(body);
if (keywordCommand) {
await this.handleCommand(roomId, event.sender, `!${keywordCommand}`);
return;
}
// Regular chat message
await this.handleChat(roomId, event.sender, body);
}
private detectKeywordCommand(message: string): string | null {
const lowerMessage = message.toLowerCase().trim();
// Only match if the message is short (likely a command, not a question containing a keyword)
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(' ');
@ -189,6 +332,11 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
await this.handleVisionAll(roomId, sender, argString);
break;
case 'pin':
await this.pinHelpMessage(roomId);
await this.sendMessage(roomId, '📌 Hilfe wurde angepinnt!');
break;
default:
await this.sendMessage(
roomId,
@ -198,37 +346,30 @@ export class MatrixService implements OnModuleInit, OnModuleDestroy {
}
private async sendHelp(roomId: string) {
const helpText = `**Mana Chat - Lokale KI (DSGVO-konform)**
const helpText = `**Manai - Lokale KI (100% DSGVO-konform)**
**Befehle:**
- \`!help\` - Diese Hilfe anzeigen
- \`!models\` - Verfügbare Modelle anzeigen
- \`!model [name]\` - Modell wechseln
- \`!all [frage]\` - **Alle Chat-Modelle vergleichen**
- \`!mode [modus]\` - System-Prompt ändern
- \`!clear\` - Chat-Verlauf löschen
- \`!status\` - Ollama Status prüfen
**Einfache Befehle** (sag einfach):
"hilfe" - Diese Hilfe
"modelle" - Verfügbare KI-Modelle
"status" - Verbindungsstatus
"lösche verlauf" - Chat zurücksetzen
**Bild-Analyse (Vision):**
1. Sende ein Bild in den Chat
2. Nutze dann:
- \`!vision [frage]\` - Bild analysieren
- \`!vision:all [frage]\` - **Alle Vision-Modelle vergleichen**
**Power-User Befehle** (mit !):
\`!model [name]\` - Modell wechseln
\`!all [frage]\` - Alle Modelle vergleichen
\`!mode [modus]\` - Modus ändern (default/code/translate/summarize)
**Modi:**
- \`default\` - Allgemeiner Assistent
- \`classify\` - Text-Klassifizierung
- \`summarize\` - Zusammenfassungen
- \`translate\` - Übersetzungen
- \`code\` - Programmier-Hilfe
**Bild-Analyse:**
1. Sende ein Bild
2. Dann: \`!vision [frage]\` oder \`!vision:all [frage]\`
**Verwendung:**
Schreibe einfach eine Nachricht und ich antworte!
**Beispiele:**
- \`!all Was ist der Sinn des Lebens?\`
- [Bild senden] \`!vision Was siehst du?\`
- [Bild senden] \`!vision:all Beschreibe das Bild\`
"Was ist Kubernetes?" Direkte Antwort
"modelle" Zeigt alle Modelle
\`!all Erkläre Docker\` → Vergleicht alle Modelle
**Aktuelles Modell:** \`${this.ollamaService.getDefaultModel()}\``;