Files
Jarvis-Ai/src/javis/providers/ollama.py
T

110 lines
3.9 KiB
Python

"""Ollama implementation of the local model provider contract."""
from __future__ import annotations
import json
import socket
from dataclasses import dataclass
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from javis.providers.base import (
ChatMessage,
InvalidProviderResponseError,
ModelNotInstalledError,
ProviderTimeoutError,
ProviderUnavailableError,
ResponseAbortedError,
)
@dataclass(frozen=True, slots=True)
class OllamaStatus:
reachable: bool
model_available: bool
class OllamaProvider:
name = "ollama"
def __init__(self, model: str, base_url: str, timeout_seconds: float) -> None:
self.model = model
self._base_url = base_url.rstrip("/")
self._endpoint = f"{self._base_url}/api/chat"
self._timeout_seconds = timeout_seconds
def probe(self) -> OllamaStatus:
request = Request(f"{self._base_url}/api/tags", method="GET")
try:
with urlopen(request, timeout=min(self._timeout_seconds, 3)) as response:
result = json.loads(response.read())
except (HTTPError, URLError, TimeoutError, OSError, json.JSONDecodeError):
return OllamaStatus(False, False)
models = result.get("models")
if not isinstance(models, list):
return OllamaStatus(True, False)
names = {
value
for item in models
if isinstance(item, dict)
for value in (item.get("name"), item.get("model"))
if isinstance(value, str)
}
return OllamaStatus(True, self.model in names)
def chat(self, messages: list[ChatMessage]) -> str:
payload = {
"model": self.model,
"messages": [
{"role": message.role, "content": message.content} for message in messages
],
"stream": False,
"think": False,
}
request = Request(
self._endpoint,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=self._timeout_seconds) as response:
raw_response = response.read()
except HTTPError as exc:
details = exc.read().decode("utf-8", errors="replace")
if exc.code == 404 or "not found" in details.lower():
raise ModelNotInstalledError(
f"Das lokale Modell '{self.model}' ist nicht installiert."
) from exc
raise ProviderUnavailableError(f"Ollama meldet HTTP-Fehler {exc.code}.") from exc
except TimeoutError as exc:
raise ProviderTimeoutError(
"Die Modellantwort hat das Zeitlimit überschritten."
) from exc
except URLError as exc:
if isinstance(exc.reason, (TimeoutError, socket.timeout)):
raise ProviderTimeoutError(
"Die Modellantwort hat das Zeitlimit überschritten."
) from exc
raise ProviderUnavailableError(
"Ollama ist unter der konfigurierten lokalen Adresse nicht erreichbar."
) from exc
try:
result = json.loads(raw_response)
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
raise InvalidProviderResponseError(
"Ollama hat keine gültige JSON-Antwort geliefert."
) from exc
if result.get("done") is False:
raise ResponseAbortedError("Die Modellantwort wurde vorzeitig abgebrochen.")
content = result.get("message", {}).get("content")
if not isinstance(content, str) or not content.strip():
raise InvalidProviderResponseError(
"Ollama hat keine verwendbare Textantwort geliefert."
)
return content.strip()