llama_cpp.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. # -*- coding: utf-8 -*-
  2. """
  3. llama-cpp-python Backend für NLP.
  4. Verwendet llama-cpp-python für lokale LLM-Inferenz.
  5. Optimiert für Raspberry Pi 5 mit 4GB RAM.
  6. """
  7. from pathlib import Path
  8. from typing import Any
  9. from trixy_core.nlp import NLPConfig, NLPState
  10. from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
  11. from plugins.nlp_llm.provider import LLMNLPProvider
  12. class LlamaCppNLPBackend(LLMNLPProvider):
  13. """
  14. llama-cpp-python Backend.
  15. Lädt und verwaltet lokale GGUF-Modelle.
  16. Unterstützt CPU und GPU (CUDA/Metal) Inferenz.
  17. """
  18. # Standard-Modellpfade
  19. DEFAULT_MODEL_PATHS = [
  20. Path("models/nlp"),
  21. Path("~/.local/share/trixy/models").expanduser(),
  22. Path("/usr/share/trixy/models"),
  23. ]
  24. def __init__(self) -> None:
  25. super().__init__()
  26. self._llm = None
  27. async def _initialize_backend(self, config: NLPConfig) -> bool:
  28. """
  29. Initialisiert das llama-cpp-python Backend.
  30. Args:
  31. config: NLP-Konfiguration
  32. Returns:
  33. True bei Erfolg
  34. """
  35. try:
  36. from llama_cpp import Llama
  37. except ImportError:
  38. perror("llama-cpp-python nicht installiert. Installieren mit: pip install llama-cpp-python")
  39. return False
  40. # Modellpfad ermitteln
  41. model_path = self._find_model_path(config)
  42. if model_path is None:
  43. perror(f"Modell nicht gefunden: {config.model_name}")
  44. pwarn("Verfügbare Modelle können mit 'trixy model list' angezeigt werden")
  45. return False
  46. pinfo(f"Lade Modell: {model_path}")
  47. try:
  48. # LLM laden mit optimierten Einstellungen für Pi 5
  49. self._llm = Llama(
  50. model_path=str(model_path),
  51. n_ctx=config.context_window,
  52. n_threads=config.num_threads,
  53. n_gpu_layers=-1 if config.use_gpu else 0,
  54. verbose=False,
  55. # Speicher-Optimierungen für Pi 5
  56. use_mmap=True,
  57. use_mlock=False,
  58. )
  59. pinfo(f"Modell geladen: {config.model_name}")
  60. return True
  61. except Exception as e:
  62. perror(f"Fehler beim Laden des Modells: {e}")
  63. return False
  64. def _find_model_path(self, config: NLPConfig) -> Path | None:
  65. """
  66. Findet den Pfad zum Modell.
  67. Args:
  68. config: NLP-Konfiguration
  69. Returns:
  70. Modellpfad oder None
  71. """
  72. # Expliziter Pfad in Konfiguration
  73. if config.model_path:
  74. path = Path(config.model_path)
  75. if path.exists():
  76. return path
  77. # Versuche mit .gguf Endung
  78. if not path.suffix:
  79. for suffix in [".gguf", ".bin"]:
  80. with_suffix = path.with_suffix(suffix)
  81. if with_suffix.exists():
  82. return with_suffix
  83. # Suche in Standard-Pfaden
  84. model_name = config.model_name
  85. possible_names = [
  86. model_name,
  87. f"{model_name}.gguf",
  88. f"{model_name}.Q4_K_M.gguf",
  89. f"{model_name}.Q5_K_M.gguf",
  90. f"{model_name}.Q8_0.gguf",
  91. ]
  92. for base_path in self.DEFAULT_MODEL_PATHS:
  93. if not base_path.exists():
  94. continue
  95. for name in possible_names:
  96. full_path = base_path / name
  97. if full_path.exists():
  98. return full_path
  99. # Suche in Unterverzeichnissen
  100. for subdir in base_path.iterdir():
  101. if subdir.is_dir():
  102. sub_path = subdir / name
  103. if sub_path.exists():
  104. return sub_path
  105. return None
  106. async def _generate(self, messages: list[dict[str, str]]) -> str:
  107. """
  108. Generiert eine Antwort vom Modell.
  109. Args:
  110. messages: Nachrichtenliste
  111. Returns:
  112. Generierte Antwort
  113. """
  114. if self._llm is None:
  115. raise RuntimeError("Modell nicht geladen")
  116. # Chat-Completion verwenden
  117. response = self._llm.create_chat_completion(
  118. messages=messages,
  119. temperature=self._config.temperature if self._config else 0.1,
  120. max_tokens=self._config.max_tokens if self._config else 256,
  121. stop=["</s>", "<|end|>", "<|eot_id|>"],
  122. )
  123. # Antwort extrahieren
  124. if response and "choices" in response and len(response["choices"]) > 0:
  125. content = response["choices"][0].get("message", {}).get("content", "")
  126. return content.strip()
  127. return ""
  128. async def _shutdown_backend(self) -> None:
  129. """Gibt Modell-Ressourcen frei."""
  130. if self._llm is not None:
  131. # llama-cpp-python hat keinen expliziten close()
  132. # Setze Referenz auf None für Garbage Collection
  133. self._llm = None
  134. pdebug("llama-cpp Modell entladen")
  135. async def health_check(self) -> bool:
  136. """Prüft ob das Modell funktionsfähig ist."""
  137. if self._llm is None:
  138. return False
  139. try:
  140. # Kurzer Test-Prompt
  141. response = self._llm.create_chat_completion(
  142. messages=[{"role": "user", "content": "Hi"}],
  143. max_tokens=5,
  144. )
  145. return response is not None
  146. except Exception:
  147. return False