# code_executor.py import os import sys import uuid import subprocess import tempfile import re from typing import Dict, List, Optional, Tuple, Any import importlib.util import logging # Logging einrichten logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) class CodeExecutor: """ Führt generierten Code in einer isolierten virtuellen Umgebung aus, während Zugriff auf spezifische App-Module gewährt wird und automatisch erforderliche Pakete installiert werden. """ def __init__(self, app_modules: List[str] = None, venv_path: Optional[str] = None, timeout: int = 30, max_memory_mb: int = 512, allowed_packages: List[str] = None, blocked_packages: List[str] = None): """ Initialisiert den CodeExecutor. Args: app_modules: Liste von Modulnamen, die dem generierten Code zur Verfügung stehen sollen venv_path: Pfad zur virtuellen Umgebung. Falls None, wird eine temporäre erstellt timeout: Maximale Ausführungszeit in Sekunden max_memory_mb: Maximaler Arbeitsspeicher in MB allowed_packages: Liste erlaubter Pakete (wenn None, werden alle erlaubt, außer blockierte) blocked_packages: Liste blockierter Pakete (z.B. gefährliche oder ressourcenintensive) """ self.app_modules = app_modules or [] self.venv_path = venv_path self.timeout = timeout self.max_memory_mb = max_memory_mb self.temp_dir = None self.allowed_packages = allowed_packages self.blocked_packages = blocked_packages or ["cryptography", "flask", "django", "tornado", "requests"] def _create_venv(self) -> str: """Erstellt eine virtuelle Umgebung und gibt den Pfad zurück.""" if self.venv_path and os.path.exists(self.venv_path): return self.venv_path # Temporäres Verzeichnis für die virtuelle Umgebung erstellen self.temp_dir = tempfile.mkdtemp(prefix="ai_code_exec_") venv_path = os.path.join(self.temp_dir, "venv") try: # Virtuelle Umgebung erstellen logger.info(f"Erstelle virtuelle Umgebung in {venv_path}") subprocess.run([sys.executable, "-m", "venv", venv_path], check=True, capture_output=True) return venv_path except subprocess.CalledProcessError as e: logger.error(f"Fehler beim Erstellen der virtuellen Umgebung: {e}") raise RuntimeError(f"Konnte venv nicht erstellen: {e}") def _get_pip_executable(self, venv_path: str) -> str: """Ermittelt den Pfad zum pip-Executable in der virtuellen Umgebung.""" if os.name == 'nt': # Windows return os.path.join(venv_path, "Scripts", "pip.exe") else: # Unix/Linux return os.path.join(venv_path, "bin", "pip") def _get_python_executable(self, venv_path: str) -> str: """Ermittelt den Pfad zum Python-Executable in der virtuellen Umgebung.""" if os.name == 'nt': # Windows return os.path.join(venv_path, "Scripts", "python.exe") else: # Unix/Linux return os.path.join(venv_path, "bin", "python") def _install_packages(self, packages: List[str], venv_path: str) -> Tuple[bool, str]: """ Installiert Pakete in der virtuellen Umgebung. Args: packages: Liste der zu installierenden Pakete venv_path: Pfad zur virtuellen Umgebung Returns: Tuple aus (Erfolg, Fehlermeldung) """ if not packages: return True, "" # Überprüfen, ob Pakete erlaubt sind blocked = [] for package in packages: # Paketname ohne Version extrahieren pkg_name = re.split('[=<>]', package)[0].strip() if self.blocked_packages and pkg_name.lower() in [p.lower() for p in self.blocked_packages]: blocked.append(pkg_name) if self.allowed_packages and pkg_name.lower() not in [p.lower() for p in self.allowed_packages]: blocked.append(pkg_name) if blocked: return False, f"Die folgenden Pakete sind nicht erlaubt: {', '.join(blocked)}" # Pakete installieren pip_executable = self._get_pip_executable(venv_path) logger.info(f"Installiere Pakete in virtueller Umgebung: {', '.join(packages)}") try: # pip aktualisieren subprocess.run( [pip_executable, "install", "--upgrade", "pip"], check=True, capture_output=True, timeout=60 ) # Pakete installieren process = subprocess.run( [pip_executable, "install"] + packages, check=True, capture_output=True, text=True, timeout=120 # 2 Minuten Timeout für Paketinstallation ) return True, process.stdout except subprocess.CalledProcessError as e: error_msg = f"Fehler bei der Paketinstallation: {e.stderr}" logger.error(error_msg) return False, error_msg except subprocess.TimeoutExpired: return False, "Zeitüberschreitung bei der Paketinstallation." except Exception as e: return False, f"Unerwarteter Fehler bei der Paketinstallation: {str(e)}" def _extract_required_packages(self, code: str) -> List[str]: """ Extrahiert benötigte Pakete aus dem Code durch Analyse von Import-Statements und Pip-Installationsanweisungen. Args: code: Der Python-Code Returns: Liste der erkannten Paketnamen """ packages = set() # Paketkommentare erkennen (# pip install package) pip_comments = re.findall(r'#\s*pip\s+install\s+([^#\n]+)', code) for comment in pip_comments: for pkg in comment.split(): if pkg and not pkg.startswith('-'): packages.add(pkg.strip()) # Import-Statements analysieren import_lines = re.findall(r'^(?:import|from)\s+([^\s.]+)(?:\s+import|\s*$|\.)', code, re.MULTILINE) # Standardmodule, die nicht installiert werden müssen std_modules = { 'os', 'sys', 'time', 'datetime', 'math', 're', 'random', 'json', 'collections', 'itertools', 'functools', 'pathlib', 'shutil', 'tempfile', 'uuid', 'subprocess', 'threading', 'logging', 'traceback', 'io', 'copy' } # Module der App, die nicht installiert werden müssen app_modules_prefixes = set(m.split('.')[0] for m in self.app_modules) for module in import_lines: if module not in std_modules and module not in app_modules_prefixes: packages.add(module) return list(packages) def _create_module_loader(self) -> str: """ Erstellt ein Hilfsskript, das App-Module in die venv importiert. Gibt den Pfad zum Hilfsskript zurück. """ if not self.app_modules: return "" # Temporäre Datei für den Module-Loader erstellen module_loader_path = os.path.join(self.temp_dir or tempfile.mkdtemp(prefix="ai_code_exec_"), "module_loader.py") # Pfad zu den App-Modulen bestimmen app_path = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) # Modul-Loader-Code generieren loader_code = f""" import sys import importlib.util import os # App-Pfad zum Suchpfad hinzufügen sys.path.insert(0, "{app_path}") # Module importieren modules = {{}} """ # Code zum Importieren der Module hinzufügen for module_name in self.app_modules: loader_code += f""" try: modules["{module_name}"] = __import__("{module_name}", fromlist=["*"]) print(f"Modul '{module_name}' erfolgreich importiert") except ImportError as e: print(f"Fehler beim Importieren von '{module_name}': {{e}}") """ # Loader-Datei schreiben with open(module_loader_path, "w") as f: f.write(loader_code) return module_loader_path def execute_code(self, code: str, input_data: Dict[str, Any] = None) -> Dict[str, Any]: """ Führt den generierten Code in einer isolierten Umgebung aus. Args: code: Der auszuführende Python-Code input_data: Eingabedaten für den Code (werden als JSON serialisiert) Returns: Dict mit Ausführungsergebnissen, Ausgabe und Fehlern """ # Virtuelle Umgebung erstellen oder bestehende verwenden venv_path = self._create_venv() # Erforderliche Pakete aus dem Code extrahieren required_packages = self._extract_required_packages(code) # Pakete installieren, falls erforderlich install_success = True install_log = "" if required_packages: install_success, install_log = self._install_packages(required_packages, venv_path) if not install_success: return { "success": False, "output": "", "error": f"Fehler bei der Installation der erforderlichen Pakete: {install_log}", "result": None, "installed_packages": required_packages } # Temporäre Datei für den Code erstellen code_id = str(uuid.uuid4())[:8] code_file_path = os.path.join(self.temp_dir or tempfile.mkdtemp(prefix="ai_code_exec_"), f"ai_code_{code_id}.py") # Module-Loader erstellen module_loader_path = self._create_module_loader() # Eingabedaten als JSON speichern, wenn vorhanden input_path = "" if input_data: import json input_path = os.path.join(self.temp_dir or tempfile.mkdtemp(prefix="ai_code_exec_"), f"input_{code_id}.json") with open(input_path, "w") as f: json.dump(input_data, f) # Outputpfad für Ergebnisse output_path = os.path.join(self.temp_dir or tempfile.mkdtemp(prefix="ai_code_exec_"), f"output_{code_id}.json") # Wrapper für den Code erstellen, damit die App-Module verfügbar sind wrapped_code = f""" import sys import json import traceback import os # Ergebnisstruktur result = {{ "success": False, "output": "", "error": "", "result": None, "installed_packages": {required_packages} }} try: # Module laden, falls erforderlich if "{module_loader_path}": module_loader = __import__("module_loader") globals().update({{k: v for k, v in module_loader.modules.items()}}) # Eingabedaten laden, falls vorhanden input_data = None if "{input_path}": with open("{input_path}", "r") as f: input_data = json.load(f) # Ausgabeumleitung from io import StringIO original_stdout = sys.stdout original_stderr = sys.stderr captured_stdout = StringIO() captured_stderr = StringIO() sys.stdout = captured_stdout sys.stderr = captured_stderr # Benutzercode ausführen try: # Den Code in einem lokalen Namespace ausführen local_vars = {{"input_data": input_data}} exec('''{code}''', globals(), local_vars) # Ergebnis speichern, falls eine Variable 'result' definiert wurde if "result" in local_vars: result["result"] = local_vars["result"] result["success"] = True except Exception as e: result["error"] = str(e) result["error"] += "\\n" + traceback.format_exc() finally: # Ausgabe erfassen result["output"] = captured_stdout.getvalue() result["error"] += captured_stderr.getvalue() # Ausgabeumleitung zurücksetzen sys.stdout = original_stdout sys.stderr = original_stderr except Exception as outer_e: result["error"] = f"Fehler beim Ausführen des Setups: {{outer_e}}\\n{{traceback.format_exc()}}" # Ergebnis speichern with open("{output_path}", "w") as f: json.dump(result, f, default=str) """ # Code in temporäre Datei schreiben with open(code_file_path, "w") as f: f.write(wrapped_code) # Python-Interpreter aus der virtuellen Umgebung bestimmen python_executable = self._get_python_executable(venv_path) # Code ausführen logger.info(f"Führe Code in virtueller Umgebung aus: {python_executable}") try: # Prozess mit Ressourcenbeschränkungen ausführen cmd = [python_executable, code_file_path] # Umgebungsvariablen setzen, um Speicherlimit zu erzwingen env = os.environ.copy() if self.max_memory_mb: if os.name == 'posix': # Unix/Linux # Auf Unix-Systemen können wir ulimit verwenden cmd = ["bash", "-c", f"ulimit -v {self.max_memory_mb * 1024} && {python_executable} {code_file_path}"] elif os.name == 'nt': # Windows # Auf Windows können wir keine harten Speichergrenzen setzen, aber Job Objects verwenden # Hier müsste eine komplexere Lösung implementiert werden pass # Prozess starten und mit Timeout ausführen process = subprocess.run( cmd, timeout=self.timeout, env=env, capture_output=True, text=True ) # Ergebnis aus der Ausgabedatei lesen if os.path.exists(output_path): with open(output_path, "r") as f: import json execution_result = json.load(f) else: execution_result = { "success": False, "output": process.stdout, "error": f"Keine Ergebnisdatei gefunden. Stderr: {process.stderr}", "result": None, "installed_packages": required_packages } except subprocess.TimeoutExpired: execution_result = { "success": False, "output": "", "error": f"Zeitüberschreitung bei der Ausführung (Timeout nach {self.timeout} Sekunden)", "result": None, "installed_packages": required_packages } except Exception as e: execution_result = { "success": False, "output": "", "error": f"Fehler bei der Ausführung: {str(e)}", "result": None, "installed_packages": required_packages } # Informationen zur Paketinstallation hinzufügen if install_log: execution_result["package_install_log"] = install_log # Temporäre Dateien aufräumen self._cleanup_temp_files([code_file_path, input_path, output_path]) return execution_result def _cleanup_temp_files(self, file_paths: List[str]): """Räumt temporäre Dateien auf.""" for path in file_paths: if path and os.path.exists(path): try: os.remove(path) except Exception as e: logger.warning(f"Konnte temporäre Datei nicht löschen {path}: {e}") def cleanup(self): """Räumt alle temporären Ressourcen auf.""" if self.temp_dir and os.path.exists(self.temp_dir): import shutil try: shutil.rmtree(self.temp_dir) logger.info(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}") except Exception as e: logger.warning(f"Konnte temporäres Verzeichnis nicht löschen {self.temp_dir}: {e}") def __del__(self): """Aufräumen beim Garbage Collection.""" self.cleanup() # Beispiel zur Verwendung des erweiterten CodeExecutor in einem AI Chat # from code_executor import CodeExecutor def execute_ai_generated_code(prompt_result: str, input_data=None): """ Führt von einer KI generierten Code aus und installiert automatisch benötigte Pakete Args: prompt_result: Der von der KI generierte Python-Code input_data: Optionale Eingabedaten für den Code Returns: Ergebnis der Code-Ausführung """ # Verfügbare App-Module definieren available_modules = [ "utils.sharepoint_crud", # Weitere Module hier hinzufügen ] # Liste erlaubter Pakete (optional) allowed_packages = None # None bedeutet alle erlaubt, außer blockierte # Liste blockierter Pakete (Sicherheitsrisiken oder ressourcenintensive Pakete) blocked_packages = [ "cryptography", "flask", "django", "tornado", # Sicherheit "tensorflow", "pytorch", "scikit-learn", # Ressourcenintensiv ] # CodeExecutor initialisieren executor = CodeExecutor( app_modules=available_modules, timeout=120, # 2 Minuten Timeout max_memory_mb=1024, # 1GB Speicherlimit allowed_packages=allowed_packages, blocked_packages=blocked_packages ) try: # Code ausführen result = executor.execute_code(prompt_result, input_data) if result["success"]: print("Code erfolgreich ausgeführt!") print(f"Ausgabe: {result['output']}") # Zeige installierte Pakete an if "installed_packages" in result and result["installed_packages"]: print(f"Installierte Pakete: {', '.join(result['installed_packages'])}") return result["result"] else: print(f"Fehler bei der Ausführung: {result['error']}") return None finally: # Aufräumen executor.cleanup() # Beispiel für die Verwendung if __name__ == "__main__": # Angenommen, dies ist der von der KI generierte Code mit Paketabhängigkeiten ai_generated_code = """ # pip install pandas matplotlib import pandas as pd import matplotlib.pyplot as plt import utils.sharepoint_crud as sp # Daten aus input_data verwenden file_path = input_data.get('file_path') site_url = input_data.get('site_url') # Beispieldaten erstellen data = pd.DataFrame({ 'Monat': ['Jan', 'Feb', 'Mär', 'Apr', 'Mai'], 'Umsatz': [1200, 1400, 1300, 1500, 1800] }) # Plot erstellen plt.figure(figsize=(10, 6)) plt.bar(data['Monat'], data['Umsatz']) plt.title('Umsatz nach Monat') plt.savefig('umsatz_plot.png') print('Diagramm erstellt und gespeichert') # SharePoint-Datei hochladen result = sp.upload_file(file_path, site_url) print(f"Datei wurde hochgeladen: {result}") # Ergebnis zurückgeben result = { 'data': data.to_dict(), 'plot_saved': True, 'upload_result': result } """ # Daten für den Code bereitstellen data = { "file_path": "/path/to/document.docx", "site_url": "https://example.sharepoint.com/sites/mysite" } # Code ausführen execute_ai_generated_code(ai_generated_code, data)