diff --git a/source/main.py b/source/main.py index 5325fc7..05eb125 100644 --- a/source/main.py +++ b/source/main.py @@ -63,7 +63,10 @@ templates = Jinja2Templates(directory=BASE_DIR / "templates") app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") SSH_KEY = os.path.expanduser("~/.ssh/id_rsa") -chat_history = [] +# Ein Dictionary für verschiedene Chat-Historien +chat_histories = { + "private": [] # Hier landen Webchat UND private Telegram-Nachrichten +} # KI KONFIGURATION AI_PROVIDER = os.getenv("AI_PROVIDER", "google").lower() @@ -136,17 +139,15 @@ def get_system_prompt(current_user=WEB_USER_NAME): # --- KI FUNKTIONEN --- -async def get_ai_response(user_input, system_prompt): - global chat_history - now = datetime.now().strftime("%d.%m.%Y %H:%M") - chat_history.append({"role": "user", "content": user_input, "timestamp": now}) - chat_history = chat_history[-30:] +async def get_ai_response(user_msg, system_prompt, history_list): ai_msg = "" try: # Kombinierte Logik für OpenAI, Ollama und NVIDIA (alle nutzen das OpenAI SDK) if AI_PROVIDER in ["openai", "ollama", "nvidia"]: - messages = [{"role": "system", "content": system_prompt}] + chat_history + # WICHTIG: Wir nutzen die übergebene 'history_list', NICHT starr "private" + # Da der Aufrufer (Telegram/Web) die aktuelle Frage schon hinzugefügt hat, ist sie hier schon drin. + messages = [{"role": "system", "content": system_prompt}] + history_list if AI_PROVIDER == "ollama": url = OLLAMA_BASE_URL @@ -163,7 +164,6 @@ async def get_ai_response(user_input, system_prompt): key = OPENAI_API_KEY model_to_use = OPENAI_MODEL - # WICHTIG: Hier .AsyncOpenAI nutzen, da die Funktion async ist client = openai.AsyncOpenAI(base_url=url, api_key=key) response = await client.chat.completions.create( model=model_to_use, @@ -172,41 +172,36 @@ async def get_ai_response(user_input, system_prompt): ai_msg = response.choices[0].message.content elif AI_PROVIDER == "google": - # Für Google Gemini if not GOOGLE_API_KEY: return "Fehler: GOOGLE_API_KEY fehlt in der .env Datei!" client = genai.Client(api_key=GOOGLE_API_KEY) - # Wir müssen unser Array in das spezielle Google-Format umwandeln google_history = [] - # Alle Nachrichten AUSSER der allerletzten (die aktuelle User-Frage) in die History packen - for msg in chat_history[:-1]: + # Alle Nachrichten AUSSER der allerletzten (das ist die aktuelle Frage von gerade eben) + for msg in history_list[:-1]: role = "user" if msg["role"] == "user" else "model" google_history.append( types.Content(role=role, parts=[types.Part.from_text(text=msg["content"])]) ) - # Chat MIT dem übersetzten Gedächtnis starten chat = client.chats.create( model=GOOGLE_MODEL, config=types.GenerateContentConfig(system_instruction=system_prompt), history=google_history ) - # Jetzt erst die neue Nachricht an den Chat mit Gedächtnis schicken - response = chat.send_message(user_input) + # Sende die aktuelle Nachricht an den Chat + response = chat.send_message(user_msg) ai_msg = response.text except Exception as e: ai_msg = f"Fehler bei der KI-Anfrage: {e}" print(f"KI Fehler: {e}") - # 3. Die Antwort der KI ebenfalls ins Gedächtnis aufnehmen - now = datetime.now().strftime("%d.%m.%Y %H:%M") - chat_history.append({"role": "assistant", "content": ai_msg, "timestamp": now}) - + # Wir geben nur den Text zurück. Das Speichern der KI-Antwort in die Historie + # erledigt der Aufrufer (Web-Chat Endpoint oder Telegram Funktion)! return ai_msg async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -216,37 +211,54 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_ chat_type = update.effective_chat.type user_msg = update.message.text user_id = str(update.message.from_user.id) + chat_id = str(update.effective_chat.id) - # 1. Gruppen-Logik: Nur reagieren, wenn der Bot @erwähnt wird + # 1. Weiche für Gruppen-Logik & Historien-Zuordnung if chat_type in ['group', 'supergroup']: if bot_username not in user_msg: return # Bot wurde nicht erwähnt, Nachricht ignorieren - # Den @Namen aus dem Text entfernen, damit die KI nicht verwirrt wird + # Den @Namen aus dem Text entfernen user_msg = user_msg.replace(bot_username, "").strip() + + # Jede Gruppe bekommt ihr komplett eigenes Gedächtnis + history_key = chat_id else: - # Im Einzelchat kann optional weiterhin nur der Admin zugelassen werden. - # Wenn du willst, dass auch andere den Bot privat nutzen können, entferne diesen Block: + # Sicherheitscheck im Einzelchat if user_id != ALLOWED_ID: await update.message.reply_text("Zugriff auf den privaten Chat verweigert. 🔒") return + + # Privat-Chats teilen sich das Gedächtnis mit dem Webchat + history_key = "private" - # Tipp-Status anzeigen - await update.message.reply_chat_action(action="typing") + # Sicherstellen, dass die Liste für diesen Chat existiert + if history_key not in chat_histories: + chat_histories[history_key] = [] + + current_history = chat_histories[history_key] + + # Nutzer-Nachricht mit Zeitstempel in die RICHTIGE Historie aufnehmen + now = datetime.now().strftime("%d.%m.%Y %H:%M") + current_history.append({"role": "user", "content": user_msg, "timestamp": now}) - # 2. KI fragen # Vorname des Telegram-Nutzers auslesen sender_name = update.message.from_user.first_name # Tipp-Status anzeigen await update.message.reply_chat_action(action="typing") - # 2. KI fragen (und den dynamischen Namen übergeben!) - ai_response = await get_ai_response(user_msg, get_system_prompt(sender_name)) + # 2. KI fragen (Wir übergeben die spezifische Historie!) + # HINWEIS: Stelle sicher, dass deine get_ai_response Funktion die current_history auch annimmt. + ai_response = await get_ai_response(user_msg, get_system_prompt(sender_name), current_history) commands = re.findall(r'(.*?)', ai_response, re.I | re.S) clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() + # KI-Antwort ebenfalls in die Historie speichern + now = datetime.now().strftime("%d.%m.%Y %H:%M") + current_history.append({"role": "assistant", "content": clean_msg, "timestamp": now}) + # KI Text-Antwort senden if clean_msg: await update.message.reply_text(clean_msg) @@ -277,13 +289,19 @@ async def handle_telegram_message(update: Update, context: ContextTypes.DEFAULT_ result_text = output[:4000] if output else "✅ Befehl ohne Output ausgeführt." await update.message.reply_text(f"💻 **Output von {n['name']}:**\n```\n{result_text}\n```", parse_mode='Markdown') + + # System-Output auch in die RICHTIGE Historie packen, damit die KI weiß, was passiert ist now = datetime.now().strftime("%d.%m.%Y %H:%M") - chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}", "timestamp": now}) + current_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {target} fertig:\n{result_text}", "timestamp": now}) except Exception as e: await update.message.reply_text(f"❌ Fehler bei der Ausführung: {e}") else: await update.message.reply_text(f"⚠️ Node '{target}' nicht in der Datenbank gefunden.") + # Optionaler Bonus: Halte die Historie sauber (z.B. max. 15 Nachrichten), damit der RAM nicht überläuft + if len(current_history) > 15: + chat_histories[history_key] = current_history[-15:] + # --- FASTAPI LIFESPAN EVENTS (Bot starten/stoppen) --- @app.on_event("startup") async def startup_event(): @@ -599,37 +617,80 @@ async def terminal_websocket(websocket: WebSocket, ip: str): @app.websocket("/ws/chat") async def chat_endpoint(websocket: WebSocket): await websocket.accept() + # Sicherstellen, dass die private History existiert + if "private" not in chat_histories: + chat_histories["private"] = [] + try: while True: user_msg = await websocket.receive_text() - ai_response = await get_ai_response(user_msg, get_system_prompt()) + + # 1. User-Nachricht in Historie speichern + now = datetime.now().strftime("%d.%m.%Y %H:%M") + chat_histories["private"].append({"role": "user", "content": user_msg, "timestamp": now}) + + # 2. KI fragen (mit der Historie!) + ai_response = await get_ai_response(user_msg, get_system_prompt(), chat_histories["private"]) + commands = re.findall(r'(.*?)', ai_response, re.I | re.S) clean_msg = re.sub(r'.*?', '', ai_response, flags=re.I | re.S).strip() - if clean_msg: await websocket.send_text(clean_msg) + + # 3. KI-Antwort in Historie speichern + if clean_msg: + now = datetime.now().strftime("%d.%m.%Y %H:%M") + chat_histories["private"].append({"role": "assistant", "content": clean_msg, "timestamp": now}) + await websocket.send_text(clean_msg) + + # Begrenzung der Historie + if len(chat_histories["private"]) > 30: + chat_histories["private"] = chat_histories["private"][-30:] + if commands: tasks = [] for target, cmd in commands: - conn = get_db(); n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone(); conn.close() - if n: tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip())) + conn = get_db() + n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target.strip(), target.strip())).fetchone() + conn.close() + if n: + # Wir übergeben hier die History, damit run_remote_task weiß, wo er loggen soll + tasks.append(run_remote_task(n['ip'], n['user'], cmd.strip(), "private")) + if tasks: await websocket.send_text("ℹ️ *Führe Befehle aus...*") await asyncio.gather(*tasks) - summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt()) + # Nochmal KI fragen für Zusammenfassung (optional) + summary = await get_ai_response("Zusammenfassung der Ergebnisse?", get_system_prompt(), chat_histories["private"]) await websocket.send_text(f"--- Info ---\n{summary}") - except: pass + except WebSocketDisconnect: + pass + except Exception as e: + print(f"Webchat Fehler: {e}") -async def run_remote_task(ip, user, cmd): +async def run_remote_task(ip, user, cmd, history_key="private"): # history_key als Parameter await manager.broadcast(f"🚀 Task: {cmd} auf {ip}") - proc = await asyncio.create_subprocess_shell(f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} '{cmd}'", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) + proc = await asyncio.create_subprocess_shell( + f"ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null {user}@{ip} '{cmd}'", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT + ) full_output = "" while True: line = await proc.stdout.readline() if not line: break out = line.decode('utf-8', errors='ignore').strip() - if out: await manager.broadcast(f"🛠️ {out}"); full_output += out + "\n" + if out: + await manager.broadcast(f"🛠️ {out}") + full_output += out + "\n" await proc.wait() + now = datetime.now().strftime("%d.%m.%Y %H:%M") - chat_history.append({"role": "user", "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}", "timestamp": now}) + # Jetzt wird in die richtige Historie geschrieben! + if history_key in chat_histories: + chat_histories[history_key].append({ + "role": "user", + "content": f"[SYSTEM] Befehl '{cmd}' auf {ip} fertig:\n{full_output or 'Kein Output'}", + "timestamp": now + }) # --- Settings API --- @app.get("/api/settings")