init
This commit is contained in:
99
assistant_bot/ollama.py
Normal file
99
assistant_bot/ollama.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import asyncio
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
from .ai import AIClientError
|
||||
|
||||
|
||||
class OllamaError(AIClientError):
|
||||
pass
|
||||
|
||||
|
||||
class OllamaClient:
|
||||
provider = "local"
|
||||
display_name = "Ollama"
|
||||
|
||||
def __init__(self, base_url: str) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
|
||||
def normalize_model(self, model: str) -> str:
|
||||
return model.strip()
|
||||
|
||||
async def list_models(self) -> list[str]:
|
||||
return await asyncio.to_thread(self._list_models)
|
||||
|
||||
async def chat(
|
||||
self,
|
||||
model: str,
|
||||
messages: list[dict[str, str]],
|
||||
json_mode: bool = False,
|
||||
) -> str:
|
||||
return await asyncio.to_thread(self._chat, model, messages, json_mode)
|
||||
|
||||
def _list_models(self) -> list[str]:
|
||||
data = self._request_json("GET", "/api/tags", None, timeout=15)
|
||||
models = data.get("models", [])
|
||||
return sorted(model["name"] for model in models if model.get("name"))
|
||||
|
||||
def _chat(
|
||||
self,
|
||||
model: str,
|
||||
messages: list[dict[str, str]],
|
||||
json_mode: bool,
|
||||
) -> str:
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
}
|
||||
if json_mode:
|
||||
payload["format"] = "json"
|
||||
|
||||
data = self._request_json("POST", "/api/chat", payload, timeout=180)
|
||||
content = data.get("message", {}).get("content")
|
||||
if isinstance(content, str) and content.strip():
|
||||
return content.strip()
|
||||
|
||||
fallback = data.get("response")
|
||||
if isinstance(fallback, str) and fallback.strip():
|
||||
return fallback.strip()
|
||||
|
||||
raise OllamaError("Ollama returned an empty response.")
|
||||
|
||||
def _request_json(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
payload: dict[str, Any] | None,
|
||||
timeout: int,
|
||||
) -> dict[str, Any]:
|
||||
url = f"{self.base_url}{path}"
|
||||
data = json.dumps(payload).encode("utf-8") if payload is not None else None
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=data,
|
||||
method=method,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
raw = response.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode("utf-8", errors="replace")[:400]
|
||||
raise OllamaError(f"Ollama HTTP {exc.code}: {body}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise OllamaError(f"Ollama is unavailable at {self.base_url}: {exc.reason}") from exc
|
||||
except TimeoutError as exc:
|
||||
raise OllamaError("Ollama request timed out.") from exc
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise OllamaError("Ollama returned invalid JSON.") from exc
|
||||
|
||||
if not isinstance(parsed, dict):
|
||||
raise OllamaError("Ollama returned an unexpected response.")
|
||||
return parsed
|
||||
Reference in New Issue
Block a user