ollama.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. # -*- coding: utf-8 -*-
  2. """
  3. Ollama Backend für NLP.
  4. Verwendet Ollama für lokale LLM-Inferenz.
  5. Ollama verwaltet Modelle und bietet eine REST-API.
  6. """
  7. import asyncio
  8. import json
  9. from typing import Any
  10. from trixy_core.nlp import NLPConfig, NLPState
  11. from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
  12. from plugins.nlp_llm.provider import LLMNLPProvider
  13. class OllamaNLPBackend(LLMNLPProvider):
  14. """
  15. Ollama Backend.
  16. Kommuniziert mit dem Ollama-Server über die REST-API.
  17. Ollama muss separat installiert und gestartet sein.
  18. """
  19. DEFAULT_HOST = "http://localhost:11434"
  20. def __init__(self) -> None:
  21. super().__init__()
  22. self._host = self.DEFAULT_HOST
  23. self._model_name = ""
  24. self._session = None
  25. async def _initialize_backend(self, config: NLPConfig) -> bool:
  26. """
  27. Initialisiert das Ollama Backend.
  28. Prüft Verbindung zum Ollama-Server und ob das Modell verfügbar ist.
  29. Args:
  30. config: NLP-Konfiguration
  31. Returns:
  32. True bei Erfolg
  33. """
  34. self._host = config.extra.get("ollama_host", self.DEFAULT_HOST)
  35. self._model_name = config.model_name
  36. # HTTP-Client erstellen
  37. try:
  38. import aiohttp
  39. self._session = aiohttp.ClientSession()
  40. except ImportError:
  41. perror("aiohttp nicht installiert. Installieren mit: pip install aiohttp")
  42. return False
  43. # Verbindung testen
  44. if not await self._check_connection():
  45. perror(f"Keine Verbindung zum Ollama-Server: {self._host}")
  46. pwarn("Stellen Sie sicher, dass Ollama läuft: ollama serve")
  47. return False
  48. # Prüfen ob Modell verfügbar
  49. if not await self._check_model():
  50. pwarn(f"Modell '{self._model_name}' nicht gefunden, versuche Pull...")
  51. if not await self._pull_model():
  52. perror(f"Modell konnte nicht geladen werden: {self._model_name}")
  53. return False
  54. pinfo(f"Ollama Backend initialisiert: {self._model_name}")
  55. return True
  56. async def _check_connection(self) -> bool:
  57. """Prüft Verbindung zum Ollama-Server."""
  58. try:
  59. async with self._session.get(f"{self._host}/api/tags", timeout=5) as resp:
  60. return resp.status == 200
  61. except Exception:
  62. return False
  63. async def _check_model(self) -> bool:
  64. """Prüft ob das Modell verfügbar ist."""
  65. try:
  66. async with self._session.get(f"{self._host}/api/tags") as resp:
  67. if resp.status != 200:
  68. return False
  69. data = await resp.json()
  70. models = data.get("models", [])
  71. for model in models:
  72. name = model.get("name", "")
  73. # Prüfe exakten Namen oder ohne Tag
  74. if name == self._model_name or name.split(":")[0] == self._model_name:
  75. return True
  76. return False
  77. except Exception as e:
  78. pdebug(f"Fehler beim Prüfen des Modells: {e}")
  79. return False
  80. async def _pull_model(self) -> bool:
  81. """Lädt das Modell von Ollama herunter."""
  82. try:
  83. pinfo(f"Lade Modell: {self._model_name} (dies kann einige Zeit dauern)")
  84. async with self._session.post(
  85. f"{self._host}/api/pull",
  86. json={"name": self._model_name, "stream": False},
  87. timeout=None, # Kein Timeout für Download
  88. ) as resp:
  89. if resp.status == 200:
  90. pinfo(f"Modell geladen: {self._model_name}")
  91. return True
  92. error = await resp.text()
  93. perror(f"Fehler beim Laden des Modells: {error}")
  94. return False
  95. except Exception as e:
  96. perror(f"Fehler beim Laden des Modells: {e}")
  97. return False
  98. async def _generate(self, messages: list[dict[str, str]]) -> str:
  99. """
  100. Generiert eine Antwort über die Ollama API.
  101. Args:
  102. messages: Nachrichtenliste
  103. Returns:
  104. Generierte Antwort
  105. """
  106. if self._session is None:
  107. raise RuntimeError("Ollama-Session nicht initialisiert")
  108. try:
  109. payload = {
  110. "model": self._model_name,
  111. "messages": messages,
  112. "stream": False,
  113. "options": {
  114. "temperature": self._config.temperature if self._config else 0.1,
  115. "num_predict": self._config.max_tokens if self._config else 256,
  116. "num_ctx": self._config.context_window if self._config else 2048,
  117. "num_thread": self._config.num_threads if self._config else 4,
  118. },
  119. }
  120. async with self._session.post(
  121. f"{self._host}/api/chat",
  122. json=payload,
  123. timeout=60,
  124. ) as resp:
  125. if resp.status != 200:
  126. error = await resp.text()
  127. raise RuntimeError(f"Ollama API Fehler: {error}")
  128. data = await resp.json()
  129. message = data.get("message", {})
  130. return message.get("content", "").strip()
  131. except asyncio.TimeoutError:
  132. raise RuntimeError("Ollama Timeout - Anfrage dauerte zu lange")
  133. except Exception as e:
  134. raise RuntimeError(f"Ollama Fehler: {e}")
  135. async def _shutdown_backend(self) -> None:
  136. """Schließt die HTTP-Session."""
  137. if self._session is not None:
  138. await self._session.close()
  139. self._session = None
  140. pdebug("Ollama-Session geschlossen")
  141. async def health_check(self) -> bool:
  142. """Prüft ob Ollama erreichbar ist."""
  143. return await self._check_connection() and await self._check_model()
  144. def supports_streaming(self) -> bool:
  145. """Ollama unterstützt Streaming."""
  146. return True
  147. async def generate_stream(self, messages: list[dict[str, str]]):
  148. """
  149. Generiert eine Antwort mit Streaming.
  150. Args:
  151. messages: Nachrichtenliste
  152. Yields:
  153. Text-Chunks
  154. """
  155. if self._session is None:
  156. raise RuntimeError("Ollama-Session nicht initialisiert")
  157. payload = {
  158. "model": self._model_name,
  159. "messages": messages,
  160. "stream": True,
  161. "options": {
  162. "temperature": self._config.temperature if self._config else 0.1,
  163. "num_predict": self._config.max_tokens if self._config else 256,
  164. },
  165. }
  166. async with self._session.post(
  167. f"{self._host}/api/chat",
  168. json=payload,
  169. ) as resp:
  170. async for line in resp.content:
  171. if line:
  172. try:
  173. data = json.loads(line)
  174. content = data.get("message", {}).get("content", "")
  175. if content:
  176. yield content
  177. except json.JSONDecodeError:
  178. continue