code v3

Zuletzt geändert von René Schmidt am 2025/10/12 08:42

import asyncio
import logging
import json
import random
import time
import base64
import io
from datetime import datetime
from pathlib import Path
from aiohttp import web, ClientSession
from nio import AsyncClient, MatrixRoom, RoomMessageText, RoomMessageImage, RoomMessageAudio, RoomMessageFile, DownloadError

# Für Dokumenten-Verarbeitung
try:
    import PyPDF2
    import docx
    import pandas as pd
    from PIL import Image
    PDF_SUPPORT = True
except ImportError:
    PDF_SUPPORT = False
    logging.warning("⚠️  PDF/DOCX Support nicht installiert. Installiere: pip install PyPDF2 python-docx pandas pillow")

# -------------------------------
# Logging-Setup
# -------------------------------
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

# Reduziere Noise
logging.getLogger("nio").setLevel(logging.ERROR)
logging.getLogger("aiohttp").setLevel(logging.WARNING)

# -------------------------------
# Konfiguration - HIER ANPASSEN!
# -------------------------------
HOMESERVER = "https://matrix.rs-servertech.com"
USER_ID = "@terry:matrix.rs-servertech.com"
ACCESS_TOKEN = "HIER_DEIN_TOKEN"  # ← Dein Access Token
DEVICE_ID = "MATRIXBOT"

# n8n Konfiguration
N8N_WEBHOOK_URL = "https://n8n.rs-servertech.com/webhook/matrix-bot"
WEBHOOK_PORT = 8080
WEBHOOK_SECRET = "mein-sicherer-key-2024-xyz"
DEFAULT_ROOM_ID = "!dYNoYyZHeJfkqfsSFH:matrix.rs-servertech.com"

# KI Konfiguration - Ollama
OLLAMA_URL = "http://192.168.10.210:11434"
OLLAMA_MODEL = "llama3.2"  # Für Text
OLLAMA_VISION_MODEL = "llama3.2-vision"  # Für Bilder
OLLAMA_WHISPER_MODEL = "whisper"  # Für Sprache (falls installiert)
AI_ENABLED = True

# Bot-Verhalten
BOT_PREFIX = "!"
RESPOND_TO_ALL = False
RESPOND_TO_MENTIONS = True
RESPOND_TO_DM = False

# Feature-Flags
ENABLE_IMAGE_ANALYSIS = True
ENABLE_VOICE_TRANSCRIPTION = True
ENABLE_DOCUMENT_ANALYSIS = True

# -------------------------------
# Globale Variablen
# -------------------------------
client: AsyncClient = None
app = web.Application()
conversation_history = {}
active_conversations = {}

# -------------------------------
# Hilfsfunktionen für Datei-Download
# -------------------------------
async def download_file(mxc_url: str) -> bytes:
    """Lädt eine Datei von Matrix herunter."""
    try:
        response = await client.download(mxc_url)
        if isinstance(response, DownloadError):
            logger.error(f"Download-Fehler: {response}")
            return None
        return response.body
    except Exception as e:
        logger.error(f"Fehler beim Download: {e}")
        return None

