| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- # -*- coding: utf-8 -*-
- """
- PluginManager für das dynamische Laden und Verwalten von Plugins.
- """
- import asyncio
- import importlib.util
- import json
- import sys
- from pathlib import Path
- from typing import Any, Awaitable, Callable, Type, TYPE_CHECKING
- from trixy_core.plugins.trixy_plugin import TrixyPlugin
- from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
- from trixy_core.utils.logging.file_router import log_as
- if TYPE_CHECKING:
- from trixy_core.application import IApplication
- # Lazy import für Intent-System (verhindert zirkuläre Imports)
- _intent_registry = None
- _discover_handlers = None
- def _get_intent_registry():
- """Gibt die IntentRegistry-Instanz zurück (lazy loading)."""
- global _intent_registry
- if _intent_registry is None:
- from trixy_core.nlp.intent_registry import IntentRegistry
- _intent_registry = IntentRegistry.get_instance()
- return _intent_registry
- def _get_discover_handlers():
- """Gibt die discover_intent_handlers Funktion zurück (lazy loading)."""
- global _discover_handlers
- if _discover_handlers is None:
- from trixy_core.nlp.decorators import discover_intent_handlers
- _discover_handlers = discover_intent_handlers
- return _discover_handlers
- class PluginManager:
- """
- Verwaltet das Laden, Aktivieren und Deaktivieren von Plugins.
- Plugins werden aus dem konfigurierten Verzeichnis geladen.
- Jedes Plugin muss in einem eigenen Unterverzeichnis liegen
- mit einer main.py und optionaler config.json.
- """
- DEFAULT_DIRECTORY = "plugins"
- def __init__(
- self,
- application: "IApplication",
- directory: str | Path = DEFAULT_DIRECTORY,
- timeout_seconds: float = 30.0,
- max_failures: int = 3,
- ) -> None:
- """
- Initialisiert den PluginManager.
- Args:
- application: Referenz zur Hauptanwendung
- directory: Plugin-Verzeichnis
- timeout_seconds: Timeout fuer Plugin-Aufrufe in Sekunden
- max_failures: Maximale Fehlschlaege bevor Plugin deaktiviert wird
- """
- self._application = application
- self._directory = Path(directory)
- self._plugins: dict[str, TrixyPlugin] = {}
- self._failed_plugins: dict[str, str] = {} # Name -> Fehler
- self._timeout_seconds = timeout_seconds
- self._max_failures = max_failures
- self._failure_counts: dict[str, int] = {}
- @property
- def count(self) -> int:
- """Anzahl geladener Plugins."""
- return len(self._plugins)
- @property
- def enabled_count(self) -> int:
- """Anzahl aktivierter Plugins."""
- return sum(1 for p in self._plugins.values() if p.is_enabled())
- def get(self, name: str) -> TrixyPlugin | None:
- """
- Gibt ein Plugin nach Namen zurück.
- Args:
- name: Plugin-Name
- Returns:
- Plugin oder None
- """
- return self._plugins.get(name)
- def get_all(self) -> list[TrixyPlugin]:
- """Gibt alle geladenen Plugins zurück."""
- return list(self._plugins.values())
- def get_enabled(self) -> list[TrixyPlugin]:
- """Gibt alle aktivierten Plugins zurück."""
- return [p for p in self._plugins.values() if p.is_enabled()]
- def get_failed(self) -> dict[str, str]:
- """Gibt fehlgeschlagene Plugins mit Fehlern zurück."""
- return self._failed_plugins.copy()
- async def _call_plugin(
- self,
- plugin: TrixyPlugin,
- coro_factory: Callable[[], Awaitable[Any]],
- operation: str,
- ) -> bool:
- """
- Ruft eine Plugin-Coroutine mit ContextVar-Routing und Timeout-Schutz auf.
- Args:
- plugin: Plugin-Instanz
- coro_factory: Factory-Funktion die die Coroutine erzeugt
- operation: Name der Operation (fuer Logging)
- Returns:
- True bei Erfolg, False bei Fehler
- """
- with log_as(f"plugin.{plugin.name}"):
- try:
- await asyncio.wait_for(
- coro_factory(),
- timeout=self._timeout_seconds,
- )
- # Erfolg: Fehlerzaehler zuruecksetzen
- self._failure_counts.pop(plugin.name, None)
- return True
- except asyncio.TimeoutError:
- perror(
- f"Plugin '{plugin.name}' Timeout bei '{operation}' "
- f"(>{self._timeout_seconds}s)"
- )
- self._handle_plugin_failure(plugin.name, operation)
- return False
- except Exception as e:
- perror(f"Plugin '{plugin.name}' Fehler bei '{operation}': {e}")
- self._handle_plugin_failure(plugin.name, operation)
- return False
- def _handle_plugin_failure(self, plugin_name: str, operation: str) -> None:
- """
- Behandelt Plugin-Fehlschlaege und deaktiviert nach max_failures.
- Args:
- plugin_name: Name des fehlgeschlagenen Plugins
- operation: Name der fehlgeschlagenen Operation
- """
- count = self._failure_counts.get(plugin_name, 0) + 1
- self._failure_counts[plugin_name] = count
- if count >= self._max_failures:
- plugin = self._plugins.get(plugin_name)
- if plugin and plugin.is_enabled():
- pwarn(
- f"Plugin '{plugin_name}' nach {count} Fehlschlaegen "
- f"automatisch deaktiviert (letzte: '{operation}')"
- )
- plugin.enabled = False
- async def load_all(self) -> int:
- """
- Lädt alle Plugins aus dem Plugin-Verzeichnis.
- Returns:
- Anzahl erfolgreich geladener Plugins
- """
- if not self._directory.exists():
- self._directory.mkdir(parents=True, exist_ok=True)
- pinfo(f"Plugin-Verzeichnis erstellt: {self._directory}")
- return 0
- loaded = 0
- for plugin_dir in self._directory.iterdir():
- if not plugin_dir.is_dir():
- continue
- if plugin_dir.name.startswith("_") or plugin_dir.name.startswith("."):
- continue
- if await self.load(plugin_dir.name):
- loaded += 1
- pinfo(f"{loaded} Plugins geladen")
- return loaded
- async def load(self, name: str) -> bool:
- """
- Lädt ein einzelnes Plugin.
- Args:
- name: Name des Plugin-Verzeichnisses
- Returns:
- True bei Erfolg
- """
- plugin_dir = self._directory / name
- main_file = plugin_dir / "main.py"
- config_file = plugin_dir / "config.json"
- if not plugin_dir.exists():
- perror(f"Plugin-Verzeichnis nicht gefunden: {plugin_dir}")
- self._failed_plugins[name] = "Verzeichnis nicht gefunden"
- return False
- if not main_file.exists():
- perror(f"main.py nicht gefunden in: {plugin_dir}")
- self._failed_plugins[name] = "main.py nicht gefunden"
- return False
- try:
- # Konfiguration laden
- config = {}
- if config_file.exists():
- with open(config_file, "r", encoding="utf-8") as f:
- config = json.load(f)
- # Plugin-Modul laden
- spec = importlib.util.spec_from_file_location(
- f"trixy_plugins.{name}",
- main_file
- )
- if spec is None or spec.loader is None:
- raise ImportError(f"Kann Plugin-Spec nicht erstellen: {name}")
- module = importlib.util.module_from_spec(spec)
- sys.modules[spec.name] = module
- spec.loader.exec_module(module)
- # Plugin-Klasse finden
- plugin_class = self._find_plugin_class(module)
- if plugin_class is None:
- raise ImportError(f"Keine TrixyPlugin-Unterklasse gefunden in: {name}")
- # Prüfen ob Plugin aktiviert ist
- is_enabled = config.get("enabled", True)
- # Plugin instanziieren
- plugin = plugin_class(
- application=self._application,
- plugin_path=plugin_dir,
- config=config
- )
- # enabled-Status setzen
- plugin.enabled = is_enabled
- # Nur on_load und on_enable aufrufen wenn Plugin aktiviert ist
- if is_enabled:
- if not await self._call_plugin(plugin, plugin.on_load, "on_load"):
- self._failed_plugins[name] = "on_load fehlgeschlagen"
- return False
- plugin._loaded = True
- # Event-Handler registrieren
- if hasattr(self._application, "events"):
- self._application.events.register_object(plugin)
- # Intent-Handler registrieren
- with log_as(f"plugin.{plugin.name}"):
- self._register_intent_handlers(plugin)
- # on_enable aufrufen
- await self._call_plugin(plugin, plugin.on_enable, "on_enable")
- else:
- plugin._loaded = False
- self._plugins[plugin.name] = plugin
- self._failed_plugins.pop(name, None)
- if is_enabled:
- pinfo(f"Plugin geladen und aktiviert: {plugin.name} v{plugin.VERSION}")
- # plugin_loaded Event emittieren (damit NLP-Plugin Keyword-Matcher aktualisiert)
- if hasattr(self._application, "events"):
- from trixy_core.events.event_data.basic import PluginLoaded
- await self._application.events.trigger(
- "plugin_loaded",
- PluginLoaded(
- plugin_name=plugin.name,
- plugin_version=plugin.VERSION,
- ),
- )
- else:
- pdebug(f"Plugin registriert (deaktiviert): {plugin.name}")
- return True
- except Exception as e:
- error_msg = str(e)
- perror(f"Fehler beim Laden von Plugin '{name}': {error_msg}")
- self._failed_plugins[name] = error_msg
- return False
- def _find_plugin_class(self, module: object) -> Type[TrixyPlugin] | None:
- """Findet die Plugin-Klasse in einem Modul."""
- for attr_name in dir(module):
- if attr_name.startswith("_"):
- continue
- attr = getattr(module, attr_name)
- if (
- isinstance(attr, type) and
- issubclass(attr, TrixyPlugin) and
- attr is not TrixyPlugin
- ):
- return attr
- return None
- def _get_language(self) -> str:
- """Ermittelt die konfigurierte Sprache."""
- if self._application and hasattr(self._application, "config_manager"):
- try:
- lang = self._application.config_manager.get("language")
- if lang:
- return lang
- except Exception:
- pass
- return "de"
- def _register_intent_handlers(self, plugin: TrixyPlugin) -> int:
- """
- Registriert alle Intent-Handler eines Plugins.
- Durchsucht das Plugin nach @intent dekorierten Methoden
- und registriert diese in der IntentRegistry. Patterns und
- Examples werden bevorzugt aus YAML-Dateien geladen
- (intents_{sprache}.yml im Plugin-Verzeichnis).
- Args:
- plugin: Plugin-Instanz
- Returns:
- Anzahl registrierter Intents
- """
- try:
- discover_handlers = _get_discover_handlers()
- registry = _get_intent_registry()
- # YAML-Daten fuer Plugin laden (sprachspezifisch)
- yaml_data: dict = {}
- try:
- from trixy_core.nlp.intent_loader import load_intent_yaml, merge_yaml_into_metadata
- language = self._get_language()
- yaml_data = load_intent_yaml(plugin.plugin_path, language)
- except Exception as e:
- pdebug(f"YAML-Intent-Laden optional fehlgeschlagen fuer '{plugin.name}': {e}")
- handlers = discover_handlers(plugin)
- count = 0
- for intent_name, handler, metadata in handlers:
- from trixy_core.nlp.intent_registry import IntentDefinition, IntentSlot
- # YAML-Daten mergen (haben Vorrang vor Dekoratoren)
- if yaml_data:
- from trixy_core.nlp.intent_loader import merge_yaml_into_metadata
- merge_yaml_into_metadata(metadata, yaml_data)
- # Intent-Definition erstellen
- intent_def = IntentDefinition(
- name=intent_name,
- handler=handler,
- plugin_name=plugin.name,
- slots=metadata.get("slots", {}),
- examples=metadata.get("examples", []),
- description=metadata.get("description", ""),
- priority=metadata.get("priority", 0),
- enabled=True,
- patterns=metadata.get("patterns", []),
- )
- if registry.register(intent_def):
- count += 1
- pdebug(f"Intent registriert: {intent_name} (Plugin: {plugin.name})")
- if count > 0:
- pinfo(f"{count} Intent(s) registriert für Plugin: {plugin.name}")
- return count
- except Exception as e:
- perror(f"Fehler beim Registrieren von Intent-Handlern für '{plugin.name}': {e}")
- return 0
- def _unregister_intent_handlers(self, plugin: TrixyPlugin) -> int:
- """
- Entfernt alle Intent-Handler eines Plugins.
- Args:
- plugin: Plugin-Instanz
- Returns:
- Anzahl entfernter Intents
- """
- try:
- registry = _get_intent_registry()
- count = registry.unregister_plugin(plugin.name)
- if count > 0:
- pdebug(f"{count} Intent(s) entfernt für Plugin: {plugin.name}")
- return count
- except Exception as e:
- perror(f"Fehler beim Entfernen von Intent-Handlern für '{plugin.name}': {e}")
- return 0
- async def unload(self, name: str) -> bool:
- """
- Entlädt ein Plugin.
- Args:
- name: Plugin-Name
- Returns:
- True bei Erfolg
- """
- plugin = self._plugins.get(name)
- if plugin is None:
- pwarn(f"Plugin nicht geladen: {name}")
- return False
- try:
- # Intent-Handler entfernen
- with log_as(f"plugin.{plugin.name}"):
- self._unregister_intent_handlers(plugin)
- # Event-Handler entfernen
- if hasattr(self._application, "events"):
- self._application.events.unregister_object(plugin)
- # on_unload aufrufen
- await self._call_plugin(plugin, plugin.on_unload, "on_unload")
- del self._plugins[name]
- # Modul aus sys.modules entfernen
- module_name = f"trixy_plugins.{name}"
- if module_name in sys.modules:
- del sys.modules[module_name]
- pinfo(f"Plugin entladen: {name}")
- # plugin_unloaded Event emittieren
- if hasattr(self._application, "events"):
- from trixy_core.events.event_data.basic import PluginUnloaded
- await self._application.events.trigger(
- "plugin_unloaded",
- PluginUnloaded(plugin_name=name),
- )
- return True
- except Exception as e:
- perror(f"Fehler beim Entladen von Plugin '{name}': {e}")
- return False
- async def reload(self, name: str) -> bool:
- """
- Lädt ein Plugin neu.
- Args:
- name: Plugin-Name
- Returns:
- True bei Erfolg
- """
- if name in self._plugins:
- await self.unload(name)
- return await self.load(name)
- async def enable(self, name: str) -> bool:
- """
- Aktiviert ein Plugin.
- Args:
- name: Plugin-Name
- Returns:
- True bei Erfolg
- """
- plugin = self._plugins.get(name)
- if plugin is None:
- pwarn(f"Plugin nicht geladen: {name}")
- return False
- if plugin.is_enabled():
- return True
- try:
- plugin.enabled = True
- # on_load aufrufen falls noch nicht geschehen
- # (bei Plugins die beim Start deaktiviert waren)
- if not hasattr(plugin, "_loaded") or not plugin._loaded:
- if not await self._call_plugin(plugin, plugin.on_load, "on_load"):
- plugin.enabled = False
- return False
- plugin._loaded = True
- # Event-Handler registrieren
- if hasattr(self._application, "events"):
- self._application.events.register_object(plugin)
- # Intent-Handler registrieren
- with log_as(f"plugin.{plugin.name}"):
- self._register_intent_handlers(plugin)
- await self._call_plugin(plugin, plugin.on_enable, "on_enable")
- pinfo(f"Plugin aktiviert: {name}")
- return True
- except Exception as e:
- perror(f"Fehler beim Aktivieren von Plugin '{name}': {e}")
- plugin.enabled = False
- return False
- async def disable(self, name: str) -> bool:
- """
- Deaktiviert ein Plugin.
- Args:
- name: Plugin-Name
- Returns:
- True bei Erfolg
- """
- plugin = self._plugins.get(name)
- if plugin is None:
- pwarn(f"Plugin nicht geladen: {name}")
- return False
- if not plugin.is_enabled():
- return True
- try:
- await self._call_plugin(plugin, plugin.on_disable, "on_disable")
- plugin.enabled = False
- pinfo(f"Plugin deaktiviert: {name}")
- return True
- except Exception as e:
- perror(f"Fehler beim Deaktivieren von Plugin '{name}': {e}")
- return False
- async def unload_all(self) -> int:
- """
- Entlädt alle Plugins.
- Returns:
- Anzahl entladener Plugins
- """
- count = 0
- for name in list(self._plugins.keys()):
- if await self.unload(name):
- count += 1
- return count
- def __contains__(self, name: str) -> bool:
- """Prüft, ob ein Plugin geladen ist."""
- return name in self._plugins
- def __getitem__(self, name: str) -> TrixyPlugin:
- """Gibt ein Plugin nach Namen zurück."""
- if name not in self._plugins:
- raise KeyError(f"Plugin nicht gefunden: {name}")
- return self._plugins[name]
- def __iter__(self):
- """Iterator über alle Plugins."""
- return iter(self._plugins.values())
- def __len__(self) -> int:
- """Anzahl geladener Plugins."""
- return len(self._plugins)
|