diff --git a/jarvis.py b/jarvis.py index 28316bf..319661d 100644 --- a/jarvis.py +++ b/jarvis.py @@ -1,29 +1,41 @@ import os -import re -import sqlite3 import asyncio +import sqlite3 +import re +from datetime import datetime +from pathlib import Path import subprocess import openai from google import genai from google.genai import types -from datetime import datetime -from pathlib import Path from dotenv import load_dotenv +# --- FARBEN FÜR DAS TERMINAL --- +class Color: + USER = '\033[94m' # Blau + JARVIS = '\033[92m' # Grün + SYSTEM = '\033[93m' # Gelb + EXEC = '\033[95m' # Magenta für SSH + ERROR = '\033[91m' # Rot + RESET = '\033[0m' # Zurücksetzen + BOLD = '\033[1m' + # --- PFADE & SETUP --- BASE_DIR = Path(__file__).resolve().parent -CONFIG_DIR = BASE_DIR / "config" -DATA_DIR = BASE_DIR / "data" -WORKSPACE_DIR = BASE_DIR / "workspace" +ROOT_DIR = BASE_DIR.parent +CONFIG_DIR = ROOT_DIR / "config" +DATA_DIR = ROOT_DIR / "data" +WORKSPACE_DIR = ROOT_DIR / "workspace" ENV_FILE = CONFIG_DIR / ".env" load_dotenv(ENV_FILE) DB_PATH = DATA_DIR / "cluster.db" +PROMPT_FILE = CONFIG_DIR / "system_prompt.txt" NOTES_FILE = WORKSPACE_DIR / "NOTIZEN.md" TODO_FILE = WORKSPACE_DIR / "TODO.md" -WEB_USER_NAME = os.getenv("WEB_USER_NAME", "Meik") +WEB_USER_NAME = os.getenv("WEB_USER_NAME", "Admin") # Ordner & Dateien anlegen for d in [WORKSPACE_DIR, DATA_DIR, CONFIG_DIR]: @@ -40,7 +52,7 @@ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "") NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY", "") OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1") -GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.5-flash") +GOOGLE_MODEL = os.getenv("GOOGLE_MODEL", "gemini-2.0-flash") OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3") NVIDIA_MODEL = os.getenv("NVIDIA_MODEL", "moonshotai/kimi-k2.5") @@ -66,14 +78,14 @@ def get_db(): conn.row_factory = sqlite3.Row return conn -def get_system_prompt(): +def get_system_prompt(current_user=WEB_USER_NAME): prompt_path = CONFIG_DIR / "system_prompt.txt" - prompt = prompt_path.read_text(encoding="utf-8") if prompt_path.exists() else f"Hallo {WEB_USER_NAME}, ich bin J.A.R.V.I.S." + if prompt_path.exists(): + prompt = prompt_path.read_text(encoding="utf-8") + else: + prompt = f"Hallo {current_user}, ich bin J.A.R.V.I.S., dein lokaler Systemassistent." - prompt = prompt.replace("{user_name}", WEB_USER_NAME) - prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR)) - prompt = prompt.replace("{notes_file}", str(NOTES_FILE)) - prompt = prompt.replace("{todo_file}", str(TODO_FILE)) + prompt = prompt.replace("{user_name}", current_user) conn = get_db() nodes = conn.execute('SELECT * FROM nodes').fetchall() @@ -81,9 +93,15 @@ def get_system_prompt(): node_info = "" for n in nodes: - node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}\n" + docker_str = "Ja" if n['docker_installed'] else "Nein" + node_info += f"- Name: {n['name']}, IP: {n['ip']}, User: {n['user']}, OS: {n['os']}, Arch: {n['arch']}, Docker: {docker_str}\n" - return prompt.replace("{node_info}", node_info) + prompt = prompt.replace("{node_info}", node_info) + prompt = prompt.replace("{workspace_dir}", str(WORKSPACE_DIR)) + prompt = prompt.replace("{notes_file}", str(NOTES_FILE)) + prompt = prompt.replace("{todo_file}", str(TODO_FILE)) + + return prompt # --- KI KOMMUNIKATION --- async def get_ai_response(user_msg, system_prompt, history_list): @@ -104,11 +122,14 @@ async def get_ai_response(user_msg, system_prompt, history_list): return response.choices[0].message.content elif AI_PROVIDER == "google": + if not GOOGLE_API_KEY: return "Fehler: GOOGLE_API_KEY fehlt!" client = genai.Client(api_key=GOOGLE_API_KEY) + google_history = [ types.Content(role="user" if msg["role"] == "user" else "model", parts=[types.Part.from_text(text=msg["content"])]) for msg in history_list[:-1] ] + chat = client.chats.create( model=GOOGLE_MODEL, config=types.GenerateContentConfig(system_instruction=system_prompt), @@ -117,84 +138,84 @@ async def get_ai_response(user_msg, system_prompt, history_list): return chat.send_message(user_msg).text except Exception as e: - return f"Fehler bei der KI-Anfrage: {e}" + return f"{Color.ERROR}Fehler bei der KI-Anfrage: {e}{Color.RESET}" -# --- BEFEHLSAUSFÜHRUNG (Lokal & Remote) --- -async def run_task(target, cmd): - print(f"\n⚙️ J.A.R.V.I.S. führt aus auf [{target}]: {cmd}") +# --- SSH BEFEHLSAUSFÜHRUNG --- +async def run_remote_task(target, cmd): + print(f"\n{Color.EXEC}🚀 J.A.R.V.I.S. führt aus auf {target}:{Color.RESET}\n> {cmd}\n") + conn = get_db() + n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target, target)).fetchone() + conn.close() + + if not n: + msg = f"{Color.ERROR}⚠️ Node '{target}' nicht in der Datenbank gefunden.{Color.RESET}" + print(msg) + return msg + try: - if target.lower() == "localhost" or target == "127.0.0.1": - # Lokale Ausführung (für Notizen, Workspace etc.) - proc = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) - else: - # Remote Ausführung via SSH - conn = get_db() - n = conn.execute('SELECT * FROM nodes WHERE ip=? OR name=?', (target, target)).fetchone() - conn.close() - - if not n: - return f"⚠️ Node '{target}' nicht in der DB gefunden." - - ssh_cmd = f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR {n['user']}@{n['ip']} '{cmd}'" - proc = await asyncio.create_subprocess_shell(ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) - + proc = await asyncio.create_subprocess_shell( + f"ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR {n['user']}@{n['ip']} '{cmd}'", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT + ) stdout, _ = await proc.communicate() output = stdout.decode('utf-8', errors='ignore').strip() - print(f"💻 Output:\n{output or '✅ Erfolgreich ausgeführt.'}\n") + + print(f"{Color.EXEC}💻 Output:\n{output or '✅ Ohne Output ausgeführt.'}{Color.RESET}\n") return output except Exception as e: - err = f"❌ Fehler: {e}" + err = f"{Color.ERROR}❌ Fehler: {e}{Color.RESET}" print(err) return err -# --- MODULARE I/O SCHNITTSTELLEN (Für spätere Sprachsteuerung) --- -async def listen_to_user(): - # Später: Hier Mikrofon-Aufnahme und Vosk-Transkription einbauen - return await asyncio.to_thread(input, "\nDu: ") - -async def speak_to_user(text): - # Später: Hier Piper TTS einbauen, um den Text vorzulesen - print(f"\n🤖 J.A.R.V.I.S.:\n{text}") - -# --- HAUPT-LOOP --- +# --- HAUPT-TERMINAL-LOOP --- async def main_chat_loop(): - print("====================================================") - print("🤖 J.A.R.V.I.S. Terminal Interface geladen.") - print(f"Provider: {AI_PROVIDER.upper()}") - print("Tippe 'exit', um zu beenden.") - print("====================================================") + # Bunter Boot-Screen + print(f"{Color.SYSTEM}===================================================={Color.RESET}") + print(f"{Color.BOLD}{Color.JARVIS}🤖 J.A.R.V.I.S. Terminal Interface geladen.{Color.RESET}") + print(f"{Color.SYSTEM}Provider: {AI_PROVIDER.upper()}") + print(f"Tippe 'exit' oder 'quit' um zu beenden.{Color.RESET}") + print(f"{Color.SYSTEM}===================================================={Color.RESET}\n") chat_history = [] + system_prompt = get_system_prompt() while True: - user_msg = await listen_to_user() + # User Eingabe abwarten (Blau und Fett) + user_msg = await asyncio.to_thread(input, f"\n{Color.BOLD}{Color.USER}Du: {Color.RESET}") if user_msg.lower().strip() in ['exit', 'quit']: - print("J.A.R.V.I.S. geht offline. Auf Wiedersehen!") + print(f"{Color.SYSTEM}J.A.R.V.I.S. wird beendet. Auf Wiedersehen!{Color.RESET}") break if not user_msg.strip(): continue now = datetime.now().strftime("%d.%m.%Y %H:%M") chat_history.append({"role": "user", "content": user_msg, "timestamp": now}) - - print("...", end="\r") # Kleiner Ladeindikator - system_prompt = get_system_prompt() + # Ladeindikator + print(f"{Color.SYSTEM}J.A.R.V.I.S. verarbeitet...{Color.RESET}", end="\r") + + # KI antworten lassen ai_response = await get_ai_response(user_msg, system_prompt, chat_history) - # XML-Befehle extrahieren und aus dem sichtbaren Text entfernen + # Nach Tags suchen 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 speak_to_user(clean_msg) - chat_history.append({"role": "assistant", "content": clean_msg, "timestamp": now}) + # Ladezeile säubern und KI-Antwort ausgeben (Grün) + print("\r" + " " * 30 + "\r", end="") + print(f"{Color.BOLD}{Color.JARVIS}🤖 J.A.R.V.I.S.:{Color.RESET}") + print(f"{Color.JARVIS}{clean_msg}{Color.RESET}") + chat_history.append({"role": "assistant", "content": clean_msg, "timestamp": now}) + + # Ausführung der gefundenen Befehle if commands: for target, cmd in commands: - output = await run_task(target.strip(), cmd.strip()) + output = await run_remote_task(target.strip(), cmd.strip()) + # System-Output ebenfalls in die Historie packen sys_now = datetime.now().strftime("%d.%m.%Y %H:%M") chat_history.append({ "role": "user", @@ -202,6 +223,7 @@ async def main_chat_loop(): "timestamp": sys_now }) + # RAM-Schutz: Historie nicht ins Unendliche wachsen lassen if len(chat_history) > 20: chat_history = chat_history[-20:] @@ -209,4 +231,4 @@ if __name__ == "__main__": try: asyncio.run(main_chat_loop()) except KeyboardInterrupt: - print("\nJ.A.R.V.I.S. hart beendet.") \ No newline at end of file + print(f"\n{Color.ERROR}J.A.R.V.I.S. hart beendet.{Color.RESET}") \ No newline at end of file