# -------------------------------
# Bild-Analyse mit Ollama Vision
# -------------------------------
async def analyze_image(image_data: bytes, prompt: str = "Beschreibe dieses Bild detailliert auf Deutsch.") -> str:
    """Analysiert ein Bild mit Ollama Vision."""
    if not ENABLE_IMAGE_ANALYSIS:
        return None
   
    try:
        # Konvertiere zu Base64
        image_b64 = base64.b64encode(image_data).decode('utf-8')
       
        payload = {
            "model": OLLAMA_VISION_MODEL,
            "prompt": prompt,
            "images": [image_b64],
            "stream": False
        }
       
        async with ClientSession() as session:
            async with session.post(
                f"{OLLAMA_URL}/api/generate",
                json=payload,
                timeout=120
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    return data.get("response", "Keine Antwort")
                else:
                    logger.error(f"Ollama Vision Fehler: {resp.status}")
                    return None
    except Exception as e:
        logger.error(f"Bild-Analyse Fehler: {e}")
        return None

# -------------------------------
# Sprach-Transkription mit Whisper
# -------------------------------
async def transcribe_audio(audio_data: bytes) -> str:
    """Transkribiert Audio mit Whisper (lokal oder API)."""
    if not ENABLE_VOICE_TRANSCRIPTION:
        return None
   
    try:
        # Speichere temporär
        temp_file = f"/tmp/audio_{int(time.time())}.ogg"
        with open(temp_file, "wb") as f:
            f.write(audio_data)
       
        # Option 1: Lokales Whisper via Ollama (wenn installiert)
        try:
            async with ClientSession() as session:
                with open(temp_file, 'rb') as f:
                    form = aiohttp.FormData()
                    form.add_field('file', f, filename='audio.ogg')
                    form.add_field('model', OLLAMA_WHISPER_MODEL)
                   
                    async with session.post(
                        f"{OLLAMA_URL}/api/transcribe",
                        data=form,
                        timeout=120
                    ) as resp:
                        if resp.status == 200:
                            data = await resp.json()
                            Path(temp_file).unlink(missing_ok=True)
                            return data.get("text", "Keine Transkription")
        except Exception as e:
            logger.warning(f"Lokales Whisper nicht verfügbar: {e}")
       
        # Option 2: Whisper CLI (falls installiert)
        import subprocess
        try:
            result = subprocess.run(
                ["whisper", temp_file, "--language", "de", "--model", "base", "--output_format", "txt"],
                capture_output=True,
                text=True,
                timeout=120
            )
            Path(temp_file).unlink(missing_ok=True)
            return result.stdout.strip() if result.returncode == 0 else None
        except Exception as e:
            logger.warning(f"Whisper CLI nicht verfügbar: {e}")
       
        Path(temp_file).unlink(missing_ok=True)
        return "⚠️ Whisper ist nicht installiert. Installiere mit: pip install openai-whisper"
       
    except Exception as e:
        logger.error(f"Audio-Transkription Fehler: {e}")
        return None

# -------------------------------
# Dokumenten-Analyse
# -------------------------------
async def analyze_document(file_data: bytes, filename: str) -> str:
    """Analysiert verschiedene Dokumenttypen."""
    if not ENABLE_DOCUMENT_ANALYSIS:
        return None
   
    try:
        file_ext = Path(filename).suffix.lower()
       
        # PDF
        if file_ext == ".pdf" and PDF_SUPPORT:
            pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_data))
            text = ""
            for page in pdf_reader.pages[:10]:  # Erste 10 Seiten
                text += page.extract_text() + "\n"
           
            if len(text) > 3000:
                text = text[:3000] + "...\n\n(Text gekürzt)"
           
            return f"📄 **PDF Inhalt:**\n\n{text}\n\n_(Erste Seiten)_"
       
        # Word
        elif file_ext in [".docx", ".doc"] and PDF_SUPPORT:
            doc = docx.Document(io.BytesIO(file_data))
            text = "\n".join([p.text for p in doc.paragraphs[:50]])
           
            if len(text) > 3000:
                text = text[:3000] + "..."
           
            return f"📝 **Word Dokument:**\n\n{text}"
       
        # Text
        elif file_ext in [".txt", ".md", ".log"]:
            text = file_data.decode('utf-8', errors='ignore')
            if len(text) > 3000:
                text = text[:3000] + "..."
            return f"📃 **Text-Datei:**\n\n```\n{text}\n```"
       
        # CSV/Excel
        elif file_ext in [".csv", ".xlsx", ".xls"] and PDF_SUPPORT:
            if file_ext == ".csv":
                df = pd.read_csv(io.BytesIO(file_data))
            else:
                df = pd.read_excel(io.BytesIO(file_data))
           
            info = f"📊 **Tabelle:** {len(df)} Zeilen, {len(df.columns)} Spalten\n\n"
            info += f"**Spalten:** {', '.join(df.columns.tolist())}\n\n"
            info += f"**Erste Zeilen:**\n```\n{df.head(5).to_string()}\n```"
           
            return info
       
        # JSON
        elif file_ext == ".json":
            data = json.loads(file_data.decode('utf-8'))
            text = json.dumps(data, indent=2, ensure_ascii=False)
            if len(text) > 2000:
                text = text[:2000] + "..."
            return f"📋 **JSON:**\n\n```json\n{text}\n```"
       
        else:
            return f"ℹ️ Dateiformat `.{file_ext}` wird noch nicht unterstützt.\n\nUnterstützt: PDF, DOCX, TXT, CSV, XLSX, JSON"
   
    except Exception as e:
        logger.error(f"Dokument-Analyse Fehler: {e}")
        return f"❌ Fehler beim Analysieren: {str(e)}"

