| """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 |
|
|
| |
| |
| |
|
|
| 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" |
|
|
| |
| |
| |
|
|
| 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:] |
| break |
| self._buf = self._buf[end + len("</think>"):] |
| self._thinking = False |
| continue |
| start = self._buf.find("<think>") |
| if start == -1: |
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| 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: |
| 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: |
| job["status"] = "error" |
| job["error"] = str(exc) |
|
|
|
|
| downloader = Downloader() |
|
|
|
|
| |
| |
| |
|
|
| 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 |
| 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" |
|
|
| 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: |
| |
| 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 |
|
|
|
|
| |
| |
| |
|
|
| class Engine: |
| def __init__(self) -> None: |
| self.llamacpp = LlamaCppBackend() |
| self.ollama = OllamaBackend() |
|
|
| |
|
|
| 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: |
| |
| files = self.local_gguf_files() |
| if files: |
| try: |
| self.llamacpp.load(files[0]["filename"]) |
| except Exception: |
| pass |
|
|
| |
|
|
| 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 |
| |
| 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 |
| |
| 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." |
| ) |
|
|
| |
|
|
| 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 |
|
|
| |
|
|
| 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() |
|
|