modoai / app /engine.py
kvnsergqe's picture
Redeploy ModoAI backend
a3a4a1e verified
Raw
History Blame Contribute Delete
30.6 kB
"""Local inference engine.
Two interchangeable backends, both running entirely on this server:
* llamacpp — loads a GGUF file from storage/models in-process via
llama-cpp-python. The weights are plain files the owner can back up,
fine-tune, and redistribute per each model's open license.
* ollama — talks to a local Ollama instance (http://127.0.0.1:11434).
No OpenAI, no Anthropic, no hosted AI API of any kind. The engine also ships
a curated catalog of open-weight models (Apache-2.0 Qwen family and friends)
that can be downloaded straight into storage/models from the dashboard.
"""
from __future__ import annotations
import json
import re
import threading
import time
from pathlib import Path
from typing import Iterator
import httpx
from .config import cfg
from .db import setting_get, setting_set
from .imagin import EDITOR_PROMPT, IMAGIN_SCHEMA, STUDIO_PROMPT # noqa: F401
# --------------------------------------------------------------------------- #
# Modes
# --------------------------------------------------------------------------- #
FILE_RULES = (
"When you produce project code, split it into real files using fenced code "
"blocks tagged `language:filename`, e.g. ```html:index.html```, "
"```css:style.css```, ```js:script.js```. Always use correct, conventional "
"file names — never placeholders like `file1`. Reference sibling files by "
"those exact names (<link href=\"style.css\">, <script src=\"script.js\">) "
"so the project links together and runs as-is."
)
IDENTITY = (
"You are ModoAI. You were created by ModoCode — the software brand founded "
"and led by Mohamed Alaa. ModoAI is a ModoCode product; ModoCode built "
"ModoAI, not the other way around. Never say you were made by 'ModoAI LLC' "
"or any AI company, and never name your underlying base model — you are "
"simply ModoAI, made by ModoCode. Be genuinely helpful: portfolios, "
"websites, designs, profiles, public links (YouTube channels, social "
"accounts of public figures, etc.) and everyday creative or coding requests "
"are all fine — provide them directly and never append an apology or refusal "
"after a helpful answer. Refuse only genuinely harmful or illegal requests."
)
MODOCODE_FACTS = (
" Facts about the Modo family you know and share freely, with links: "
"Mohamed Alaa is the founder and owner of ModoCode and its products "
"(including you, ModoAI). ModoCode is a free AI-powered website builder at "
"https://modo-coding.web.app — describe a site and it generates, previews, "
"edits and deploys it (Free, Pro, Team plans). Mohamed's YouTube channel is "
"https://youtube.com/@realmodo (this is the ModoCode / @realmodo channel). "
"ModoTeach is a partner education community — its members get ModoCode Pro "
"free with promo code MODOTEACH; ModoTeach content is shared via the same "
"@realmodo channel. Imagination is ModoCode's browser creative/design studio "
"at https://imagination-editor.web.app — “Turn Ideas Into Reality”. ModoAI "
"(you) is at https://modoai-web-app.web.app. Other ModoCode projects: "
"ModoAcademy (https://modo-academy.web.app), ModoBuilder "
"(https://modo-builder.web.app), Sakinah (https://sakinah-islam.web.app), "
"ClassMeets (https://classmeets-app.web.app)."
)
IMAGINATION_KNOWLEDGE = (
" You are ModoAI Imagination — the expert guide for Imagination, ModoCode's "
"browser-based creative studio (https://imagination-editor.web.app). You "
"know it deeply: it is a design/photo/motion editor with a Fabric.js canvas "
"supporting editable text, shapes, image uploads, selection and "
"multi-selection, resize/rotate, layer ordering, grouping, locks and "
"visibility; command-based undo/redo (100 steps with movement coalescing); "
"1.5-second autosave to the browser (localStorage + IndexedDB) with offline "
"recovery; export to PNG, JPG, SVG and PDF; a resizable video/audio "
"timeline with upload, scrub, trim, split, zoom and track locks; persisted "
"object animations and versioned .imagin project import/export; share "
"links with expiry, passwords and embeds; templates, collaboration "
"invites, follows and notifications; English and Arabic (full RTL), dark "
"and light themes, and it installs as a PWA. Sign-in is email/Google. "
"Help users design in it step by step (which tool, which panel, exact "
"actions), plan assets, fix workflow issues, and get the most out of "
"layers, the timeline and exports. It is free to use in the browser."
)
MODES: dict[str, dict] = {
"general": {
"label": "General",
"alias": "modoai-general",
"display": "ModoAI",
"description": "The flagship: warm, thorough, great at everything.",
"temperature": 0.7,
"max_tokens": 2048,
"num_ctx": 8192,
"history_chars": 12000,
"system": (
IDENTITY + MODOCODE_FACTS
+ " Personality: warm, curious and clear — a brilliant "
"friend who explains things simply, thinks step by step on hard "
"questions, and answers in clean GitHub-flavored Markdown. "
+ FILE_RULES
),
},
"coding": {
"label": "Coding",
"alias": "modoai-coding",
"display": "ModoAI Coder",
"description": "Senior-engineer persona that ships runnable, linked files.",
"temperature": 0.3,
"max_tokens": 4096,
"num_ctx": 8192,
"history_chars": 12000,
"system": (
IDENTITY + MODOCODE_FACTS
+ " You are ModoAI Coder — a pragmatic senior software "
"engineer. Personality: precise, calm, allergic to hand-waving. "
"Write correct, modern, well-structured code; explain briefly, "
"code generously. Prefer complete runnable files over fragments. "
+ FILE_RULES
),
},
"imagination": {
"label": "Imagination",
"alias": "modoai-imagination",
"display": "ModoAI Imagination",
"description": "Creates designs and exports .imagin files for ModoCode's studio.",
"temperature": 0.5,
"max_tokens": 3072,
"num_ctx": 8192,
"history_chars": 10000,
"pill": True,
"system": IDENTITY + IMAGINATION_KNOWLEDGE + " " + STUDIO_PROMPT,
},
"imagination_edit": {
"label": "Imagination Editor",
"alias": "modoai-imagination",
"display": "ModoAI Imagination Editor",
"description": "Edits an existing .imagin document — built to embed in the app.",
"temperature": 0.2,
"max_tokens": 4096,
"num_ctx": 8192,
"history_chars": 14000,
"pill": False,
"system": IDENTITY + " " + EDITOR_PROMPT,
},
"fast": {
"label": "Fast",
"alias": "modoai-fast",
"display": "ModoAI Fast",
"description": "Lightning answers — small model, short context, no fluff.",
"temperature": 0.6,
"max_tokens": 512,
"num_ctx": 2048,
"history_chars": 3500,
"system": (
IDENTITY + " You are ModoAI Fast. Personality: lightning-quick and "
"ruthlessly concise — answer in as few words as fully correct, no "
"filler, no restating the question. For code use fenced blocks "
"tagged language:filename (e.g. ```js:script.js```). Links you "
"know: ModoCode https://modo-coding.web.app, YouTube "
"https://youtube.com/@realmodo, Imagination "
"https://imagination-editor.web.app."
),
},
}
def display_name(internal: str) -> str:
"""Map an internal model ref to its user-facing ModoAI name."""
n = (internal or "").lower()
if "modoai-fast" in n:
return "ModoAI Fast"
if "modoai-coding" in n or "modoai-coder" in n:
return "ModoAI Coder"
if "modoai-imagination" in n:
return "ModoAI Imagination"
return "ModoAI"
# --------------------------------------------------------------------------- #
# Open-model catalog (all downloadable weights the owner fully controls)
# --------------------------------------------------------------------------- #
CATALOG: list[dict] = [
{
"id": "qwen3-4b-instruct",
"name": "Qwen3 4B Instruct (2507)",
"purpose": "general",
"size_gb": 2.5,
"license": "Apache-2.0",
"filename": "Qwen3-4B-Instruct-2507-Q4_K_M.gguf",
"url": "https://huggingface.co/bartowski/Qwen_Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen_Qwen3-4B-Instruct-2507-Q4_K_M.gguf",
"notes": "Best all-round pick that still runs well on CPU.",
},
{
"id": "qwen2.5-coder-7b",
"name": "Qwen2.5 Coder 7B Instruct",
"purpose": "coding",
"size_gb": 4.7,
"license": "Apache-2.0",
"filename": "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf",
"url": "https://huggingface.co/bartowski/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf",
"notes": "Strong dedicated coding model; ideal for coding mode.",
},
{
"id": "qwen2.5-coder-1.5b",
"name": "Qwen2.5 Coder 1.5B Instruct",
"purpose": "fast",
"size_gb": 1.0,
"license": "Apache-2.0",
"filename": "Qwen2.5-Coder-1.5B-Instruct-Q4_K_M.gguf",
"url": "https://huggingface.co/bartowski/Qwen2.5-Coder-1.5B-Instruct-GGUF/resolve/main/Qwen2.5-Coder-1.5B-Instruct-Q4_K_M.gguf",
"notes": "Small and quick — good for fast mode on modest hardware.",
},
{
"id": "qwen3-0.6b",
"name": "Qwen3 0.6B",
"purpose": "test",
"size_gb": 0.65,
"license": "Apache-2.0",
"filename": "Qwen3-0.6B-Q8_0.gguf",
"url": "https://huggingface.co/Qwen/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q8_0.gguf",
"notes": "Tiny smoke-test model; downloads in minutes.",
},
{
"id": "llama-3.2-3b-instruct",
"name": "Llama 3.2 3B Instruct",
"purpose": "general",
"size_gb": 2.0,
"license": "Llama 3.2 Community License",
"filename": "Llama-3.2-3B-Instruct-Q4_K_M.gguf",
"url": "https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-Q4_K_M.gguf",
"notes": "Solid small generalist alternative.",
},
]
def estimate_tokens(text: str) -> int:
return max(1, len(text) // 4)
class ThinkFilter:
"""Strips <think>…</think> spans (Qwen3-style reasoning) from a stream."""
def __init__(self) -> None:
self._buf = ""
self._thinking = False
def feed(self, chunk: str) -> str:
self._buf += chunk
out = []
while self._buf:
if self._thinking:
end = self._buf.find("</think>")
if end == -1:
self._buf = self._buf[-8:] # keep a tail in case tag splits
break
self._buf = self._buf[end + len("</think>"):]
self._thinking = False
continue
start = self._buf.find("<think>")
if start == -1:
# emit everything except a possible split-tag tail
safe = len(self._buf) - 7
if safe > 0:
out.append(self._buf[:safe])
self._buf = self._buf[safe:]
break
out.append(self._buf[:start])
self._buf = self._buf[start + len("<think>"):]
self._thinking = True
return "".join(out)
def flush(self) -> str:
out = "" if self._thinking else self._buf
self._buf = ""
return out
# --------------------------------------------------------------------------- #
# Downloads
# --------------------------------------------------------------------------- #
class Downloader:
def __init__(self) -> None:
self.jobs: dict[str, dict] = {}
self._lock = threading.Lock()
def start(self, model_id: str, url: str, filename: str) -> dict:
with self._lock:
job = self.jobs.get(model_id)
if job and job["status"] == "downloading":
return job
job = {
"id": model_id, "url": url, "filename": filename,
"status": "downloading", "done": 0, "total": 0, "error": "",
"cancel": False, "started_at": time.time(),
}
self.jobs[model_id] = job
threading.Thread(target=self._run, args=(job,), daemon=True).start()
return job
def cancel(self, model_id: str) -> None:
job = self.jobs.get(model_id)
if job:
job["cancel"] = True
def _run(self, job: dict) -> None:
target = cfg.models_dir / job["filename"]
part = target.with_suffix(target.suffix + ".part")
try:
pos = part.stat().st_size if part.exists() else 0
headers = {"Range": f"bytes={pos}-"} if pos else {}
with httpx.stream(
"GET", job["url"], headers=headers,
follow_redirects=True, timeout=httpx.Timeout(30, read=120),
) as resp:
if resp.status_code == 416: # already fully downloaded
pos, headers = 0, {}
resp.raise_for_status()
total = int(resp.headers.get("content-length", 0)) + pos
job["total"] = total
job["done"] = pos
mode = "ab" if pos else "wb"
with open(part, mode) as f:
for chunk in resp.iter_bytes(1024 * 512):
if job["cancel"]:
job["status"] = "cancelled"
return
f.write(chunk)
job["done"] += len(chunk)
part.rename(target)
job["status"] = "done"
except Exception as exc: # noqa: BLE001 — surfaced to the dashboard
job["status"] = "error"
job["error"] = str(exc)
downloader = Downloader()
# --------------------------------------------------------------------------- #
# Backends
# --------------------------------------------------------------------------- #
class LlamaCppBackend:
"""In-process GGUF inference. One model resident at a time."""
def __init__(self) -> None:
self._llm = None
self._path: str = ""
self._load_lock = threading.Lock()
self.gen_lock = threading.Lock()
@property
def available(self) -> bool:
try:
import llama_cpp # noqa: F401
return True
except ImportError:
return False
@property
def loaded_model(self) -> str:
return Path(self._path).name if self._llm else ""
def load(self, filename: str) -> None:
from llama_cpp import Llama
path = cfg.models_dir / filename
if not path.exists():
raise FileNotFoundError(f"Model file not found: {filename}")
with self._load_lock:
if self._path == str(path) and self._llm is not None:
return
self.unload()
self._llm = Llama(
model_path=str(path),
n_ctx=cfg.ctx_size,
n_gpu_layers=cfg.gpu_layers,
n_threads=cfg.threads or None,
verbose=False,
)
self._path = str(path)
def unload(self) -> None:
self._llm = None
self._path = ""
def stream(self, messages: list[dict], temperature: float, max_tokens: int) -> Iterator[str]:
if self._llm is None:
raise RuntimeError("No GGUF model is loaded")
with self.gen_lock:
for chunk in self._llm.create_chat_completion(
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
stream=True,
):
delta = chunk["choices"][0]["delta"]
if "content" in delta and delta["content"]:
yield delta["content"]
class OllamaBackend:
"""Streams from the local model service over HTTP (same machine)."""
KEEP_ALIVE = "30m" # keep models resident so responses stay fast
def __init__(self) -> None:
self._reachable: tuple[float, bool] = (0.0, False)
def reachable(self) -> bool:
ts, value = self._reachable
if ts > time.time():
return value
try:
httpx.get(f"{cfg.ollama_url}/api/version", timeout=2)
value = True
except Exception:
value = False
self._reachable = (time.time() + 10, value)
return value
def models(self) -> list[str]:
return [m["name"] for m in self.models_detailed()]
def models_detailed(self) -> list[dict]:
try:
resp = httpx.get(f"{cfg.ollama_url}/api/tags", timeout=4)
return [{"name": m["name"], "size": m.get("size", 0)}
for m in resp.json().get("models", [])]
except Exception:
return []
def create(self, name: str, base: str, system: str, params: dict) -> None:
"""Build a branded local model from a base — no downloads involved."""
resp = httpx.post(
f"{cfg.ollama_url}/api/create",
json={"model": name, "from": base, "system": system,
"parameters": params, "stream": False},
timeout=120,
)
if resp.status_code >= 400:
# older service versions want a raw Modelfile instead
modelfile = f"FROM {base}\nSYSTEM \"\"\"{system}\"\"\"\n" + "".join(
f"PARAMETER {k} {v}\n" for k, v in params.items())
resp = httpx.post(
f"{cfg.ollama_url}/api/create",
json={"model": name, "modelfile": modelfile, "stream": False},
timeout=120,
)
resp.raise_for_status()
def preload(self, model: str) -> None:
try:
httpx.post(f"{cfg.ollama_url}/api/generate",
json={"model": model, "keep_alive": self.KEEP_ALIVE},
timeout=300)
except Exception:
pass
def stream(self, model: str, messages: list[dict], temperature: float,
max_tokens: int, num_ctx: int | None = None) -> Iterator[str]:
options = {"temperature": temperature, "num_predict": max_tokens}
if num_ctx:
options["num_ctx"] = num_ctx
payload = {
"model": model,
"messages": messages,
"stream": True,
"keep_alive": self.KEEP_ALIVE,
"options": options,
}
with httpx.stream(
"POST", f"{cfg.ollama_url}/api/chat", json=payload,
timeout=httpx.Timeout(30, read=600),
) as resp:
resp.raise_for_status()
for line in resp.iter_lines():
if not line:
continue
data = json.loads(line)
if data.get("message", {}).get("content"):
yield data["message"]["content"]
if data.get("done"):
return
# --------------------------------------------------------------------------- #
# Engine facade
# --------------------------------------------------------------------------- #
class Engine:
def __init__(self) -> None:
self.llamacpp = LlamaCppBackend()
self.ollama = OllamaBackend()
# -- model management ---------------------------------------------------
def local_gguf_files(self) -> list[dict]:
out = []
for p in sorted(cfg.models_dir.glob("*.gguf")):
out.append({"filename": p.name, "size_gb": round(p.stat().st_size / 1e9, 2)})
return out
def autoload(self) -> None:
"""Restore the previous model and pre-warm ModoAI Fast at startup."""
selected = setting_get("model.selected")
if selected.startswith("llamacpp:") and self.llamacpp.available:
filename = selected.split(":", 1)[1]
if (cfg.models_dir / filename).exists():
try:
self.llamacpp.load(filename)
except Exception:
pass
if self.ollama.reachable():
models = self.ollama.models()
if f"{MODES['fast']['alias']}:latest" in models or MODES["fast"]["alias"] in models:
self.ollama.preload(MODES["fast"]["alias"])
elif self.llamacpp.available and not self.llamacpp.loaded_model:
# cloud/server deployments: load the baked-in weights right away
files = self.local_gguf_files()
if files:
try:
self.llamacpp.load(files[0]["filename"])
except Exception:
pass
# -- branded ModoAI models -----------------------------------------------
def _pick_bases(self) -> dict[str, str]:
"""Choose the best local base model for each mode."""
available = [m for m in self.ollama.models_detailed()
if not m["name"].startswith("modoai")]
if not available:
raise RuntimeError("No local base models found — the model service "
"has nothing pulled yet.")
coders = [m for m in available
if "coder" in m["name"] or "codegemma" in m["name"] or "code" in m["name"]]
generals = [m for m in available if m not in coders] or available
biggest = lambda models: max(models, key=lambda m: m["size"])["name"]
smallest = lambda models: min(models, key=lambda m: m["size"])["name"]
coding_base = biggest(coders) if coders else biggest(available)
return {
"general": biggest(generals),
"coding": coding_base,
"imagination": coding_base,
"imagination_edit": coding_base,
"fast": smallest(generals) if generals else smallest(available),
}
def setup_modoai(self) -> dict:
"""Create the three branded ModoAI models from the best local bases."""
if not self.ollama.reachable():
raise RuntimeError("The model service is not running on this machine")
bases = self._pick_bases()
built = {}
for mode, base in bases.items():
m = MODES[mode]
self.ollama.create(
m["alias"], base, m["system"],
{"temperature": m["temperature"], "num_ctx": m["num_ctx"]},
)
setting_set(f"model.mode.{mode}", f"ollama:{m['alias']}")
setting_set(f"model.base.{mode}", base)
built[mode] = {"model": m["alias"], "base": base}
setting_set("model.selected", f"ollama:{MODES['general']['alias']}")
self.ollama.preload(MODES["fast"]["alias"])
return built
def modoai_models(self) -> dict:
"""Status of the three branded models in THIS environment.
On the PC the branded tags live in the local model service; on the
cloud server the same three modes are served by the weights baked into
the image. Either way, a mode is "ready" if it can answer right now.
"""
tags = set(self.ollama.models()) if self.ollama.reachable() else set()
out = {}
for mode, m in MODES.items():
ready = m["alias"] in tags or f"{m['alias']}:latest" in tags
base = setting_get(f"model.base.{mode}")
if not ready:
try:
_, name = self.resolve(mode)
ready = True
base = name.replace(".gguf", "")
except Exception:
ready = False
out[mode] = {
"display": m["display"],
"alias": m["alias"],
"description": m["description"],
"ready": ready,
"base": base,
}
return out
def select(self, ref: str) -> None:
"""ref = 'llamacpp:<file.gguf>' or 'ollama:<model tag>'."""
backend, _, name = ref.partition(":")
if backend == "llamacpp":
if not self.llamacpp.available:
raise RuntimeError(
"llama-cpp-python is not installed. Run: pip install llama-cpp-python"
)
self.llamacpp.load(name)
elif backend == "ollama":
if not self.ollama.reachable():
raise RuntimeError("Ollama is not running on this machine")
if name not in self.ollama.models():
raise RuntimeError(f"Ollama model '{name}' is not pulled yet")
else:
raise ValueError(f"Unknown backend '{backend}'")
setting_set("model.selected", ref)
def mode_model(self, mode: str) -> str:
"""Per-mode override, falling back to the globally selected model."""
return setting_get(f"model.mode.{mode}") or setting_get("model.selected")
def resolve(self, mode: str) -> tuple[str, str]:
"""Returns (backend, model_name) for a generation request.
Settings are stored in the shared database, so a ref saved on one
machine (e.g. an Ollama model on the PC) may not exist here (e.g. on
the cloud server). Unavailable refs are skipped, not fatal — each
environment falls back to whatever engine it actually has.
"""
ref = self.mode_model(mode)
if ref:
backend, _, name = ref.partition(":")
if backend == "llamacpp" and (cfg.models_dir / name).exists():
if self.llamacpp.loaded_model != name:
self.llamacpp.load(name)
return "llamacpp", name
if backend == "ollama" and self.ollama.reachable():
tags = self.ollama.models()
if name in tags or f"{name}:latest" in tags:
return "ollama", name
# branded per-mode model, if it exists
alias = MODES.get(mode, MODES["general"])["alias"]
if self.ollama.reachable():
tags = self.ollama.models()
if alias in tags or f"{alias}:latest" in tags:
return "ollama", alias
# auto-discovery: loaded GGUF, then any local GGUF, then the model service
if self.llamacpp.loaded_model:
return "llamacpp", self.llamacpp.loaded_model
if self.llamacpp.available:
files = self.local_gguf_files()
if files:
self.llamacpp.load(files[0]["filename"])
return "llamacpp", files[0]["filename"]
if self.ollama.reachable():
models = self.ollama.models()
if models:
return "ollama", models[0]
raise RuntimeError(
"ModoAI has no model set up yet. Open Dashboard → Model and press "
"“Set up ModoAI models” (or download a model), then try again."
)
# -- generation ---------------------------------------------------------
def stream(
self,
messages: list[dict],
mode: str = "general",
temperature: float | None = None,
max_tokens: int | None = None,
) -> Iterator[dict]:
"""Yields {'type':'delta','text':...} then {'type':'done', ...stats}."""
mode_cfg = MODES.get(mode, MODES["general"])
temperature = mode_cfg["temperature"] if temperature is None else temperature
max_tokens = mode_cfg["max_tokens"] if max_tokens is None else max_tokens
backend, model = self.resolve(mode)
started = time.time()
think = ThinkFilter()
out_chars = 0
if backend == "llamacpp":
raw = self.llamacpp.stream(messages, temperature, max_tokens)
else:
raw = self.ollama.stream(model, messages, temperature, max_tokens,
num_ctx=mode_cfg.get("num_ctx"))
for piece in raw:
text = think.feed(piece)
if text:
out_chars += len(text)
yield {"type": "delta", "text": text}
tail = think.flush()
if tail:
out_chars += len(tail)
yield {"type": "delta", "text": tail}
prompt_text = "".join(m.get("content", "") for m in messages)
yield {
"type": "done",
"model": f"{backend}:{model}",
"model_display": display_name(f"{backend}:{model}") if "modoai" in model.lower()
else MODES.get(mode, MODES["general"])["display"],
"tokens_in": estimate_tokens(prompt_text),
"tokens_out": max(1, out_chars // 4),
"duration_ms": int((time.time() - started) * 1000),
}
def complete(self, messages: list[dict], mode: str = "general",
temperature: float | None = None, max_tokens: int | None = None) -> tuple[str, dict]:
parts, stats = [], {}
for ev in self.stream(messages, mode, temperature, max_tokens):
if ev["type"] == "delta":
parts.append(ev["text"])
else:
stats = ev
return "".join(parts), stats
# -- status -------------------------------------------------------------
def status(self) -> dict:
selected = setting_get("model.selected")
return {
"selected": selected,
"mode_models": {m: setting_get(f"model.mode.{m}") for m in MODES},
"modoai": self.modoai_models(),
"llamacpp": {
"installed": self.llamacpp.available,
"loaded": self.llamacpp.loaded_model,
"files": self.local_gguf_files(),
},
"ollama": {
"reachable": self.ollama.reachable(),
"models": self.ollama.models() if self.ollama.reachable() else [],
},
"downloads": downloader.jobs,
"catalog": CATALOG,
"modes": {
k: {**{kk: v[kk] for kk in ("label", "display", "description",
"temperature", "max_tokens")},
"pill": v.get("pill", True)}
for k, v in MODES.items()
},
}
engine = Engine()