- Add comprehensive Ollama connection error handling strategy - Implement OllamaClient with non-blocking initialization and connection checks - Create OllamaAvailabilityMonitor for periodic Ollama connection tracking - Update design and requirements to support graceful Ollama unavailability - Add new project structure for AI sidebar module with initial implementation - Enhance error handling to prevent application crashes when Ollama is not running - Prepare for future improvements in AI sidebar interaction and resilience
131 lines
4.4 KiB
Python
131 lines
4.4 KiB
Python
"""Client utilities for interacting with the Ollama API via direct HTTP calls."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Dict, Iterable, Iterator
|
|
from urllib.request import Request, urlopen
|
|
from urllib.error import URLError, HTTPError
|
|
|
|
|
|
class OllamaClientError(RuntimeError):
|
|
"""Base exception raised when Ollama operations fail."""
|
|
|
|
|
|
class OllamaUnavailableError(OllamaClientError):
|
|
"""Raised when the Ollama server is not available."""
|
|
|
|
|
|
class OllamaClient:
|
|
"""HTTP client for interacting with Ollama's REST API."""
|
|
|
|
def __init__(self, host: str | None = None) -> None:
|
|
self._host = host or "http://localhost:11434"
|
|
self._cached_models: list[str] | None = None
|
|
|
|
# ------------------------------------------------------------------ helpers
|
|
@property
|
|
def is_available(self) -> bool:
|
|
"""Check if Ollama server is reachable."""
|
|
try:
|
|
req = Request(f"{self._host}/api/tags", method="GET")
|
|
with urlopen(req, timeout=2) as response:
|
|
return response.status == 200
|
|
except (URLError, HTTPError, TimeoutError):
|
|
return False
|
|
|
|
@property
|
|
def default_model(self) -> str | None:
|
|
"""Get the first available model."""
|
|
models = self.list_models()
|
|
return models[0] if models else None
|
|
|
|
def list_models(self, force_refresh: bool = False) -> list[str]:
|
|
"""Return the available model names, caching the result for quick reuse."""
|
|
if self._cached_models is not None and not force_refresh:
|
|
return list(self._cached_models)
|
|
|
|
try:
|
|
req = Request(f"{self._host}/api/tags", method="GET")
|
|
with urlopen(req, timeout=5) as response:
|
|
data = json.loads(response.read().decode())
|
|
except (URLError, HTTPError, TimeoutError) as exc:
|
|
raise OllamaClientError(f"Failed to list models: {exc}") from exc
|
|
|
|
models: list[str] = []
|
|
for item in data.get("models", []):
|
|
name = item.get("name") or item.get("model")
|
|
if name:
|
|
models.append(name)
|
|
|
|
self._cached_models = models
|
|
return list(models)
|
|
|
|
# ------------------------------------------------------------------ chat APIs
|
|
def chat(
|
|
self,
|
|
*,
|
|
model: str,
|
|
messages: Iterable[Dict[str, str]],
|
|
) -> dict[str, str] | None:
|
|
"""Execute a blocking chat call against Ollama."""
|
|
payload = {
|
|
"model": model,
|
|
"messages": list(messages),
|
|
"stream": False,
|
|
}
|
|
|
|
try:
|
|
req = Request(
|
|
f"{self._host}/api/chat",
|
|
data=json.dumps(payload).encode("utf-8"),
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
with urlopen(req, timeout=120) as response:
|
|
result = json.loads(response.read().decode())
|
|
except (URLError, HTTPError, TimeoutError) as exc:
|
|
return {
|
|
"role": "assistant",
|
|
"content": f"Unable to reach Ollama: {exc}",
|
|
}
|
|
|
|
# Parse the response
|
|
message = result.get("message")
|
|
if not message:
|
|
return {"role": "assistant", "content": ""}
|
|
|
|
role = message.get("role", "assistant")
|
|
content = message.get("content", "")
|
|
|
|
return {"role": role, "content": content}
|
|
|
|
def stream_chat(
|
|
self, *, model: str, messages: Iterable[Dict[str, str]]
|
|
) -> Iterator[dict[str, Any]]:
|
|
"""Placeholder for streaming API - not yet implemented."""
|
|
raise NotImplementedError("Streaming chat is not yet implemented")
|
|
|
|
# ------------------------------------------------------------------ internals
|
|
def _make_request(
|
|
self, endpoint: str, method: str = "GET", data: dict | None = None
|
|
) -> dict:
|
|
"""Make an HTTP request to the Ollama API."""
|
|
url = f"{self._host}{endpoint}"
|
|
|
|
if data:
|
|
req = Request(
|
|
url,
|
|
data=json.dumps(data).encode("utf-8"),
|
|
headers={"Content-Type": "application/json"},
|
|
method=method,
|
|
)
|
|
else:
|
|
req = Request(url, method=method)
|
|
|
|
try:
|
|
with urlopen(req, timeout=30) as response:
|
|
return json.loads(response.read().decode())
|
|
except (URLError, HTTPError) as exc:
|
|
raise OllamaClientError(f"Request failed: {exc}") from exc
|