This commit is contained in:
kandrusyak
2026-07-25 15:07:53 +03:00
commit 6667e0d253
40 changed files with 3258 additions and 0 deletions

99
assistant_bot/ollama.py Normal file
View 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