Add KI/LLM integration with OpenAI-compatible API support
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 30s
All checks were successful
Auto Build and Push Docker Image / build (push) Successful in 30s
Features: - /ai setup - Setup wizard via private chat for security - /ai status - Show current config (API key masked) - /ai on/off - Enable/disable AI per chat - /ai random <1-10> - Set random trigger probability (admins) - /ai help - Detailed help - /ask-ai [text] - Generate AI response Security: - API keys only entered in private chat - API keys masked in status display - Admin-only configuration Architecture: - OpenAI-compatible client (OpenAI, Ollama, OpenRouter) - Global system prompt (.env) + group prompt (DB) - 50 message context per chat - SQLite tables: ai_settings, message_context, setup_sessions Defaults: - AI disabled by default - Trigger word: ask-ai - Random probability: 1% Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
106
src/ai.ts
Normal file
106
src/ai.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* OpenAI-compatible AI client for multiple providers.
|
||||
* Supports OpenAI, Ollama, OpenRouter, and any OpenAI-compatible API.
|
||||
*/
|
||||
|
||||
export interface AIConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
systemPrompt: string;
|
||||
groupPrompt?: string;
|
||||
}
|
||||
|
||||
export interface AIMessage {
|
||||
role: 'user' | 'assistant' | 'system';
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask an API key for safe display
|
||||
* sk-proj-abc123xyz... → sk-***...***xyz
|
||||
*/
|
||||
export function maskApiKey(key: string): string {
|
||||
if (!key || key.length < 8) return '***';
|
||||
const start = key.substring(0, 3);
|
||||
const end = key.substring(key.length - 3);
|
||||
return `${start}***...***${end}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AI response using OpenAI-compatible API
|
||||
*/
|
||||
export async function generateAIResponse(
|
||||
config: AIConfig,
|
||||
context: AIMessage[],
|
||||
userMessage: string
|
||||
): Promise<string> {
|
||||
const messages: AIMessage[] = [];
|
||||
|
||||
// Build system prompt (global + group)
|
||||
let systemPrompt = config.systemPrompt;
|
||||
if (config.groupPrompt) {
|
||||
systemPrompt += '\n\n' + config.groupPrompt;
|
||||
}
|
||||
messages.push({ role: 'system', content: systemPrompt });
|
||||
|
||||
// Add context messages (last 50)
|
||||
messages.push(...context.slice(-50));
|
||||
|
||||
// Add user message
|
||||
messages.push({ role: 'user', content: userMessage });
|
||||
|
||||
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${config.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
messages,
|
||||
max_tokens: 500,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`AI API error: ${response.status} - ${error}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as { choices: Array<{ message?: { content?: string } }> };
|
||||
return data.choices[0]?.message?.content || 'Keine Antwort erhalten.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default base URL for a provider
|
||||
*/
|
||||
export function getDefaultBaseUrl(provider: string): string {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return 'https://api.openai.com/v1';
|
||||
case 'ollama':
|
||||
return 'http://localhost:11434/v1';
|
||||
case 'openrouter':
|
||||
return 'https://openrouter.ai/api/v1';
|
||||
default:
|
||||
return 'https://api.openai.com/v1';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models for a provider
|
||||
*/
|
||||
export function getProviderModels(provider: string): string[] {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
return ['gpt-4o-mini', 'gpt-4o', 'gpt-3.5-turbo'];
|
||||
case 'ollama':
|
||||
return ['llama3', 'llama3.1', 'mistral', 'codellama'];
|
||||
case 'openrouter':
|
||||
return ['openai/gpt-4o-mini', 'anthropic/claude-3.5-sonnet', 'meta-llama/llama-3.1-8b-instruct'];
|
||||
default:
|
||||
return ['gpt-4o-mini'];
|
||||
}
|
||||
}
|
||||
213
src/database.ts
213
src/database.ts
@@ -14,6 +14,24 @@ export interface ChatSettings {
|
||||
probability: number;
|
||||
}
|
||||
|
||||
export interface AISettings {
|
||||
enabled: boolean;
|
||||
triggerWord: string;
|
||||
randomProb: number;
|
||||
groupPrompt: string | null;
|
||||
provider: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
apiKey: string | null;
|
||||
}
|
||||
|
||||
export interface SetupSession {
|
||||
userId: number;
|
||||
chatId: number;
|
||||
step: number;
|
||||
data: Record<string, string>;
|
||||
}
|
||||
|
||||
export function initDatabase(): void {
|
||||
db = new Database(DB_FILE);
|
||||
|
||||
@@ -37,6 +55,33 @@ export function initDatabase(): void {
|
||||
count INTEGER,
|
||||
PRIMARY KEY (chat_id, key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_settings (
|
||||
chat_id INTEGER PRIMARY KEY,
|
||||
enabled INTEGER DEFAULT 0,
|
||||
trigger_word TEXT DEFAULT 'ask-ai',
|
||||
random_prob INTEGER DEFAULT 1,
|
||||
group_prompt TEXT,
|
||||
provider TEXT DEFAULT 'openai',
|
||||
base_url TEXT,
|
||||
model TEXT DEFAULT 'gpt-4o-mini',
|
||||
api_key TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message_context (
|
||||
chat_id INTEGER,
|
||||
role TEXT,
|
||||
content TEXT,
|
||||
timestamp INTEGER,
|
||||
PRIMARY KEY (chat_id, timestamp)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS setup_sessions (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
chat_id INTEGER,
|
||||
step INTEGER,
|
||||
data TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
// Create indexes for faster queries
|
||||
@@ -44,6 +89,7 @@ export function initDatabase(): void {
|
||||
CREATE INDEX IF NOT EXISTS idx_transitions_chat ON markov_transitions(chat_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_starts_chat ON markov_starts(chat_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_transitions_count ON markov_transitions(count);
|
||||
CREATE INDEX IF NOT EXISTS idx_context ON message_context(chat_id, timestamp);
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -177,4 +223,171 @@ export function saveChain(chatId: number, chain: MarkovChain): void {
|
||||
|
||||
export function closeDatabase(): void {
|
||||
db.close();
|
||||
}
|
||||
|
||||
// ============ AI Settings ============
|
||||
|
||||
export function getAISettings(chatId: number): AISettings {
|
||||
const row = db
|
||||
.prepare('SELECT * FROM ai_settings WHERE chat_id = ?')
|
||||
.get(chatId) as {
|
||||
enabled: number;
|
||||
trigger_word: string;
|
||||
random_prob: number;
|
||||
group_prompt: string | null;
|
||||
provider: string;
|
||||
base_url: string | null;
|
||||
model: string;
|
||||
api_key: string | null;
|
||||
} | undefined;
|
||||
|
||||
if (!row) {
|
||||
return {
|
||||
enabled: false,
|
||||
triggerWord: 'ask-ai',
|
||||
randomProb: 1, // Default 1%
|
||||
groupPrompt: null,
|
||||
provider: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
model: 'gpt-4o-mini',
|
||||
apiKey: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: row.enabled === 1,
|
||||
triggerWord: row.trigger_word,
|
||||
randomProb: row.random_prob,
|
||||
groupPrompt: row.group_prompt,
|
||||
provider: row.provider,
|
||||
baseUrl: row.base_url || 'https://api.openai.com/v1',
|
||||
model: row.model,
|
||||
apiKey: row.api_key,
|
||||
};
|
||||
}
|
||||
|
||||
export function setAISettings(chatId: number, settings: Partial<AISettings>): void {
|
||||
const existing = db.prepare('SELECT chat_id FROM ai_settings WHERE chat_id = ?').get(chatId);
|
||||
|
||||
if (existing) {
|
||||
const fields: string[] = [];
|
||||
const values: (string | number | null)[] = [];
|
||||
|
||||
if (settings.enabled !== undefined) {
|
||||
fields.push('enabled = ?');
|
||||
values.push(settings.enabled ? 1 : 0);
|
||||
}
|
||||
if (settings.triggerWord !== undefined) {
|
||||
fields.push('trigger_word = ?');
|
||||
values.push(settings.triggerWord);
|
||||
}
|
||||
if (settings.randomProb !== undefined) {
|
||||
fields.push('random_prob = ?');
|
||||
values.push(settings.randomProb);
|
||||
}
|
||||
if (settings.groupPrompt !== undefined) {
|
||||
fields.push('group_prompt = ?');
|
||||
values.push(settings.groupPrompt);
|
||||
}
|
||||
if (settings.provider !== undefined) {
|
||||
fields.push('provider = ?');
|
||||
values.push(settings.provider);
|
||||
}
|
||||
if (settings.baseUrl !== undefined) {
|
||||
fields.push('base_url = ?');
|
||||
values.push(settings.baseUrl);
|
||||
}
|
||||
if (settings.model !== undefined) {
|
||||
fields.push('model = ?');
|
||||
values.push(settings.model);
|
||||
}
|
||||
if (settings.apiKey !== undefined) {
|
||||
fields.push('api_key = ?');
|
||||
values.push(settings.apiKey);
|
||||
}
|
||||
|
||||
if (fields.length > 0) {
|
||||
values.push(chatId);
|
||||
db.prepare(`UPDATE ai_settings SET ${fields.join(', ')} WHERE chat_id = ?`).run(...values);
|
||||
}
|
||||
} else {
|
||||
db.prepare(`
|
||||
INSERT INTO ai_settings (chat_id, enabled, trigger_word, random_prob, group_prompt, provider, base_url, model, api_key)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
chatId,
|
||||
settings.enabled ? 1 : 0,
|
||||
settings.triggerWord || 'ask-ai',
|
||||
settings.randomProb ?? 1, // Default 1%
|
||||
settings.groupPrompt ?? null,
|
||||
settings.provider || 'openai',
|
||||
settings.baseUrl || 'https://api.openai.com/v1',
|
||||
settings.model || 'gpt-4o-mini',
|
||||
settings.apiKey ?? null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Message Context ============
|
||||
|
||||
export function addMessageContext(chatId: number, role: 'user' | 'assistant', content: string): void {
|
||||
db.prepare(`
|
||||
INSERT INTO message_context (chat_id, role, content, timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(chatId, role, content, Date.now());
|
||||
}
|
||||
|
||||
export function getMessageContext(chatId: number, limit: number = 50): Array<{ role: string; content: string }> {
|
||||
return db
|
||||
.prepare(`
|
||||
SELECT role, content FROM message_context
|
||||
WHERE chat_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
`)
|
||||
.all(chatId, limit) as Array<{ role: string; content: string }>;
|
||||
}
|
||||
|
||||
export function cleanupMessageContext(chatId: number, keepLast: number = 50): void {
|
||||
db.prepare(`
|
||||
DELETE FROM message_context
|
||||
WHERE chat_id = ? AND timestamp NOT IN (
|
||||
SELECT timestamp FROM message_context
|
||||
WHERE chat_id = ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
)
|
||||
`).run(chatId, chatId, keepLast);
|
||||
}
|
||||
|
||||
// ============ Setup Sessions ============
|
||||
|
||||
export function getSetupSession(userId: number): SetupSession | null {
|
||||
const row = db
|
||||
.prepare('SELECT * FROM setup_sessions WHERE user_id = ?')
|
||||
.get(userId) as { user_id: number; chat_id: number; step: number; data: string } | undefined;
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
userId: row.user_id,
|
||||
chatId: row.chat_id,
|
||||
step: row.step,
|
||||
data: row.data ? JSON.parse(row.data) : {},
|
||||
};
|
||||
}
|
||||
|
||||
export function setSetupSession(userId: number, chatId: number, step: number, data: Record<string, string>): void {
|
||||
db.prepare(`
|
||||
INSERT INTO setup_sessions (user_id, chat_id, step, data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
chat_id = excluded.chat_id,
|
||||
step = excluded.step,
|
||||
data = excluded.data
|
||||
`).run(userId, chatId, step, JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function deleteSetupSession(userId: number): void {
|
||||
db.prepare('DELETE FROM setup_sessions WHERE user_id = ?').run(userId);
|
||||
}
|
||||
393
src/index.ts
393
src/index.ts
@@ -1,10 +1,34 @@
|
||||
import 'dotenv/config';
|
||||
import { Telegraf, Context } from 'telegraf';
|
||||
import { MarkovChain } from './markov.js';
|
||||
import { initDatabase, closeDatabase, getSettings, setProbability, loadChain, saveChain, cleanupDatabase } from './database.js';
|
||||
import { generateAIResponse, maskApiKey, getDefaultBaseUrl, getProviderModels } from './ai.js';
|
||||
import {
|
||||
initDatabase,
|
||||
closeDatabase,
|
||||
getSettings,
|
||||
setProbability,
|
||||
loadChain,
|
||||
saveChain,
|
||||
cleanupDatabase,
|
||||
getAISettings,
|
||||
setAISettings,
|
||||
addMessageContext,
|
||||
getMessageContext,
|
||||
cleanupMessageContext,
|
||||
getSetupSession,
|
||||
setSetupSession,
|
||||
deleteSetupSession,
|
||||
AISettings,
|
||||
} from './database.js';
|
||||
|
||||
const bot = new Telegraf(process.env.BOT_TOKEN!);
|
||||
|
||||
// Global AI settings from .env
|
||||
const AI_DEFAULT_API_KEY = process.env.AI_DEFAULT_API_KEY || '';
|
||||
const AI_DEFAULT_BASE_URL = process.env.AI_DEFAULT_BASE_URL || 'https://api.openai.com/v1';
|
||||
const AI_DEFAULT_MODEL = process.env.AI_DEFAULT_MODEL || 'gpt-4o-mini';
|
||||
const AI_SYSTEM_PROMPT = process.env.AI_SYSTEM_PROMPT || 'Du bist ein freundlicher Chat-Bot in einer Telegram-Gruppe. Antworte kurz und prägnant auf Deutsch.';
|
||||
|
||||
// In-memory cache of chains
|
||||
const chains = new Map<number, MarkovChain>();
|
||||
|
||||
@@ -39,6 +63,319 @@ bot.use(async (ctx, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
// ============ Setup Wizard ============
|
||||
|
||||
const SETUP_STEPS = 6;
|
||||
|
||||
async function handleSetupStep(ctx: Context): Promise<void> {
|
||||
if (!ctx.from || !ctx.message || !('text' in ctx.message)) return;
|
||||
|
||||
const text = ctx.message.text.trim();
|
||||
const userId = ctx.from.id;
|
||||
|
||||
const session = getSetupSession(userId);
|
||||
if (!session) return;
|
||||
|
||||
const data = { ...session.data };
|
||||
let nextStep = session.step;
|
||||
|
||||
switch (session.step) {
|
||||
case 1: // Provider
|
||||
const providers = ['openai', 'ollama', 'openrouter', 'custom'];
|
||||
if (!providers.includes(text.toLowerCase())) {
|
||||
ctx.reply('Bitte wähle einen gültigen Provider: openai, ollama, openrouter, custom');
|
||||
return;
|
||||
}
|
||||
data.provider = text.toLowerCase();
|
||||
data.baseUrl = getDefaultBaseUrl(data.provider);
|
||||
nextStep = 2;
|
||||
ctx.reply(
|
||||
'Schritt 2/6: API-Key eingeben\n' +
|
||||
'⚠️ Der Key wird sicher gespeichert und nicht vollständig angezeigt.\n\n' +
|
||||
'Bitte sende deinen API-Key:'
|
||||
);
|
||||
break;
|
||||
|
||||
case 2: // API Key
|
||||
data.apiKey = text;
|
||||
nextStep = 3;
|
||||
ctx.reply(
|
||||
'✓ API-Key gespeichert (' + maskApiKey(text) + ')\n\n' +
|
||||
'Schritt 3/6: Model wählen\n' +
|
||||
'Verfügbare Models: ' + getProviderModels(data.provider || 'openai').join(', ') + '\n\n' +
|
||||
'Oder gib ein anderes Model ein:'
|
||||
);
|
||||
break;
|
||||
|
||||
case 3: // Model
|
||||
data.model = text;
|
||||
nextStep = 4;
|
||||
ctx.reply(
|
||||
'Schritt 4/6: Base URL\n' +
|
||||
'Standard: ' + (data.baseUrl || 'https://api.openai.com/v1') + '\n\n' +
|
||||
'Sende "skip" für Standard oder gib eine andere URL ein:'
|
||||
);
|
||||
break;
|
||||
|
||||
case 4: // Base URL
|
||||
if (text.toLowerCase() !== 'skip') {
|
||||
data.baseUrl = text;
|
||||
}
|
||||
nextStep = 5;
|
||||
ctx.reply(
|
||||
'Schritt 5/6: Trigger & Zufall\n' +
|
||||
'Triggerwort: ask-ai\n' +
|
||||
'Zufalls-Wahrscheinlichkeit (1-10%): 1%\n\n' +
|
||||
'Sende "skip" für Standard oder gib trigger wahrscheinlichkeit ein (z.B. "ask-ai 5"):'
|
||||
);
|
||||
break;
|
||||
|
||||
case 5: // Trigger & Zufall
|
||||
if (text.toLowerCase() !== 'skip') {
|
||||
const parts = text.split(' ');
|
||||
if (parts[0]) data.triggerWord = parts[0];
|
||||
if (parts[1]) {
|
||||
const prob = parseInt(parts[1], 10);
|
||||
if (prob >= 1 && prob <= 10) {
|
||||
data.randomProb = prob.toString();
|
||||
} else {
|
||||
ctx.reply('Zufall muss zwischen 1 und 10 liegen. Verwende Standard (1%).');
|
||||
data.randomProb = '1';
|
||||
}
|
||||
}
|
||||
}
|
||||
nextStep = 6;
|
||||
ctx.reply(
|
||||
'Schritt 6/6: Gruppen-Prompt\n' +
|
||||
'Dieser Prompt ergänzt den globalen System-Prompt.\n' +
|
||||
'Beispiel: "In dieser Gruppe wird über Programmieren gesprochen."\n\n' +
|
||||
'Sende "skip" für keinen Gruppen-Prompt:'
|
||||
);
|
||||
break;
|
||||
|
||||
case 6: // Gruppen-Prompt
|
||||
if (text.toLowerCase() !== 'skip') {
|
||||
data.groupPrompt = text;
|
||||
}
|
||||
// Save all settings
|
||||
setAISettings(session.chatId, {
|
||||
enabled: true,
|
||||
triggerWord: data.triggerWord || 'ask-ai',
|
||||
randomProb: data.randomProb ? parseInt(data.randomProb, 10) : 0,
|
||||
groupPrompt: data.groupPrompt || null,
|
||||
provider: data.provider || 'openai',
|
||||
baseUrl: data.baseUrl || getDefaultBaseUrl(data.provider || 'openai'),
|
||||
model: data.model || 'gpt-4o-mini',
|
||||
apiKey: data.apiKey || null,
|
||||
});
|
||||
deleteSetupSession(userId);
|
||||
ctx.reply(
|
||||
'✅ Setup abgeschlossen!\n\n' +
|
||||
'KI ist jetzt für diese Gruppe aktiv.\n' +
|
||||
'Nutze /ask-ai oder das Triggerwort zum Testen.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setSetupSession(userId, session.chatId, nextStep, data);
|
||||
}
|
||||
|
||||
bot.command('ai', async (ctx) => {
|
||||
if (!ctx.from || !ctx.message || !('text' in ctx.message)) return;
|
||||
|
||||
const args = ctx.message.text.split(' ').slice(1);
|
||||
const subcommand = args[0]?.toLowerCase();
|
||||
|
||||
// /ai setup - Start setup wizard in group, continue in private
|
||||
if (subcommand === 'setup') {
|
||||
if (ctx.chat?.type === 'private') {
|
||||
ctx.reply('Bitte starte /ai setup in einer Gruppe, nicht im Privat-Chat.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await isAdmin(ctx, ctx.from.id))) {
|
||||
ctx.reply('Nur Admins können das KI-Setup starten.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start private conversation for setup
|
||||
setSetupSession(ctx.from.id, ctx.chat.id, 1, {});
|
||||
|
||||
ctx.reply(
|
||||
'🤖 KI-Setup gestartet!\n' +
|
||||
'Ich sende dir eine private Nachricht zur Konfiguration.',
|
||||
{ reply_parameters: { message_id: ctx.message.message_id } }
|
||||
);
|
||||
|
||||
// Send private message
|
||||
try {
|
||||
await ctx.telegram.sendMessage(ctx.from.id,
|
||||
'🤖 KI-Setup für Gruppe\n\n' +
|
||||
'Schritt 1/6: Provider wählen\n' +
|
||||
'[openai] [ollama] [openrouter] [custom]\n\n' +
|
||||
'Bitte sende den gewünschten Provider:'
|
||||
);
|
||||
} catch {
|
||||
ctx.reply('Bitte starte zuerst einen Privat-Chat mit mir (/start), dann führe /ai setup erneut aus.');
|
||||
deleteSetupSession(ctx.from.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// /ai status - Show current config
|
||||
if (subcommand === 'status') {
|
||||
const settings = getAISettings(ctx.chat.id);
|
||||
const keyStatus = settings.apiKey ? maskApiKey(settings.apiKey) + ' ✓' : '✗ Nicht gesetzt';
|
||||
|
||||
ctx.reply(
|
||||
'🤖 KI-Status\n' +
|
||||
'─────────────────────────\n' +
|
||||
`Status: ${settings.enabled ? '✓ Aktiviert' : '✗ Deaktiviert'}\n` +
|
||||
`Provider: ${settings.provider}\n` +
|
||||
`Model: ${settings.model}\n` +
|
||||
`API-Key: ${keyStatus}\n` +
|
||||
`Trigger: ${settings.triggerWord}\n` +
|
||||
`Zufall: ${settings.randomProb}%\n` +
|
||||
`Gruppen-Prompt: ${settings.groupPrompt || '(nicht gesetzt)'}\n\n` +
|
||||
(settings.enabled ? '' : '→ /ai setup zum Konfigurieren')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// /ai off - Disable AI
|
||||
if (subcommand === 'off') {
|
||||
if (!(await isAdmin(ctx, ctx.from.id))) {
|
||||
ctx.reply('Nur Admins können die KI deaktivieren.');
|
||||
return;
|
||||
}
|
||||
setAISettings(ctx.chat.id, { enabled: false });
|
||||
ctx.reply('KI für diese Gruppe deaktiviert.');
|
||||
return;
|
||||
}
|
||||
|
||||
// /ai on - Enable AI
|
||||
if (subcommand === 'on') {
|
||||
if (!(await isAdmin(ctx, ctx.from.id))) {
|
||||
ctx.reply('Nur Admins können die KI aktivieren.');
|
||||
return;
|
||||
}
|
||||
const settings = getAISettings(ctx.chat.id);
|
||||
if (!settings.apiKey) {
|
||||
ctx.reply('Bitte führe zuerst /ai setup aus.');
|
||||
return;
|
||||
}
|
||||
setAISettings(ctx.chat.id, { enabled: true });
|
||||
ctx.reply('KI für diese Gruppe aktiviert.');
|
||||
return;
|
||||
}
|
||||
|
||||
// /ai random - Set random probability (1-10%)
|
||||
if (subcommand === 'random') {
|
||||
if (!(await isAdmin(ctx, ctx.from.id))) {
|
||||
ctx.reply('Nur Admins können die Zufallswahrscheinlichkeit ändern.');
|
||||
return;
|
||||
}
|
||||
const probStr = args[1];
|
||||
if (!probStr) {
|
||||
const settings = getAISettings(ctx.chat.id);
|
||||
ctx.reply(`Aktuelle Zufallswahrscheinlichkeit: ${settings.randomProb}%\n\n/ai random <1-10> - Setzen (Admins only)`);
|
||||
return;
|
||||
}
|
||||
const prob = parseInt(probStr, 10);
|
||||
if (isNaN(prob) || prob < 1 || prob > 10) {
|
||||
ctx.reply('Bitte eine Zahl zwischen 1 und 10 angeben.');
|
||||
return;
|
||||
}
|
||||
setAISettings(ctx.chat.id, { randomProb: prob });
|
||||
ctx.reply(`Zufallswahrscheinlichkeit auf ${prob}% gesetzt.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// /ai help - Show detailed help
|
||||
if (subcommand === 'help') {
|
||||
ctx.reply(
|
||||
'🤖 KI-Hilfe\n\n' +
|
||||
'Der Bot kann optional eine KI/LLM nutzen um auf Nachrichten zu antworten.\n\n' +
|
||||
'Trigger:\n' +
|
||||
'• /ask-ai [text] - Expliziter Aufruf\n' +
|
||||
'• Triggerwort (z.B. "ask-ai") - Im Text enthalten\n' +
|
||||
'• Zufällig (1-10%, einstellbar)\n\n' +
|
||||
'Setup:\n' +
|
||||
'1. /ai setup in Gruppe ausführen\n' +
|
||||
'2. Private Nachricht folgt mit Wizard\n' +
|
||||
'3. Provider, API-Key, Model konfigurieren\n\n' +
|
||||
'Provider:\n' +
|
||||
'• openai - OpenAI (GPT-4, GPT-3.5)\n' +
|
||||
'• ollama - Lokale Models\n' +
|
||||
'• openrouter - Proxy für viele APIs\n' +
|
||||
'• custom - Andere OpenAI-kompatible\n\n' +
|
||||
'Prompts:\n' +
|
||||
'• Globaler Prompt (.env) - Immer aktiv\n' +
|
||||
'• Gruppen-Prompt - Ergänzt globalen\n\n' +
|
||||
'Sicherheit:\n' +
|
||||
'• API-Keys nur im Privat-Chat\n' +
|
||||
'• API-Keys maskiert angezeigt\n' +
|
||||
'• Admin-only Konfiguration'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: show help
|
||||
ctx.reply(
|
||||
'🤖 KI-Befehle\n\n' +
|
||||
'/ai setup - KI konfigurieren (Admins)\n' +
|
||||
'/ai status - Aktuelle Konfiguration\n' +
|
||||
'/ai on/off - KI aktivieren/deaktivieren\n' +
|
||||
'/ai random <1-10> - Zufallswahrscheinlichkeit\n' +
|
||||
'/ai help - Detaillierte Hilfe\n' +
|
||||
'/ask-ai [text] - KI-Antwort generieren'
|
||||
);
|
||||
});
|
||||
|
||||
// /ask-ai command
|
||||
bot.command('ask-ai', async (ctx) => {
|
||||
if (!ctx.message || !('text' in ctx.message)) return;
|
||||
|
||||
const settings = getAISettings(ctx.chat.id);
|
||||
if (!settings.enabled || !settings.apiKey) {
|
||||
ctx.reply('KI ist nicht aktiviert. Nutze /ai setup zum Konfigurieren.');
|
||||
return;
|
||||
}
|
||||
|
||||
const query = ctx.message.text.replace(/^\/ask-ai\s*/i, '').trim();
|
||||
if (!query) {
|
||||
ctx.reply('Bitte gib eine Frage oder Nachricht ein. Beispiel: /ask-ai Wie geht es dir?');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get context
|
||||
const context = getMessageContext(ctx.chat.id, 50);
|
||||
const messages = context.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
|
||||
|
||||
try {
|
||||
const response = await generateAIResponse(
|
||||
{
|
||||
apiKey: settings.apiKey,
|
||||
baseUrl: settings.baseUrl,
|
||||
model: settings.model,
|
||||
systemPrompt: AI_SYSTEM_PROMPT,
|
||||
groupPrompt: settings.groupPrompt || undefined,
|
||||
},
|
||||
messages,
|
||||
query
|
||||
);
|
||||
|
||||
ctx.reply(response, { reply_parameters: { message_id: ctx.message.message_id } });
|
||||
|
||||
// Save to context
|
||||
addMessageContext(ctx.chat.id, 'user', query);
|
||||
addMessageContext(ctx.chat.id, 'assistant', response);
|
||||
} catch (error) {
|
||||
console.error('AI error:', error);
|
||||
ctx.reply('Fehler bei der KI-Anfrage. Bitte überprüfe die Konfiguration.');
|
||||
}
|
||||
});
|
||||
|
||||
bot.command('start', (ctx) => {
|
||||
ctx.reply(
|
||||
'Hallo! Ich lerne von Nachrichten und generiere neue Sätze.\n\n' +
|
||||
@@ -48,8 +385,7 @@ bot.command('start', (ctx) => {
|
||||
'In Gruppen antworte ich zufällig mit eingestellter Wahrscheinlichkeit.\n\n' +
|
||||
'Ich lerne nur aus Nachrichten der jeweiligen Gruppe - jede Gruppe hat ihre eigene "Persönlichkeit".\n\n' +
|
||||
'/hilfe - Diese Hilfe anzeigen\n' +
|
||||
'/prob - Aktuelle Wahrscheinlichkeit anzeigen\n' +
|
||||
'/setprob <0-100> - Antwortwahrscheinlichkeit setzen (Admins only)'
|
||||
'/ai - KI-Befehle'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -62,7 +398,9 @@ bot.command('hilfe', (ctx) => {
|
||||
'/hilfe - Diese Hilfe\n' +
|
||||
'/prob - Antwortwahrscheinlichkeit anzeigen\n' +
|
||||
'/setprob <0-100> - Wahrscheinlichkeit setzen (Admins)\n' +
|
||||
'/cleanup - Datenbank bereinigen (Admins)\n\n' +
|
||||
'/cleanup - Datenbank bereinigen (Admins)\n' +
|
||||
'/ai - KI-Befehle\n' +
|
||||
'/ask-ai <text> - KI-Antwort generieren\n\n' +
|
||||
'Trigger für Antwort:\n' +
|
||||
'• Reply auf eine meiner Nachrichten\n' +
|
||||
'• @Erwähnung meines Namens\n' +
|
||||
@@ -110,6 +448,16 @@ bot.on('text', async (ctx) => {
|
||||
|
||||
const text = ctx.message.text;
|
||||
|
||||
// Check if this is a private message for setup wizard
|
||||
if (ctx.chat?.type === 'private') {
|
||||
// Handle setup wizard steps
|
||||
const session = getSetupSession(ctx.from.id);
|
||||
if (session && !text.startsWith('/')) {
|
||||
await handleSetupStep(ctx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip commands
|
||||
if (text.startsWith('/')) return;
|
||||
|
||||
@@ -123,7 +471,42 @@ bot.on('text', async (ctx) => {
|
||||
const isReplyToBot = ctx.message.reply_to_message?.from?.id === botId;
|
||||
const isMentioned = botUsername && text.toLowerCase().includes(`@${botUsername.toLowerCase()}`);
|
||||
|
||||
// Determine if should respond
|
||||
// Check for AI trigger word
|
||||
const aiSettings = getAISettings(ctx.chat.id);
|
||||
const isAITrigger = aiSettings.enabled && aiSettings.triggerWord && text.toLowerCase().includes(aiSettings.triggerWord.toLowerCase());
|
||||
const isAIRandom = aiSettings.enabled && aiSettings.randomProb > 0 && Math.random() * 100 < aiSettings.randomProb;
|
||||
|
||||
// Handle AI trigger
|
||||
if ((isAITrigger || isAIRandom) && aiSettings.apiKey) {
|
||||
const query = text.replace(new RegExp(aiSettings.triggerWord, 'gi'), '').trim() || text;
|
||||
const context = getMessageContext(ctx.chat.id, 50);
|
||||
const messages = context.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
|
||||
|
||||
try {
|
||||
const response = await generateAIResponse(
|
||||
{
|
||||
apiKey: aiSettings.apiKey,
|
||||
baseUrl: aiSettings.baseUrl,
|
||||
model: aiSettings.model,
|
||||
systemPrompt: AI_SYSTEM_PROMPT,
|
||||
groupPrompt: aiSettings.groupPrompt || undefined,
|
||||
},
|
||||
messages,
|
||||
query
|
||||
);
|
||||
|
||||
ctx.reply(response, { reply_parameters: { message_id: ctx.message.message_id } });
|
||||
|
||||
// Save to context
|
||||
addMessageContext(ctx.chat.id, 'user', query);
|
||||
addMessageContext(ctx.chat.id, 'assistant', response);
|
||||
} catch (error) {
|
||||
console.error('AI error:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if should respond (Markov)
|
||||
let shouldRespond = false;
|
||||
|
||||
if (isReplyToBot || isMentioned) {
|
||||
|
||||
Reference in New Issue
Block a user