mindXtrain / mindxtrain /ui /console.py
Gregory-L's picture
autoresearch: the keep/reset search, and the UI modules that were never shipped
c27193b verified
Raw History Blame Contribute Delete
7.08 kB
"""Talk to what you just trained — Ollama helpers, ported from this house's own consoles.
Provenance: `pythaiml/automindx · ollama_codephreak.py` (the live model picker, streaming `/api/chat`,
a token counter built from the engine's final object, errors written as instructions) and
`Professor-Codephreak/automind · memory.py` (one record per exchange). Two years of interfaces agreed
on one rule and it is kept here: **never block the interface on a model load** — the daemon holds the
weights, the UI asks it questions.
Nothing in this module imports torch. A training box that cannot spare the RAM can still use the
console, because the model lives in Ollama's process, not in this one.
"""
from __future__ import annotations
import json
import os
import time
import urllib.error
import urllib.request
from collections.abc import Iterator
from pathlib import Path
from typing import Any
HOST = os.environ.get("OLLAMA_HOST", "http://127.0.0.1:11434").rstrip("/")
# The standard set, with the defaults Ollama itself uses. The UI renders these; the engine honours
# every one of them, so nothing here is decorative.
DEFAULTS: dict[str, Any] = {
"temperature": 0.7, "top_p": 0.9, "top_k": 40, "min_p": 0.0,
"repeat_penalty": 1.1, "repeat_last_n": 64, "num_predict": 256, "num_ctx": 4096,
"seed": 0, "stop": [], "presence_penalty": 0.0, "frequency_penalty": 0.0,
"mirostat": 0, "mirostat_tau": 5.0, "mirostat_eta": 0.1,
}
# The imprint gate's decoding. A score measured under anything else is not comparable with the
# numbers in an ascent log, and saying so is cheaper than explaining a mismatch later.
GATE_DECODING: dict[str, Any] = {"temperature": 0.0, "repeat_penalty": 1.3, "top_p": 1.0, "top_k": 0}
def models(host: str = "", timeout: float = 4.0) -> list[str]:
"""Tags the daemon is serving, local first. Empty when it is down — never an exception."""
try:
with urllib.request.urlopen((host or HOST).rstrip("/") + "/api/tags", timeout=timeout) as r:
names = [m["name"] for m in json.loads(r.read().decode()).get("models", [])]
except Exception:
return []
names.sort(key=lambda n: (n.endswith(":cloud"), n))
return names
def running(host: str = "", timeout: float = 4.0) -> list[dict[str, Any]]:
"""What is resident right now, with its size and expiry — the honest answer to "is it warm?"."""
try:
with urllib.request.urlopen((host or HOST).rstrip("/") + "/api/ps", timeout=timeout) as r:
return json.loads(r.read().decode()).get("models", []) or []
except Exception:
return []
def token_line(done: dict[str, Any]) -> str:
prompt = int(done.get("prompt_eval_count") or 0)
completion = int(done.get("eval_count") or 0)
eval_s = (done.get("eval_duration") or 0) / 1e9
load_s = (done.get("load_duration") or 0) / 1e9
rate = f" · {completion / eval_s:.2f} tok/s" if eval_s > 0 else ""
load = f" · {load_s:.1f}s load" if load_s > 0.5 else ""
return f"{prompt + completion} tokens — {prompt} prompt + {completion} completion{rate}{load}"
def token_stats(done: dict[str, Any]) -> dict[str, Any]:
prompt = int(done.get("prompt_eval_count") or 0)
completion = int(done.get("eval_count") or 0)
eval_s = (done.get("eval_duration") or 0) / 1e9
return {"prompt_tokens": prompt, "completion_tokens": completion, "total_tokens": prompt + completion,
"tokens_per_s": round(completion / eval_s, 2) if eval_s > 0 else None,
"load_s": round((done.get("load_duration") or 0) / 1e9, 2),
"eval_s": round(eval_s, 2), "done_reason": done.get("done_reason")}
def explain(status: int, body: str, model: str, host: str = "") -> str:
text = (body or "")[:400]
if status == 404 or "not found" in text.lower():
return f"**{model}** is not on this daemon:\n\n```bash\nollama pull {model}\n```\n\nOr serve a checkpoint you trained: `mindxtrain serve --to ollama --tag {model}`."
if status in (401, 402, 403) or "subscription" in text.lower():
return f"**{model}** looks like a cloud tag needing a subscription. Use a local tag instead."
if "context" in text.lower():
return "The prompt is longer than `num_ctx`. Raise it, or send less."
return f"Ollama answered **{status}** from `{host or HOST}`:\n\n```\n{text}\n```"
def unreachable(host: str = "") -> str:
return (f"No Ollama at `{host or HOST}`. Start it (`ollama serve`), then either pull a tag or serve "
"a checkpoint from the **Serve** room. The UI stays up either way — it never holds the weights.")
def chat(messages: list[dict[str, str]], model: str, *, options: dict[str, Any] | None = None,
keep_alive: str = "10m", host: str = "", timeout: float = 900.0) -> Iterator[tuple[str, dict[str, Any]]]:
"""Stream `(text_so_far, stats)`. `stats` is empty until the final object, which carries the real
counts. Options are passed through verbatim; the engine implements them, so none are dropped."""
host = (host or HOST).rstrip("/")
body = {"model": model, "messages": messages, "stream": True, "keep_alive": keep_alive,
"options": {k: v for k, v in (options or {}).items() if v not in (None, "", [])}}
req = urllib.request.Request(f"{host}/api/chat", data=json.dumps(body).encode(), method="POST",
headers={"Content-Type": "application/json"})
acc = ""
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
for raw in r:
line = raw.decode("utf-8", errors="replace").strip()
if not line:
continue
try:
obj = json.loads(line)
except Exception:
continue
acc += (obj.get("message") or {}).get("content", "")
if obj.get("done"):
yield acc, token_stats(obj)
return
if acc:
yield acc, {}
except urllib.error.HTTPError as e:
yield explain(e.code, e.read().decode("utf-8", errors="replace"), model, host), {"error": True}
return
except Exception:
yield unreachable(host), {"error": True}
return
if not acc:
yield "[the model returned nothing — raise num_predict, or try another tag]", {"error": True}
def record(prompt: str, answer: str, meta: dict[str, Any] | None = None, path: Path | None = None) -> Path | None:
"""One JSONL line per exchange, so a session leaves evidence rather than scrollback."""
try:
p = Path(path or os.environ.get("MINDXTRAIN_CONSOLE_LOG") or (Path.home() / ".mindxtrain" / "console.jsonl"))
p.parent.mkdir(parents=True, exist_ok=True)
with p.open("a", encoding="utf-8") as fh:
fh.write(json.dumps({"ts": time.time(), "prompt": prompt[:4000],
"answer": (answer or "")[:8000], **(meta or {})}, ensure_ascii=False) + "\n")
return p
except Exception:
return None