feat: add local chat provider and CLI

This commit is contained in:
2026-07-30 17:00:37 +02:00
parent 10d7cd7662
commit ec9b2f94ad
7 changed files with 704 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
"""Ollama implementation of the local model provider contract."""
from __future__ import annotations
import json
import socket
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from javis.providers.base import (
ChatMessage,
InvalidProviderResponseError,
ModelNotInstalledError,
ProviderTimeoutError,
ProviderUnavailableError,
ResponseAbortedError,
)
class OllamaProvider:
name = "ollama"
def __init__(self, model: str, base_url: str, timeout_seconds: float) -> None:
self.model = model
self._endpoint = f"{base_url.rstrip('/')}/api/chat"
self._timeout_seconds = timeout_seconds
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()