| """ |
| llm_local.py — local sub-agent pool (llama.cpp runtime, no cloud APIs). |
| |
| Two independent model instances ("sub-agents"), each with its own lock, so |
| features never block each other with "model busy": |
| |
| fast · Summary Sub-Agent (Chan-Tuned Qwen3-1.7B) |
| → Explain-in-English, sector-rotation narrative, news briefs. |
| Small = quick CPU prefill, answers start streaming in seconds. |
| deep · Analyst — Qwen3-4B Q4_K_M by default (swappable in the Model tab) |
| → the multi-step Auto Research agent's report writing. |
| |
| Both run through llama.cpp (llama-cpp-python) and are far below the 32B cap; |
| the fast worker doubles as the "Tiny Titan" (≤4B) story. ~5 GB RAM total on a |
| 32 GB Space. Earns "Off the Grid" + "Llama Champion". |
| """ |
| from __future__ import annotations |
|
|
| import os |
| import re |
| import threading |
|
|
| import paths |
| from huggingface_hub import hf_hub_download |
|
|
| |
| MODEL_ZOO = { |
| "Chan-Tuned Qwen3-1.7B · my fine-tune": ( |
| "ranranrunforit/chan-compass-qwen3-1.7b-gguf", "qwen3-1.7b.Q8_0.gguf"), |
| "Qwen3-1.7B · Tiny Titan (≤4B award class)": ( |
| "Qwen/Qwen3-1.7B-GGUF", "Qwen3-1.7B-Q8_0.gguf"), |
| "Qwen3-4B · default — fast + smart, still ≤4B": ( |
| "Qwen/Qwen3-4B-GGUF", "Qwen3-4B-Q4_K_M.gguf"), |
| "Qwen3-8B · best balance on 8 vCPU / 32 GB": ( |
| "Qwen/Qwen3-8B-GGUF", "Qwen3-8B-Q4_K_M.gguf"), |
| "Qwen3-14B · max quality (still far under 32B cap)": ( |
| "Qwen/Qwen3-14B-GGUF", "Qwen3-14B-Q4_K_M.gguf"), |
| } |
| |
| |
| FAST_MODEL = "Qwen3-1.7B · Tiny Titan (≤4B award class)" |
| TRANSLATOR_MODEL = "Chan-Tuned Qwen3-1.7B · my fine-tune" |
| DEFAULT_MODEL = "Qwen3-4B · default — fast + smart, still ≤4B" |
|
|
| _THINK_RE = re.compile(r"<think>.*?</think>", re.S) |
| _NCPU = max(2, (os.cpu_count() or 4)) |
|
|
| |
| |
| |
| |
| WORKER_LABEL = { |
| "interpreter": "Interpreter sub-agent (Signals · Explain)", |
| "narrator": "Narrator sub-agent (Sector Rotation)", |
| "reporter": "Reporter sub-agent (News · Research support)", |
| "analyst": "Analyst sub-agent (Auto Research)", |
| } |
|
|
|
|
| def _mk(model): |
| return {"model": model, "llm": None, "lock": threading.Lock(), |
| "load_lock": threading.Lock(), "stage": "idle", "detail": "", "ts": None} |
|
|
|
|
| WORKERS = { |
| "interpreter": _mk(TRANSLATOR_MODEL), |
| "narrator": _mk(FAST_MODEL), |
| "reporter": _mk(FAST_MODEL), |
| "analyst": _mk(DEFAULT_MODEL), |
| } |
| |
| _ALIAS = {"fast": "interpreter", "deep": "analyst"} |
|
|
|
|
| def _wk(worker: str) -> str: |
| return _ALIAS.get(worker, worker) |
|
|
| _install_lock = threading.Lock() |
|
|
|
|
| def _set_stage(worker: str, stage: str, detail: str = ""): |
| import datetime as _dt |
| w = WORKERS[worker] |
| w.update(stage=stage, detail=detail[:400], |
| ts=_dt.datetime.utcnow().strftime("%H:%M:%S UTC")) |
| try: |
| import automation |
| automation._log(f"[{WORKER_LABEL[worker]}] {stage}: {detail[:140]}") |
| except Exception: |
| pass |
|
|
|
|
| |
| |
| |
| |
| |
| _WHEEL_INDEX = "https://abetlen.github.io/llama-cpp-python/whl/cpu" |
| _LLAMA_REQ = "llama-cpp-python>=0.3.8" |
|
|
|
|
| def _ensure_llama_cpp(worker: str) -> str: |
| try: |
| import llama_cpp |
| return "" |
| except ImportError: |
| pass |
| import subprocess |
| import sys |
| with _install_lock: |
| try: |
| import llama_cpp |
| return "" |
| except ImportError: |
| pass |
| env = dict(os.environ) |
| env["CMAKE_BUILD_PARALLEL_LEVEL"] = "4" |
| _set_stage(worker, "installing llama.cpp runtime", |
| "trying official prebuilt CPU wheel (≈1 min)…") |
| if paths.PERSISTENT: |
| base = [sys.executable, "-m", "pip", "install", "--prefer-binary", |
| "--target", paths.PYLIBS_DIR] |
| else: |
| base = [sys.executable, "-m", "pip", "install", "--user", "--prefer-binary"] |
| r = subprocess.run(base + ["--extra-index-url", _WHEEL_INDEX, |
| "--only-binary", "llama-cpp-python", _LLAMA_REQ], |
| capture_output=True, text=True, env=env, timeout=600) |
| if r.returncode != 0: |
| _set_stage(worker, "installing llama.cpp runtime", |
| "no prebuilt wheel matched — compiling from source " |
| "(one-time ~10-15 min; other tabs keep working)…") |
| r = subprocess.run(base + ["--extra-index-url", _WHEEL_INDEX, _LLAMA_REQ], |
| capture_output=True, text=True, env=env, timeout=2400) |
| if r.returncode != 0: |
| err = (r.stderr or r.stdout or "")[-800:] |
| _set_stage(worker, "install FAILED", err) |
| return "Could not install llama-cpp-python at runtime:\n" + err |
| import importlib |
| import site |
| cands = [paths.PYLIBS_DIR] if paths.PERSISTENT else [] |
| usp = site.getusersitepackages() |
| cands += usp if isinstance(usp, list) else [usp] |
| for p in cands: |
| if p and p not in sys.path: |
| sys.path.append(p) |
| importlib.invalidate_caches() |
| try: |
| import llama_cpp |
| return "" |
| except Exception as e: |
| _set_stage(worker, "install FAILED", f"installed but import failed: {e}") |
| return f"Installed but import failed: {e}" |
|
|
|
|
| |
| def load_model(name: str, worker: str = "analyst") -> str: |
| worker = _wk(worker) |
| """Load a GGUF into a worker slot. Non-blocking: if that worker is already |
| installing/loading, returns its live stage instead of hanging the click.""" |
| w = WORKERS[worker] |
| if not w["load_lock"].acquire(timeout=2): |
| return (f"⏳ {WORKER_LABEL[worker]} is busy — current stage: " |
| f"**{w['stage']}** ({w['detail'] or '…'}). Press “↻ Refresh status”.") |
| try: |
| if w["llm"] is not None and w["model"] == name: |
| return f"Already loaded on {WORKER_LABEL[worker]}: {name}" |
| err = _ensure_llama_cpp(worker) |
| if err: |
| return err |
| try: |
| from llama_cpp import Llama |
| except Exception as e: |
| _set_stage(worker, "import FAILED", str(e)) |
| return f"llama-cpp-python is not available: {e}" |
| repo, fname = MODEL_ZOO[name] |
| try: |
| _set_stage(worker, "downloading GGUF", |
| f"{repo}/{fname} (cached on /data after first time)") |
| path = hf_hub_download(repo_id=repo, filename=fname) |
| except Exception as e: |
| _set_stage(worker, "download FAILED", str(e)) |
| return f"Could not download {repo}/{fname}: {e}" |
| try: |
| _set_stage(worker, "loading model into RAM", name) |
| w["llm"] = None |
| small = worker != "analyst" |
| w["llm"] = Llama( |
| model_path=path, |
| n_ctx=4096 if small else 6144, |
| n_threads=(4 if small else _NCPU), |
| n_threads_batch=(6 if small else _NCPU), |
| n_batch=512, |
| verbose=False, |
| ) |
| w["model"] = name |
| _set_stage(worker, "ready", name) |
| return f"✅ {WORKER_LABEL[worker]} ready: {name}" |
| except Exception as e: |
| w["llm"] = None |
| _set_stage(worker, "load FAILED", str(e)) |
| return f"Failed to load model: {e}" |
| finally: |
| w["load_lock"].release() |
|
|
|
|
| def auto_load_all(): |
| """Startup: tiny agents first (one small GGUF download serves all three), |
| then the Analyst. Runs in a background thread.""" |
| for key in ("interpreter", "narrator", "reporter", "analyst"): |
| load_model(WORKERS[key]["model"], worker=key) |
|
|
|
|
| |
| def is_loaded(worker: str = None) -> bool: |
| if worker: |
| return WORKERS[_wk(worker)]["llm"] is not None |
| return any(w["llm"] is not None for w in WORKERS.values()) |
|
|
|
|
| def status() -> str: |
| lines = [] |
| for key in ("interpreter", "narrator", "reporter", "analyst"): |
| w = WORKERS[key] |
| label = WORKER_LABEL[key] |
| if w["llm"] is not None: |
| lines.append(f"✅ **{label}** — {w['model']} · llama.cpp, local") |
| elif w["stage"] == "idle": |
| lines.append(f"⚪ **{label}** — not loaded yet (auto-loads at startup)") |
| else: |
| lines.append(f"⏳ **{label}** — {w['stage']} ({w['ts']}): {w['detail'] or '…'}") |
| lines.append("\n_Each sub-agent has its own lock — Explain / narrative / " |
| "research run in parallel without “model busy”._") |
| return "\n\n".join(lines) |
|
|
|
|
| |
| DEFAULT_SYSTEM = ("You are a sub-agent of Chan Compass, a US-equity dashboard. " |
| "Answer in clear, concise English.") |
| MAX_PROMPT_CHARS = 3200 |
|
|
|
|
| def _messages(user: str, system: str): |
| return [{"role": "system", "content": system + " /no_think"}, |
| {"role": "user", "content": user[:MAX_PROMPT_CHARS]}] |
|
|
|
|
| def chat(user: str, max_tokens: int = 500, temperature: float = 0.3, |
| system: str = DEFAULT_SYSTEM, worker: str = "interpreter") -> str: |
| """Blocking chat on one sub-agent (used by pipeline/agent code).""" |
| w = WORKERS[_wk(worker)]; worker = _wk(worker) |
| if w["llm"] is None: |
| return "" |
| if not w["lock"].acquire(timeout=180): |
| return f"({WORKER_LABEL[worker]} busy — try again in a moment)" |
| try: |
| out = w["llm"].create_chat_completion( |
| messages=_messages(user, system), |
| max_tokens=max_tokens, temperature=temperature) |
| txt = out["choices"][0]["message"]["content"] or "" |
| return _THINK_RE.sub("", txt).strip() |
| except Exception as e: |
| return f"(model error: {e})" |
| finally: |
| w["lock"].release() |
|
|
|
|
| def chat_stream(user: str, max_tokens: int = 500, temperature: float = 0.3, |
| system: str = DEFAULT_SYSTEM, worker: str = "interpreter"): |
| """Streaming chat on one sub-agent — yields cumulative text immediately.""" |
| w = WORKERS[_wk(worker)]; worker = _wk(worker) |
| if w["llm"] is None: |
| yield (f"⏳ {WORKER_LABEL[worker]} isn't ready yet — " |
| f"stage: {w['stage']}. Check the **Model** tab.") |
| return |
| if not w["lock"].acquire(timeout=5): |
| yield f"⏳ {WORKER_LABEL[worker]} is finishing another answer — try again in a few seconds." |
| return |
| try: |
| acc = "" |
| for chunk in w["llm"].create_chat_completion( |
| messages=_messages(user, system), |
| max_tokens=max_tokens, temperature=temperature, stream=True): |
| delta = chunk["choices"][0]["delta"].get("content") or "" |
| if not delta: |
| continue |
| acc += delta |
| yield _THINK_RE.sub("", acc).replace("<think>", "").strip() |
| if not acc.strip(): |
| yield "(model returned no text — try again)" |
| except Exception as e: |
| yield f"(model error: {e})" |
| finally: |
| w["lock"].release() |
|
|
|
|
| def quick_test() -> str: |
| """Sanity check both sub-agents.""" |
| import time |
| outs = [] |
| for key in ("interpreter", "narrator", "reporter", "analyst"): |
| if WORKERS[key]["llm"] is None: |
| outs.append(f"{WORKER_LABEL[key]}: not loaded ({WORKERS[key]['stage']})") |
| continue |
| t0 = time.time() |
| out = chat("Reply with exactly: OK", max_tokens=6, temperature=0.0, worker=key) |
| outs.append(f"{WORKER_LABEL[key]}: **{out or '(no output)'}** · {time.time()-t0:.1f}s") |
| return "\n\n".join(outs) |
|
|