mirror of
https://github.com/RichieCahill/dotfiles.git
synced 2026-04-17 13:08:19 -04:00
add Signal command and control bot service
Python service for jeeves that communicates over Signal via signal-cli-rest-api. Implements device verification via safety numbers (unverified devices cannot run commands until verified over SSH), and a van inventory command that uses an LLM on BOB (ollama) to parse receipt photos or text lists into structured inventory data. The LLM backend is configurable to swap models easily. https://claude.ai/code/session_01AKXQBuVBsW7J1YbukDiQ7A
This commit is contained in:
73
python/signal_bot/llm_client.py
Normal file
73
python/signal_bot/llm_client.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Flexible LLM client for ollama backends."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import httpx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from python.signal_bot.models import LLMConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMClient:
|
||||
"""Talk to an ollama instance.
|
||||
|
||||
Designed to be swappable — change ``config.model`` to try a new LLM
|
||||
without touching any calling code.
|
||||
|
||||
Args:
|
||||
config: LLM connection and model configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LLMConfig) -> None:
|
||||
self.config = config
|
||||
self._client = httpx.Client(base_url=config.base_url, timeout=120)
|
||||
|
||||
def chat(self, prompt: str, *, system: str = "") -> str:
|
||||
"""Send a text prompt and return the response."""
|
||||
messages: list[dict[str, Any]] = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
return self._generate(messages)
|
||||
|
||||
def chat_with_image(self, prompt: str, image_data: bytes, *, system: str = "") -> str:
|
||||
"""Send a prompt with an image and return the response.
|
||||
|
||||
Requires a vision-capable model (e.g. qwen3-vl).
|
||||
"""
|
||||
encoded = base64.b64encode(image_data).decode()
|
||||
messages: list[dict[str, Any]] = []
|
||||
if system:
|
||||
messages.append({"role": "system", "content": system})
|
||||
messages.append({"role": "user", "content": prompt, "images": [encoded]})
|
||||
return self._generate(messages)
|
||||
|
||||
def _generate(self, messages: list[dict[str, Any]]) -> str:
|
||||
"""Call the ollama chat API."""
|
||||
payload = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"stream": False,
|
||||
"options": {"temperature": self.config.temperature},
|
||||
}
|
||||
logger.info(f"LLM request to {self.config.model}")
|
||||
response = self._client.post("/api/chat", json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data["message"]["content"]
|
||||
|
||||
def list_models(self) -> list[str]:
|
||||
"""List available models on the ollama instance."""
|
||||
response = self._client.get("/api/tags")
|
||||
response.raise_for_status()
|
||||
return [m["name"] for m in response.json().get("models", [])]
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the HTTP client."""
|
||||
self._client.close()
|
||||
Reference in New Issue
Block a user