# -------------------------------
# KI-Integration (Ollama Text)
# -------------------------------
async def ask_ai(message: str, user_id: str) -> str:
    """Sendet Nachricht an Ollama."""
    if not AI_ENABLED:
        return None
   
    if user_id not in conversation_history:
        conversation_history[user_id] = []
   
    conversation_history[user_id].append({"role": "user", "content": message})
   
    if len(conversation_history[user_id]) > 20:
        conversation_history[user_id] = conversation_history[user_id][-20:]
   
    try:
        messages = [
            {"role": "system", "content": "Du bist Terry, ein hilfreicher Matrix-Bot. Antworte kurz, freundlich und auf Deutsch."},
            *conversation_history[user_id]
        ]
       
        payload = {
            "model": OLLAMA_MODEL,
            "messages": messages,
            "stream": False,
            "options": {"temperature": 0.7, "num_predict": 300}
        }
       
        async with ClientSession() as session:
            async with session.post(
                f"{OLLAMA_URL}/api/chat",
                json=payload,
                timeout=60
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    answer = data["message"]["content"]
                    conversation_history[user_id].append({"role": "assistant", "content": answer})
                    return answer
                else:
                    logger.error(f"Ollama Fehler: {resp.status}")
                    return None
    except Exception as e:
        logger.error(f"KI-Fehler: {e}")
        return None

# -------------------------------
# Webhook & Health Check
# -------------------------------
async def webhook_handler(request):
    """n8n Webhook."""
    try:
        data = await request.json()
        if data.get("secret") != WEBHOOK_SECRET:
            return web.json_response({"error": "Unauthorized"}, status=401)
       
        message = data.get("message")
        room_id = data.get("room_id", DEFAULT_ROOM_ID)
       
        if not message:
            return web.json_response({"error": "No message"}, status=400)
       
        await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": message})
        return web.json_response({"status": "success"})
    except Exception as e:
        return web.json_response({"error": str(e)}, status=500)

async def health_check(request):
    """Health Check."""
    return web.json_response({
        "status": "online",
        "features": {
            "images": ENABLE_IMAGE_ANALYSIS,
            "voice": ENABLE_VOICE_TRANSCRIPTION,
            "documents": ENABLE_DOCUMENT_ANALYSIS and PDF_SUPPORT
        }
    })

async def trigger_n8n(trigger_type: str, data: dict):
    """n8n Trigger."""
    try:
        payload = {"trigger": trigger_type, "data": data, "timestamp": time.time()}
        async with ClientSession() as session:
            async with session.post(N8N_WEBHOOK_URL, json=payload, timeout=10) as resp:
                return await resp.json() if resp.status == 200 else None
    except:
        return None

# -------------------------------
# Command Handler
# -------------------------------
async def handle_command(room: MatrixRoom, event: RoomMessageText, message: str):
    """Befehle verarbeiten."""
    response = None
    parts = message.split()
    command = parts[0].lower() if parts else ""
    args = parts[1:] if len(parts) > 1 else []
   
    if message.startswith(BOT_PREFIX) or message.startswith("/"):
       
        if command in ["!start", "!hilfe", "/start", "/hilfe"]:
            response = f"""🤖 **Terry Bot - Multimedia Edition**

**Chat:**
• `hey terry` - Konversation starten
• `stop` - Beenden
• `/status` - Status

**Medien:**
• 🖼️ Bild senden → Automatische Analyse
• 🎤 Sprachnachricht → Transkription
• 📄 Dokument → Text-Extraktion

**Befehle:**
• `/würfel` `/münze` `/rechne` `/zeit` `/witz`
• `/webhook` - n8n Info
• `/features` - Verfügbare Features

Schick mir einfach Bilder, Sprache oder Dokumente! 📤"""
       
        elif command in ["!status", "/status"]:
            active = len(active_conversations)
            response = f"""📊 **Bot Status**

**KI:** ✅ Ollama ({OLLAMA_MODEL})
**Features:**
• 🖼️ Bildanalyse: {'✅' if ENABLE_IMAGE_ANALYSIS else '❌'}
• 🎤 Spracherkennung: {'✅' if ENABLE_VOICE_TRANSCRIPTION else '❌'}
• 📄 Dokumente: {'✅' if ENABLE_DOCUMENT_ANALYSIS and PDF_SUPPORT else '❌'}

**Konversationen:** {active} aktiv
**Räume:** {len(client.rooms)}"""
       
        elif command in ["!features", "/features"]:
            response = f"""✨ **Verfügbare Features**

**🖼️ Bildanalyse:**
• Bildbeschreibung mit KI
• Objekt-Erkennung
• Text-Extraktion (OCR)
→ Einfach Bild hochladen!

**🎤 Spracherkennung:**
• Audio → Text
• Deutsch & Englisch
→ Sprachnachricht senden!

**📄 Dokumentenanalyse:**
• PDF, Word, Excel, CSV
• Text-Extraktion
• Daten-Übersicht
→ Datei hochladen!

**Beispiel:**
Lade ein Foto hoch und frage:
"Was ist auf dem Bild?"
"Welcher Text steht da?"
"Beschreibe die Szene"
"""
       
        elif command in ["!würfel", "/würfel"]:
            response = f"🎲 **{random.randint(1, 6)}**"
       
        elif command in ["!münze", "/münze"]:
            response = random.choice(["Kopf 🪙", "Zahl 🔢"])
       
        elif command in ["!zeit", "/zeit"]:
            response = f"🕐 **{datetime.now().strftime('%H:%M:%S')}** Uhr"
       
        elif command in ["!witz", "/witz"]:
            witze = [
                "Warum können Geister so schlecht lügen? Weil sie so leicht zu durchschauen sind! 👻",
                "Was ist grün und steht vor der Tür? Ein Klopfsalat! 🥬",
                "Warum summen Bienen? Weil sie den Text nicht kennen! 🐝"
            ]
            response = random.choice(witze)
       
        elif command in ["!rechne", "/rechne"]:
            if not args:
                response = "❌ Beispiel: `/rechne 5 + 3`"
            else:
                try:
                    expr = " ".join(args)
                    if all(c in "0123456789+-*/(). " for c in expr):
                        result = eval(expr)
                        response = f"🧮 `{expr}` = **{result}**"
                    else:
                        response = "❌ Nur Zahlen und +-*/() erlaubt"
                except:
                    response = "❌ Rechenfehler"
       
        elif command in ["!webhook", "/webhook"]:
            response = f"""📡 **Webhook**
POST http://YOUR_IP:{WEBHOOK_PORT}/webhook
Secret: {WEBHOOK_SECRET}
Room: {room.room_id}"""
   
    # Normale Nachricht
    else:
        user_key = f"{event.sender}_{room.room_id}"
       
        activation_words = ["hey terry", "hallo terry", "hi terry", "terry"]
        is_activation = any(message.lower().startswith(w) for w in activation_words)
       
        deactivation_words = ["stop", "bye terry", "tschüss terry"]
        is_deactivation = any(w in message.lower() for w in deactivation_words)
       
        if is_deactivation and user_key in active_conversations:
            del active_conversations[user_key]
            response = "👋 Okay, bis später! Schreib 'hey terry' zum Aktivieren."
       
        elif is_activation:
            active_conversations[user_key] = time.time()
            clean_message = message
            for w in activation_words:
                clean_message = clean_message.lower().replace(w, "").strip()
           
            if clean_message and AI_ENABLED:
                ai_response = await ask_ai(clean_message, event.sender)
                response = ai_response if ai_response else "🤖 Ollama antwortet nicht."
            else:
                response = "👋 Hi! Ich bin aktiviert. Schreib drauf los oder schick mir Medien!\n\n'stop' zum Beenden."
       
        elif user_key in active_conversations:
            if AI_ENABLED:
                ai_response = await ask_ai(message, event.sender)
                response = ai_response if ai_response else "🤖 Ollama Problem."
   
    if response:
        try:
            await client.room_send(room.room_id, "m.room.message", {"msgtype": "m.text", "body": response})
            logger.info(f"✓ Antwort → {event.sender.split(':')[0]}")
        except Exception as e:
            logger.error(f"Senden fehlgeschlagen: {e}")

# -------------------------------
# Message Callbacks
# -------------------------------
async def message_callback(room: MatrixRoom, event: RoomMessageText):
    """Text Messages."""
    if event.sender == client.user_id:
        return
   
    if hasattr(event, 'server_timestamp'):
        age = time.time() * 1000 - event.server_timestamp
        if age > 10000:
            return
   
    message = event.body.strip()
    logger.info(f"📨 {event.sender.split(':')[0]}: {message[:50]}...")
    await handle_command(room, event, message)

async def image_callback(room: MatrixRoom, event: RoomMessageImage):
    """Bild-Messages."""
    if event.sender == client.user_id or not ENABLE_IMAGE_ANALYSIS:
        return
   
    logger.info(f"🖼️ Bild von {event.sender.split(':')[0]}")
   
    try:
        image_data = await download_file(event.url)
        if not image_data:
            await client.room_send(room.room_id, "m.room.message", 
                {"msgtype": "m.text", "body": "❌ Konnte Bild nicht laden"})
            return
       
        # Prüfe ob User aktiv ist oder Bildanalyse explizit gewünscht
        user_key = f"{event.sender}_{room.room_id}"
        if user_key in active_conversations:
            analysis = await analyze_image(image_data)
            if analysis:
                response = f"🖼️ **Bildanalyse:**\n\n{analysis}"
                await client.room_send(room.room_id, "m.room.message", 
                    {"msgtype": "m.text", "body": response})
        else:
            await client.room_send(room.room_id, "m.room.message",
                {"msgtype": "m.text", "body": "🖼️ Bild empfangen! Schreib 'hey terry' um es analysieren zu lassen."})
   
    except Exception as e:
        logger.error(f"Bild-Verarbeitung Fehler: {e}")

async def audio_callback(room: MatrixRoom, event: RoomMessageAudio):
    """Audio-Messages."""
    if event.sender == client.user_id or not ENABLE_VOICE_TRANSCRIPTION:
        return
   
    logger.info(f"🎤 Audio von {event.sender.split(':')[0]}")
   
    try:
        audio_data = await download_file(event.url)
        if not audio_data:
            return
       
        user_key = f"{event.sender}_{room.room_id}"
        if user_key in active_conversations:
            await client.room_send(room.room_id, "m.room.message",
                {"msgtype": "m.text", "body": "🎤 Transkribiere Audio..."})
           
            transcription = await transcribe_audio(audio_data)
            if transcription:
                response = f"🎤 **Transkription:**\n\n{transcription}"
               
                # Frage KI basierend auf Transkription
                if AI_ENABLED:
                    ai_response = await ask_ai(transcription, event.sender)
                    if ai_response:
                        response += f"\n\n**Antwort:**\n{ai_response}"
               
                await client.room_send(room.room_id, "m.room.message",
                    {"msgtype": "m.text", "body": response})
            else:
                await client.room_send(room.room_id, "m.room.message",
                    {"msgtype": "m.text", "body": "❌ Transkription fehlgeschlagen"})
        else:
            await client.room_send(room.room_id, "m.room.message",
                {"msgtype": "m.text", "body": "🎤 Sprachnachricht! Schreib 'hey terry' für Transkription."})
   
    except Exception as e:
        logger.error(f"Audio-Verarbeitung Fehler: {e}")

async def file_callback(room: MatrixRoom, event: RoomMessageFile):
    """Datei-Messages."""
    if event.sender == client.user_id or not ENABLE_DOCUMENT_ANALYSIS:
        return
   
    filename = event.body
    logger.info(f"📄 Datei von {event.sender.split(':')[0]}: {filename}")
   
    try:
        file_data = await download_file(event.url)
        if not file_data:
            return
       
        user_key = f"{event.sender}_{room.room_id}"
        if user_key in active_conversations:
            await client.room_send(room.room_id, "m.room.message",
                {"msgtype": "m.text", "body": f"📄 Analysiere '{filename}'..."})
           
            analysis = await analyze_document(file_data, filename)
            if analysis:
                await client.room_send(room.room_id, "m.room.message",
                    {"msgtype": "m.text", "body": analysis})
            else:
                await client.room_send(room.room_id, "m.room.message",
                    {"msgtype": "m.text", "body": "❌ Dokument-Analyse fehlgeschlagen"})
        else:
            await client.room_send(room.room_id, "m.room.message",
                {"msgtype": "m.text", "body": f"📄 Datei '{filename}' empfangen! Schreib 'hey terry' für Analyse."})
   
    except Exception as e:
        logger.error(f"Datei-Verarbeitung Fehler: {e}")

# -------------------------------
# Server & Client
# -------------------------------
async def start_webserver():
    """Webhook-Server."""
    app.router.add_post('/webhook', webhook_handler)
    app.router.add_get('/health', health_check)
   
    runner = web.AppRunner(app)
    await runner.setup()
    site = web.TCPSite(runner, '0.0.0.0', WEBHOOK_PORT)
    await site.start()
    logger.info(f"🌐 Webhook: Port {WEBHOOK_PORT}")

async def start_matrix_client():
    """Matrix Client."""
    global client
   
    client = AsyncClient(HOMESERVER, USER_ID, device_id=DEVICE_ID)
    client.access_token = ACCESS_TOKEN
   
    # Registriere alle Callbacks
    client.add_event_callback(message_callback, RoomMessageText)
    client.add_event_callback(image_callback, RoomMessageImage)
    client.add_event_callback(audio_callback, RoomMessageAudio)
    client.add_event_callback(file_callback, RoomMessageFile)
   
    await client.sync(timeout=30000, full_state=True)
   
    logger.info(f"✓ In {len(client.rooms)} Räumen")
    for room_id, room in client.rooms.items():
        logger.info(f"   • {room.display_name or room_id}")
   
    await client.sync_forever(timeout=30000)

async def main():
    """Hauptfunktion."""
    logger.info("=" * 70)
    logger.info("🤖 Terry Bot - Multimedia Edition v4.0")
    logger.info("=" * 70)
    logger.info(f"🧠 KI: Ollama ({OLLAMA_MODEL})")
    logger.info(f"🖼️ Bilder: {'✅' if ENABLE_IMAGE_ANALYSIS else '❌'}")
    logger.info(f"🎤 Sprache: {'✅' if ENABLE_VOICE_TRANSCRIPTION else '❌'}")
    logger.info(f"📄 Dokumente: {'✅' if ENABLE_DOCUMENT_ANALYSIS and PDF_SUPPORT else '❌'}")
    logger.info("=" * 70)
   
    try:
        await asyncio.gather(
            start_webserver(),
            start_matrix_client()
        )
    except KeyboardInterrupt:
        logger.info("\n⏹️  Bot beendet")
    finally:
        if client:
            await client.close()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass

    

Anwendungen

Benötigen Sie Hilfe?

Wenn Sie Hilfe mit XWiki benötigen, wenden Sie sich an: