110 lines
3.9 KiB
Python
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()
|