From c5042149254fb310836e4885ccf786389809d2b6 Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Sun, 21 Jun 2026 11:02:35 +0000 Subject: [PATCH] Cookbook model workflow fixes --- routes/chat_helpers.py | 37 +- routes/chat_routes.py | 11 +- routes/cookbook_helpers.py | 19 +- routes/cookbook_routes.py | 212 +++- routes/email_helpers.py | 53 +- routes/email_routes.py | 204 +++- routes/hwfit_routes.py | 96 +- routes/model_routes.py | 11 +- routes/shell_routes.py | 58 +- src/agent_loop.py | 182 +++- src/agent_tools/web_tools.py | 44 +- src/builtin_actions.py | 98 +- src/endpoint_resolver.py | 3 + src/llm_core.py | 2 + src/task_endpoint.py | 66 +- src/task_scheduler.py | 45 +- src/tool_implementations.py | 31 +- static/icons/ollama-mark-crop.png | Bin 0 -> 7498 bytes static/icons/ollama-mark.png | Bin 0 -> 7487 bytes static/icons/sglang-logo.png | Bin 0 -> 109100 bytes static/icons/sglang-mark.png | Bin 0 -> 2171 bytes static/index.html | 9 +- static/js/chat.js | 24 +- static/js/compare/index.js | 90 +- static/js/compare/stream.js | 31 +- static/js/compare/vote.js | 2 +- static/js/cookbook-hwfit.js | 12 - static/js/cookbook.js | 295 +++++- static/js/cookbookRunning.js | 79 +- static/js/cookbookServe.js | 935 +++++++++++++++--- static/js/document.js | 159 ++- static/js/emailLibrary.js | 51 +- static/js/markdown.js | 25 +- static/js/modelPicker.js | 21 + static/js/tasks.js | 25 +- static/style.css | 520 +++++++++- tests/test_cookbook_cpu_only_serve.py | 30 + tests/test_copy_message_strips_thinking_js.py | 21 + 38 files changed, 3042 insertions(+), 459 deletions(-) create mode 100644 static/icons/ollama-mark-crop.png create mode 100644 static/icons/ollama-mark.png create mode 100644 static/icons/sglang-logo.png create mode 100644 static/icons/sglang-mark.png diff --git a/routes/chat_helpers.py b/routes/chat_helpers.py index 25f12d566..f030d6c91 100644 --- a/routes/chat_helpers.py +++ b/routes/chat_helpers.py @@ -22,6 +22,31 @@ from fastapi import HTTPException logger = logging.getLogger(__name__) +_CASUAL_OPENING_RE = re.compile( + r"^\s*(?:h+i+|hey+|hello+|yo+|sup+|what'?s up|wass?up|hiya|howdy|" + r"lol|lmao|haha+|hehe+|thanks?|thank you|ty|idk|dunno|meh|bruh|bro)\b(?P.*)$", + re.IGNORECASE, +) +_CASUAL_BLOCKLIST_RE = re.compile( + r"\b(?:cookbook|serve|serving|launch|start|vllm|sglang|llama\.?cpp|ollama|" + r"download|model|email|document|doc|note|calendar|task|search|web|research|" + r"file|folder|repo|git|settings?|endpoint|api|token|mcp)\b", + re.IGNORECASE, +) + + +def _is_casual_low_signal(text: str) -> bool: + """Short greetings/slang should not pull memory, skills, RAG, or docs.""" + s = str(text or "").strip() + m = _CASUAL_OPENING_RE.match(s) + if not m: + return False + tail = m.group("tail") or "" + if _CASUAL_BLOCKLIST_RE.search(tail): + return False + tail_words = re.findall(r"[A-Za-z0-9_'-]+", tail) + return len(tail_words) <= 2 + # ── Data containers ────────────────────────────────────────────────────── # @@ -579,6 +604,7 @@ async def build_chat_context( # Resolve user prefs user = get_current_user(request) uprefs = load_prefs_for_user(user) + casual_low_signal = _is_casual_low_signal(message) # Memory enabled? mem_enabled = not incognito and not no_memory and uprefs.get("memory_enabled", True) @@ -588,6 +614,9 @@ async def build_chat_context( if not allow_tool_preprocessing: mem_enabled = False skills_enabled = False + if casual_low_signal: + mem_enabled = False + skills_enabled = False logger.debug( "Memory enabled=%s for user=%s (incognito=%s, no_memory=%s, pref=%s)", mem_enabled, user, incognito, no_memory, uprefs.get("memory_enabled", "NOT_SET"), @@ -603,11 +632,11 @@ async def build_chat_context( # Use RAG? use_rag_val = (str(use_rag).lower() != "false") if use_rag is not None else True - if incognito or not allow_tool_preprocessing or is_research_spinoff: + if incognito or not allow_tool_preprocessing or is_research_spinoff or casual_low_signal: use_rag_val = False # If pre-fetched search context was provided (compare mode), skip live web search - skip_web = bool(search_context) or not allow_tool_preprocessing + skip_web = bool(search_context) or not allow_tool_preprocessing or casual_low_signal # Build context preface # The stream path uses enhanced_message (with CoT/preprocessing applied), @@ -626,7 +655,7 @@ async def build_chat_context( incognito=incognito, use_skills=skills_enabled, ) - if use_rag is not None or is_research_spinoff: + if use_rag is not None or is_research_spinoff or casual_low_signal: _preface_kwargs["use_rag"] = use_rag_val preface, rag_sources, web_sources = chat_processor.build_context_preface(**_preface_kwargs) @@ -634,7 +663,7 @@ async def build_chat_context( used_memories = getattr(chat_processor, '_last_used_memories', []) # Inject pre-fetched search context (compare mode) - if search_context and allow_tool_preprocessing: + if search_context and allow_tool_preprocessing and not casual_low_signal: preface.append(untrusted_context_message("prefetched search context", search_context)) # YouTube transcripts diff --git a/routes/chat_routes.py b/routes/chat_routes.py index c33f7c2c7..0967667f1 100644 --- a/routes/chat_routes.py +++ b/routes/chat_routes.py @@ -826,7 +826,11 @@ def setup_chat_routes( from src.settings import get_setting _global_disabled = get_setting("disabled_tools", []) if _global_disabled and isinstance(_global_disabled, list): - disabled_tools.update(_global_disabled) + explicit_web_allowed = allow_web_search is not None and str(allow_web_search).lower() == "true" + if explicit_web_allowed: + disabled_tools.update(t for t in _global_disabled if t not in {"web_search", "web_fetch"}) + else: + disabled_tools.update(_global_disabled) # Light auto-escalation: the user is in chat mode and just expressed a # notes/calendar/email intent. Grant the relevant managers but withhold @@ -1256,6 +1260,10 @@ def setup_chat_routes( _max_rounds = _DEFAULT_ROUNDS _max_rounds = max(1, min(_max_rounds, 200)) + _forced_tools = None + if allow_web_search is not None and str(allow_web_search).lower() == "true": + _forced_tools = {"web_search", "web_fetch"} + async for chunk in stream_agent_loop( sess.endpoint_url, sess.model, @@ -1277,6 +1285,7 @@ def setup_chat_routes( plan_mode=plan_mode, approved_plan=approved_plan or None, workspace=workspace or None, + forced_tools=_forced_tools, ): if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"): try: diff --git a/routes/cookbook_helpers.py b/routes/cookbook_helpers.py index 3600a9ad1..bf1124933 100644 --- a/routes/cookbook_helpers.py +++ b/routes/cookbook_helpers.py @@ -964,18 +964,31 @@ def _append_llama_cpp_linux_accel_build_lines(runner_lines: list[str]) -> None: runner_lines.append(' fi # end _odysseus_have_prebuilt guard') -def _llama_cpp_rebuild_cmd() -> str: +def _llama_cpp_rebuild_cmd(update_source: bool = False) -> str: """Shell command that clears the Cookbook-managed llama.cpp build. Removes the cached ``llama-server`` symlink and the ``~/llama.cpp/build*`` directory so the next llama.cpp serve recompiles from source, picking up a CUDA or HIP toolchain if one is now available. The serve bootstrap only builds when ``llama-server`` is missing from PATH, so without this an - existing CPU-only build is reused forever. It deliberately installs and - downloads nothing; the rebuild itself happens on the next serve. + existing CPU-only build is reused forever. When ``update_source`` is true, + the command also fast-forwards the Cookbook-managed ``~/llama.cpp`` checkout + if it exists. The rebuild itself happens on the next serve. """ + update_cmd = '' + if update_source: + update_cmd = ( + 'if [ -d "$HOME/llama.cpp/.git" ]; then ' + 'git -C "$HOME/llama.cpp" pull --ff-only --depth 1 || ' + 'echo "[odysseus] WARNING: llama.cpp source update failed; clearing cached build anyway."; ' + 'elif command -v git >/dev/null 2>&1; then ' + 'git clone --depth 1 https://github.com/ggml-org/llama.cpp "$HOME/llama.cpp" || ' + 'echo "[odysseus] WARNING: llama.cpp clone failed; clearing cached build anyway."; ' + 'fi && ' + ) return ( 'mkdir -p "$HOME/bin" && ' + f'{update_cmd}' 'rm -f "$HOME/bin/llama-server" && ' 'rm -rf "$HOME/llama.cpp/build" "$HOME/llama.cpp/build-vulkan" && ' 'echo "[odysseus] Cleared the cached llama.cpp build. ' diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py index 0bd38d19f..0d8257574 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -273,6 +273,78 @@ def setup_cookbook_routes() -> APIRouter: def _load_stored_hf_token() -> str: return load_stored_hf_token(state_path=_cookbook_state_path) + def _normalize_minimax_m3_vllm_cmd(cmd: str) -> str: + """Patch MiniMax M3 vLLM launches into the known-good local form. + + The browser form can be stale or omit advanced-only fields. MiniMax M3 + is sensitive to several flags: using the HF repo id with block-size 128 + fails KV-cache setup, and FlashInfer sampler JIT fails on this host's + system nvcc. Normalize server-side before writing the tmux runner. + """ + if not cmd or "vllm serve" not in cmd or not re.search(r"minimax.*m3", cmd, re.I): + return cmd + try: + parts = shlex.split(cmd) + except ValueError: + return cmd + if "serve" not in parts: + return cmd + + env_re = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=") + env_parts = [p for p in parts if env_re.match(p)] + body = [p for p in parts if not env_re.match(p)] + try: + serve_i = body.index("serve") + except ValueError: + return cmd + if serve_i + 1 >= len(body): + return cmd + + repo_id = "cyankiwi/MiniMax-M3-AWQ-INT4" + snapshot = ( + "/home/pewds/.cache/huggingface/hub/" + "models--cyankiwi--MiniMax-M3-AWQ-INT4/" + "snapshots/4082acbbec1236d21828d55b6bb0fe02ade4ab5b" + ) + if body[serve_i + 1] == repo_id: + body[serve_i + 1] = snapshot + + def add_env(key: str, value: str) -> None: + if not any(p.startswith(f"{key}=") for p in env_parts): + env_parts.append(f"{key}={value}") + + def has_flag(flag: str) -> bool: + return any(p == flag or p.startswith(flag + "=") for p in body) + + def set_flag(flag: str, value: str) -> None: + for i, part in enumerate(body): + if part == flag: + if i + 1 < len(body): + body[i + 1] = value + else: + body.append(value) + return + if part.startswith(flag + "="): + body[i] = f"{flag}={value}" + return + body.extend([flag, value]) + + def add_bool(flag: str) -> None: + if not has_flag(flag): + body.append(flag) + + add_env("VLLM_TARGET_DEVICE", "cuda") + add_env("VLLM_USE_FLASHINFER_SAMPLER", "0") + set_flag("--served-model-name", repo_id) + set_flag("--tool-call-parser", "minimax_m3") + set_flag("--reasoning-parser", "minimax_m3") + set_flag("--attention-backend", "TRITON_ATTN") + set_flag("--block-size", "128") + add_bool("--language-model-only") + add_bool("--disable-custom-all-reduce") + add_bool("--enable-expert-parallel") + return shlex.join(env_parts + body) + def _cookbook_ssh_dir() -> Path: # The Docker image keeps cookbook keys under /app/.ssh; that path only # exists inside the container. On Windows (and any non-container host) @@ -1249,6 +1321,7 @@ def setup_cookbook_routes() -> APIRouter: # `TypeError: argument of type 'NoneType'` (a 500 instead of a clean 400). req.cmd = _validate_serve_cmd(req.cmd) or "" req.cmd = _normalize_llama_cpp_python_cache_types(req.cmd) or "" + req.cmd = _normalize_minimax_m3_vllm_cmd(req.cmd) req.cmd = _venv_safe_local_pip_install_cmd( req.cmd, local=not bool(req.remote_host), @@ -1579,6 +1652,96 @@ def setup_cookbook_routes() -> APIRouter: runner_lines.append(' echo "ERROR: vLLM is not installed."') runner_lines.append(' ODYSSEUS_PREFLIGHT_EXIT=127') runner_lines.append('fi') + runner_lines.append(f"ODYSSEUS_SERVE_CMD='{_bash_squote(req.cmd)}'") + runner_lines.append('if [ -z "$ODYSSEUS_PREFLIGHT_EXIT" ]; then') + runner_lines.append(' ODYSSEUS_VLLM_HELP_CMD="$(python3 - "$ODYSSEUS_SERVE_CMD" <<\'PY\'') + runner_lines.append('import shlex, sys') + runner_lines.append('parts = shlex.split(sys.argv[1])') + runner_lines.append('try:') + runner_lines.append(' serve_i = parts.index("serve")') + runner_lines.append('except ValueError:') + runner_lines.append(' print("vllm serve --help")') + runner_lines.append('else:') + runner_lines.append(' print(shlex.join(parts[:serve_i + 1] + ["--help"]))') + runner_lines.append('PY') + runner_lines.append(')"') + runner_lines.append(' ODYSSEUS_VLLM_SUPPORTS_SWAP=0') + runner_lines.append(' if eval "$ODYSSEUS_VLLM_HELP_CMD" 2>&1 | grep -q -- "--swap-space"; then ODYSSEUS_VLLM_SUPPORTS_SWAP=1; fi') + runner_lines.append('fi') + runner_lines.append('if [ -z "$ODYSSEUS_PREFLIGHT_EXIT" ] && [ "${ODYSSEUS_VLLM_SUPPORTS_SWAP:-0}" = "1" ] && ! printf "%s" "$ODYSSEUS_SERVE_CMD" | grep -q -- "--swap-space"; then') + runner_lines.append(' echo "[odysseus] Setting vLLM --swap-space 0 so the runtime does not reserve CPU swap per GPU."') + runner_lines.append(' ODYSSEUS_SERVE_CMD="${ODYSSEUS_SERVE_CMD} --swap-space 0"') + runner_lines.append('fi') + runner_lines.append('if [ -z "$ODYSSEUS_PREFLIGHT_EXIT" ] && [ "${ODYSSEUS_VLLM_SUPPORTS_SWAP:-0}" != "1" ]; then') + runner_lines.append(' if printf "%s" "$ODYSSEUS_SERVE_CMD" | grep -q -- "--swap-space"; then') + runner_lines.append(' echo "[odysseus] vLLM serve does not expose --swap-space; removing the flag and patching the runtime default to 0."') + runner_lines.append(' ODYSSEUS_SERVE_CMD="$(python3 - "$ODYSSEUS_SERVE_CMD" <<\'PY\'') + runner_lines.append('import shlex, sys') + runner_lines.append('parts = shlex.split(sys.argv[1])') + runner_lines.append('out = []') + runner_lines.append('skip = False') + runner_lines.append('for part in parts:') + runner_lines.append(' if skip:') + runner_lines.append(' skip = False') + runner_lines.append(' continue') + runner_lines.append(' if part == "--swap-space":') + runner_lines.append(' skip = True') + runner_lines.append(' continue') + runner_lines.append(' if part.startswith("--swap-space="):') + runner_lines.append(' continue') + runner_lines.append(' out.append(part)') + runner_lines.append('print(shlex.join(out))') + runner_lines.append('PY') + runner_lines.append(')"') + runner_lines.append(' fi') + runner_lines.append(' ODYSSEUS_SERVE_CMD="$(python3 - "$ODYSSEUS_SERVE_CMD" <<\'PY\'') + runner_lines.append('import shlex, sys') + runner_lines.append('parts = shlex.split(sys.argv[1])') + runner_lines.append('patch = r"""import inspect, sys') + runner_lines.append('from vllm.engine.arg_utils import EngineArgs, AsyncEngineArgs') + runner_lines.append('def _odysseus_swap0(cls):') + runner_lines.append(' params = list(inspect.signature(cls).parameters)') + runner_lines.append(' if "swap_space" not in params:') + runner_lines.append(' return') + runner_lines.append(' idx = params.index("swap_space")') + runner_lines.append(' defaults = list(cls.__init__.__defaults__ or ())') + runner_lines.append(' if idx < len(defaults):') + runner_lines.append(' defaults[idx] = 0') + runner_lines.append(' cls.__init__.__defaults__ = tuple(defaults)') + runner_lines.append(' fields = getattr(cls, "__dataclass_fields__", {})') + runner_lines.append(' if "swap_space" in fields:') + runner_lines.append(' fields["swap_space"].default = 0') + runner_lines.append('_odysseus_swap0(EngineArgs)') + runner_lines.append('_odysseus_swap0(AsyncEngineArgs)') + runner_lines.append('try:') + runner_lines.append(' from vllm.config import CacheConfig') + runner_lines.append(' CacheConfig.swap_space = 0') + runner_lines.append('except Exception:') + runner_lines.append(' pass') + runner_lines.append('_orig_create_engine_config = EngineArgs.create_engine_config') + runner_lines.append('def _odysseus_create_engine_config(self, *args, **kwargs):') + runner_lines.append(' self.swap_space = 0') + runner_lines.append(' return _orig_create_engine_config(self, *args, **kwargs)') + runner_lines.append('EngineArgs.create_engine_config = _odysseus_create_engine_config') + runner_lines.append('AsyncEngineArgs.create_engine_config = _odysseus_create_engine_config') + runner_lines.append('from vllm.entrypoints.cli.main import main') + runner_lines.append('sys.exit(main())"""') + runner_lines.append('try:') + runner_lines.append(' serve_i = parts.index("serve")') + runner_lines.append('except ValueError:') + runner_lines.append(' print(shlex.join(parts))') + runner_lines.append('else:') + runner_lines.append(' exe_i = serve_i - 1') + runner_lines.append(' exe = parts[exe_i] if exe_i >= 0 else "vllm"') + runner_lines.append(' py = "python3"') + runner_lines.append(' if exe.endswith("/bin/vllm"):') + runner_lines.append(' py = exe[:-len("/bin/vllm")] + "/bin/python"') + runner_lines.append(' parts[exe_i:serve_i] = [py, "-c", patch]') + runner_lines.append(' print(shlex.join(parts))') + runner_lines.append('PY') + runner_lines.append(')"') + runner_lines.append(' echo "[odysseus] Patched vLLM internal swap_space default to 0 for this runtime."') + runner_lines.append('fi') elif "sglang.launch_server" in req.cmd: runner_lines.append('export PATH="$HOME/.local/bin:$PATH"') runner_lines.append('if ! command -v sglang &>/dev/null; then') @@ -1620,7 +1783,10 @@ def setup_cookbook_routes() -> APIRouter: runner_lines, keep_shell_open=not local_windows, ) - runner_lines.append(req.cmd) + if "vllm serve" in req.cmd: + runner_lines.append('eval "$ODYSSEUS_SERVE_CMD"') + else: + runner_lines.append(req.cmd) if local_windows: # Detached background process — no interactive shell to keep open. # Print the exit marker the status poller looks for, then stop. @@ -2418,16 +2584,14 @@ def setup_cookbook_routes() -> APIRouter: # Add 30% headroom for KV cache, activations, etc. needed_vram = (est_vram * 1.3) if est_vram else None - if vram_gb > 0 and needed_vram is not None and needed_vram > vram_gb: - continue - # Unknown-size models (e.g. MiniMax-M2.7, DeepSeek-V4-Flash) have no - # "NB" in the repo id, so the regex above can't extract their - # param count. Previously we dropped them entirely, which made - # brand-new flagship releases silently vanish from this list even - # on rigs with hundreds of GB of VRAM. Adapters/LoRAs are already - # filtered by _is_excluded(), so what falls through here is - # overwhelmingly full models — keep them, just without a size - # badge (the frontend handles needed_vram_gb=null gracefully). + if vram_gb > 0: + if needed_vram is None: + # The "trending models that fit" list must be conservative: + # if we cannot estimate size from the repo id/tags, do not + # present it as runnable on this hardware. + continue + if needed_vram > vram_gb: + continue out.append({ "repo_id": repo_id, @@ -2624,6 +2788,32 @@ def setup_cookbook_routes() -> APIRouter: except Exception as e: logger.warning(f"orphan sweep: state write failed: {e}") + @router.get("/api/cookbook/hf-gguf-files") + async def hf_gguf_files(repo_id: str, owner: str = Depends(require_user)): + """List GGUF files in a HuggingFace repo for the direct-download picker.""" + import httpx + + repo_id = _validate_repo_id(repo_id) + url = f"https://huggingface.co/api/models/{repo_id}" + try: + headers = {} + token = _load_stored_hf_token() + if token: + headers["Authorization"] = f"Bearer {token}" + async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client: + resp = await client.get(url, headers=headers) + if resp.status_code != 200: + return {"ok": False, "files": [], "error": f"HF API HTTP {resp.status_code}"} + data = resp.json() + except Exception as e: + return {"ok": False, "files": [], "error": str(e)} + files = [ + str(s.get("rfilename") or "") + for s in data.get("siblings", []) + if str(s.get("rfilename") or "").lower().endswith(".gguf") + ] + return {"ok": True, "repo_id": repo_id, "files": files} + # In-memory cache for the Ollama library scrape. ollama.com is a public # site, but it doesn't expose a stable JSON listing — we fetch the HTML # search page and regex out the model cards. Cached for 1 h so a busy diff --git a/routes/email_helpers.py b/routes/email_helpers.py index b3df6a560..a054f4df8 100644 --- a/routes/email_helpers.py +++ b/routes/email_helpers.py @@ -1109,22 +1109,30 @@ def _list_attachments_from_msg(msg): return attachments idx = 0 for part in msg.walk(): - if part.is_multipart(): - continue cd = str(part.get("Content-Disposition", "")) ct = part.get_content_type() + is_attached_email = ct == "message/rfc822" and ("attachment" in cd.lower() or part.get_filename()) + if part.is_multipart() and not is_attached_email: + continue # Skip text/html body parts (only consider real attachments) if ct in ("text/plain", "text/html") and "attachment" not in cd: continue filename = part.get_filename() if filename: filename = _decode_header(filename) + if ct == "message/rfc822" and not re.search(r"\.[A-Za-z0-9]{1,8}$", filename): + filename = f"{filename}.eml" else: # Inline images, etc. - generate a name - ext = ct.split("/")[-1] if "/" in ct else "bin" + ext = "eml" if ct == "message/rfc822" else (ct.split("/")[-1] if "/" in ct else "bin") filename = f"attachment_{idx}.{ext}" payload = part.get_payload(decode=True) - size = len(payload) if payload else 0 + if payload is None and ct == "message/rfc822": + try: + payload = part.as_bytes() + except Exception: + payload = b"" + size = len(payload) if payload is not None else 0 attachments.append({ "index": idx, "filename": filename, @@ -1136,29 +1144,58 @@ def _list_attachments_from_msg(msg): return attachments +def _is_likely_signature_image_attachment(att: dict) -> bool: + """Match the reader's inline signature/logo image filter.""" + filename = str((att or {}).get("filename") or "").lower() + if not re.search(r"\.(png|jpe?g|gif|bmp|svg|webp)$", filename): + return False + size = int((att or {}).get("size") or 0) + if re.search(r"^image\d{3,}\.(png|jpe?g|gif)$", filename): + return True + if re.search(r"^(signature|logo|sig|footer|banner)[-_\d]*\.(png|jpe?g|gif|svg)$", filename): + return True + return 0 < size < 30 * 1024 + + +def _has_visible_attachments(msg) -> bool: + """Return True only for attachments the reader will render as chips.""" + return any( + not _is_likely_signature_image_attachment(att) + for att in _list_attachments_from_msg(msg) + ) + + def _extract_attachment_to_disk(msg, index, target_dir): """Extract a specific attachment to disk and return the file path.""" if not msg.is_multipart(): return None idx = 0 for part in msg.walk(): - if part.is_multipart(): - continue cd = str(part.get("Content-Disposition", "")) ct = part.get_content_type() + is_attached_email = ct == "message/rfc822" and ("attachment" in cd.lower() or part.get_filename()) + if part.is_multipart() and not is_attached_email: + continue if ct in ("text/plain", "text/html") and "attachment" not in cd: continue if idx == index: filename = part.get_filename() if filename: filename = _decode_header(filename) + if ct == "message/rfc822" and not re.search(r"\.[A-Za-z0-9]{1,8}$", filename): + filename = f"{filename}.eml" else: - ext = ct.split("/")[-1] if "/" in ct else "bin" + ext = "eml" if ct == "message/rfc822" else (ct.split("/")[-1] if "/" in ct else "bin") filename = f"attachment_{idx}.{ext}" # Sanitize safe_name = re.sub(r"[^\w\s\-.]", "_", filename).strip() payload = part.get_payload(decode=True) - if not payload: + if payload is None and ct == "message/rfc822": + try: + payload = part.as_bytes() + except Exception: + payload = b"" + if payload is None: return None target_dir.mkdir(parents=True, exist_ok=True) filepath = target_dir / safe_name diff --git a/routes/email_routes.py b/routes/email_routes.py index 416ad55b2..81d2b7330 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -44,7 +44,7 @@ from routes.email_helpers import ( _send_smtp_message, _smtp_security_mode, _IMAP_TIMEOUT_SECONDS, _open_imap_connection, _imap_connect, _imap, _decode_header, _detect_sent_folder, _detect_drafts_folder, - _extract_attachment_text, _list_attachments_from_msg, + _extract_attachment_text, _list_attachments_from_msg, _has_visible_attachments, _is_likely_signature_image_attachment, _extract_attachment_to_disk, _extract_html, _extract_text, _fetch_sender_thread_context, _pre_retrieve_context, _EMAIL_REPLY_SYS_PROMPT_BASE, _POOL_HOOKS, @@ -58,6 +58,7 @@ from routes.email_pollers import _start_poller logger = logging.getLogger(__name__) ODYSSEUS_MAIL_ORIGIN = "odysseus-ui" +EMAIL_READ_ATTACHMENT_VERSION = 2 def _email_tag_owner_aliases(account_id: str | None, owner: str = "") -> list[str]: @@ -244,6 +245,21 @@ def _imap_uid_fetch(conn, uid_set: str | bytes, query: str): return conn.uid("FETCH", _uid_bytes(uid_set), query) +def _imap_search_quote(value: str) -> str: + return '"' + str(value or "").replace("\\", "\\\\").replace('"', '\\"') + '"' + + +def _message_id_chain(*values: str) -> list[str]: + seen = set() + out = [] + for value in values: + for mid in re.findall(r"<[^>]+>", value or ""): + if mid not in seen: + seen.add(mid) + out.append(mid) + return out + + def _uid_from_fetch_meta(meta_b: bytes) -> str: m = re.search(rb"\bUID\s+(\d+)\b", meta_b) return m.group(1).decode() if m else "" @@ -1003,6 +1019,65 @@ def setup_email_routes(): except Exception: pass + def _related_thread_attachments_sync( + folder: str, + account_id: str | None, + owner: str, + current_uid: str, + current_message_id: str, + in_reply_to: str, + references: str, + limit: int = 12, + ) -> list[dict]: + """Return visible attachments from referenced messages in this folder.""" + wanted_ids = _message_id_chain(references, in_reply_to) + current_mid = (current_message_id or "").strip() + wanted_ids = [mid for mid in wanted_ids if mid and mid != current_mid] + if not wanted_ids: + return [] + + related: list[dict] = [] + try: + with _imap(account_id, owner=owner) as conn: + conn.select(_q(folder), readonly=True) + # Search newest referenced messages first; cap work so opening + # a long thread stays bounded. + for mid in reversed(wanted_ids[-10:]): + if len(related) >= limit: + break + status, data = _imap_uid_search(conn, f'(HEADER Message-ID {_imap_search_quote(mid)})') + if status != "OK" or not data or not data[0]: + continue + for uid_b in reversed(data[0].split()[-3:]): + source_uid = uid_b.decode(errors="ignore") + if not source_uid or source_uid == str(current_uid): + continue + st2, msg_data = _imap_uid_fetch(conn, source_uid, "(BODY.PEEK[])") + if st2 != "OK" or not msg_data or not isinstance(msg_data[0], tuple): + continue + msg = email_mod.message_from_bytes(msg_data[0][1]) + source_from = _decode_header(msg.get("From", "")) + source_subject = _decode_header(msg.get("Subject", "")) + source_date = msg.get("Date", "") + for att in _list_attachments_from_msg(msg): + if _is_likely_signature_image_attachment(att): + continue + enriched = dict(att) + enriched.update({ + "source_uid": source_uid, + "source_folder": folder, + "source_message_id": (msg.get("Message-ID") or "").strip(), + "source_from": source_from, + "source_subject": source_subject, + "source_date": source_date, + }) + related.append(enriched) + if len(related) >= limit: + break + except Exception as e: + logger.debug(f"related thread attachment lookup failed uid={current_uid}: {e}") + return related + @router.get("/list") async def list_emails( folder: str = Query("INBOX"), @@ -1273,6 +1348,17 @@ def setup_email_routes(): sender_name, sender_addr = email.utils.parseaddr(sender) parsed_date = email.utils.parsedate_to_datetime(date_str) if date_str else None attachments = _list_attachments_from_msg(msg) + related_attachments = [] + if not _has_visible_attachments(msg): + related_attachments = _related_thread_attachments_sync( + folder, + account_id, + owner, + uid, + message_id, + in_reply_to, + references, + ) if mark_seen: # Set \Seen in a separate readwrite session so concurrent reads @@ -1381,6 +1467,8 @@ def setup_email_routes(): "body": body, "body_html": body_html, "attachments": attachments, + "related_attachments": related_attachments, + "attachment_version": EMAIL_READ_ATTACHMENT_VERSION, "cached_summary": cached_summary, "cached_ai_reply": cached_ai_reply, "boundaries": cached_boundaries, @@ -1411,6 +1499,12 @@ def setup_email_routes(): """Read email body. Cached for 30m, sync IMAP work runs in a thread.""" ck = _read_cache_key(account_id, folder, uid, owner=owner) cached = _read_cache_get(ck) + if cached is not None: + # Older cached read responses lack the thread-attachment fallback. + # Fetch once so replies that reference prior attachments can show + # those files without waiting for cache expiry. + if cached.get("attachment_version") != EMAIL_READ_ATTACHMENT_VERSION: + cached = None if cached is not None: if mark_seen: try: @@ -1599,6 +1693,65 @@ def setup_email_routes(): return None doc_session_id = _resolve_doc_session() + def _create_markdown_doc(content: str, summary: str): + from src.database import SessionLocal as _SL, Document as _Doc, DocumentVersion as _DV + doc_id = str(uuid.uuid4()) + ver_id = str(uuid.uuid4()) + _db = _SL() + try: + _db.query(_Doc).filter(_Doc.is_active == True).update({"is_active": False}) + _db.add(_Doc( + id=doc_id, session_id=doc_session_id, title=title, + language="markdown", current_content=content, + version_count=1, is_active=True, + )) + _db.add(_DV( + id=ver_id, document_id=doc_id, version_number=1, + content=content, summary=summary, source="upload", + )) + _db.commit() + finally: + _db.close() + _tag_doc_with_source(doc_id) + return doc_id + + def _attached_email_markdown(path): + raw_bytes = path.read_bytes() + if not raw_bytes: + return f"# Attached email: {base}\n\n_(empty email attachment)_" + try: + attached_msg = email_mod.message_from_bytes(raw_bytes) + except Exception as e: + return f"# Attached email: {base}\n\nCould not parse this email attachment: {e}" + + attached_subject = _decode_header(attached_msg.get("Subject", "")) or base + attached_from = _decode_header(attached_msg.get("From", "")) + attached_to = _decode_header(attached_msg.get("To", "")) + attached_cc = _decode_header(attached_msg.get("Cc", "")) + attached_date = attached_msg.get("Date", "") + attached_body = _extract_text(attached_msg).strip() + attached_atts = _list_attachments_from_msg(attached_msg) + + lines = [f"# Attached email: {attached_subject}", ""] + if attached_from: + lines.append(f"**From:** {attached_from}") + if attached_to: + lines.append(f"**To:** {attached_to}") + if attached_cc: + lines.append(f"**Cc:** {attached_cc}") + if attached_date: + lines.append(f"**Date:** {attached_date}") + lines.extend(["", "## Body", "", attached_body or "_(no readable body)_"]) + if attached_atts: + lines.extend(["", "## Attachments", ""]) + for att in attached_atts: + size = int(att.get("size") or 0) + size_label = f"{size} B" if size < 1024 else f"{round(size / 1024)} KB" + name = att.get("filename") or f"attachment_{att.get('index', '')}" + ctype = att.get("content_type") or "application/octet-stream" + lines.append(f"- {name} ({ctype}, {size_label})") + return "\n".join(lines).strip() + # ── PDF path (existing) ──────────────────────────────────── if ext == ".pdf": import shutil as _shutil @@ -1645,6 +1798,15 @@ def setup_email_routes(): _tag_doc_with_source(doc_id) return {"doc_id": doc_id, "filename": filepath.name} + # ── Attached email (.eml / message/rfc822) ──────────────── + if ext == ".eml": + try: + content = _attached_email_markdown(filepath) + except Exception as e: + return {"error": f"Failed to read email attachment: {e}", "filename": base} + doc_id = _create_markdown_doc(content, "Imported attached email") + return {"doc_id": doc_id, "filename": filepath.name} + # ── DOCX path: extract text → markdown document ─────────── if ext == ".docx": try: @@ -1682,25 +1844,7 @@ def setup_email_routes(): lines.append("") content = "\n".join(lines).strip() or f"_(empty {base})_" - from src.database import SessionLocal as _SL, Document as _Doc, DocumentVersion as _DV - doc_id = str(uuid.uuid4()) - ver_id = str(uuid.uuid4()) - _db = _SL() - try: - _db.query(_Doc).filter(_Doc.is_active == True).update({"is_active": False}) - _db.add(_Doc( - id=doc_id, session_id=doc_session_id, title=title, - language="markdown", current_content=content, - version_count=1, is_active=True, - )) - _db.add(_DV( - id=ver_id, document_id=doc_id, version_number=1, - content=content, summary="Imported from DOCX", source="upload", - )) - _db.commit() - finally: - _db.close() - _tag_doc_with_source(doc_id) + doc_id = _create_markdown_doc(content, "Imported from DOCX") return {"doc_id": doc_id, "filename": filepath.name} # ── Plain text / markdown ──────────────────────────────── @@ -1709,25 +1853,7 @@ def setup_email_routes(): content = filepath.read_text(encoding="utf-8", errors="replace") except Exception as e: return {"error": f"Failed to read text file: {e}", "filename": base} - from src.database import SessionLocal as _SL, Document as _Doc, DocumentVersion as _DV - doc_id = str(uuid.uuid4()) - ver_id = str(uuid.uuid4()) - _db = _SL() - try: - _db.query(_Doc).filter(_Doc.is_active == True).update({"is_active": False}) - _db.add(_Doc( - id=doc_id, session_id=doc_session_id, title=title, - language="markdown", current_content=content, - version_count=1, is_active=True, - )) - _db.add(_DV( - id=ver_id, document_id=doc_id, version_number=1, - content=content, summary="Imported from email attachment", source="upload", - )) - _db.commit() - finally: - _db.close() - _tag_doc_with_source(doc_id) + doc_id = _create_markdown_doc(content, "Imported from email attachment") return {"doc_id": doc_id, "filename": filepath.name} return {"error": f"Unsupported attachment type: {ext}", "filename": base} diff --git a/routes/hwfit_routes.py b/routes/hwfit_routes.py index 5e38b9ca3..0dad0ddd7 100644 --- a/routes/hwfit_routes.py +++ b/routes/hwfit_routes.py @@ -1,8 +1,13 @@ +import json +import os import re +import shlex +import subprocess from copy import deepcopy from fastapi import APIRouter, HTTPException +from core.platform_compat import run_ssh_command from routes._validators import validate_remote_host, validate_ssh_port @@ -107,6 +112,73 @@ def _apply_manual_hardware(system, manual_mode="", manual_gpu_count="", manual_v return system +def _run_model_probe(host: str, ssh_port: str, cmd: str) -> str: + try: + if host: + r = run_ssh_command( + host, + ssh_port or None, + cmd, + timeout=15, + connect_timeout=5, + strict_host_key_checking=False, + text=True, + ) + else: + r = subprocess.run(["bash", "-lc", cmd], capture_output=True, text=True, timeout=15) + if r.returncode == 0: + return (r.stdout or "").strip() + except Exception: + return "" + return "" + + +def _inspect_model_path(model_path: str, host: str = "", ssh_port: str = "") -> dict: + """Read lightweight metadata from a local or SSH-visible HF model folder.""" + path = (model_path or "").strip() + if not path or path.startswith(("http://", "https://")): + return {} + if not (path.startswith("/") or path.startswith("~")): + return {} + + qpath = shlex.quote(path) + qconfig = shlex.quote(os.path.join(path, "config.json")) + out = {} + exists = _run_model_probe(host, ssh_port, f"test -d {qpath} && printf found || printf missing") + if exists != "found": + target = host or "local container" + out["model_probe_error"] = f"Model path is not visible on {target}: {path}" + return out + raw_config = _run_model_probe(host, ssh_port, f"test -f {qconfig} && sed -n '1,240p' {qconfig}") + if raw_config: + try: + cfg = json.loads(raw_config) + except Exception: + cfg = {} + for key in ("context_length", "max_position_embeddings", "n_ctx_train", "model_max_length", "max_seq_len"): + value = cfg.get(key) + if isinstance(value, (int, float)) and value > 0: + out["model_ctx_max"] = int(value) + break + else: + out["model_probe_error"] = f"config.json not found in model path: {path}" + + size_cmd = ( + f"find {qpath} -type f \\( -name '*.safetensors' -o -name '*.bin' -o -name '*.gguf' \\) " + "-printf '%s\\n' 2>/dev/null | awk '{s+=$1} END {if (s>0) printf \"%.6f\", s/1073741824}'" + ) + weights = _run_model_probe(host, ssh_port, size_cmd) + try: + weights_gb = float(weights) + except Exception: + weights_gb = 0.0 + if weights_gb > 0: + out["model_weights_gb"] = round(weights_gb, 3) + elif "model_probe_error" not in out: + out["model_probe_error"] = f"No model weight files found in: {path}" + return out + + def setup_hwfit_routes(): router = APIRouter(prefix="/api/hwfit", tags=["hwfit"]) @@ -235,7 +307,7 @@ def setup_hwfit_routes(): return {"system": system, "models": results} @router.get("/profiles") - def get_serve_profiles(model: str = "", host: str = "", ssh_port: str = "", platform: str = "", fresh: bool = False, serve_weights_gb: float = 0.0, serve_quant: str = ""): + def get_serve_profiles(model: str = "", model_path: str = "", host: str = "", ssh_port: str = "", platform: str = "", fresh: bool = False, serve_weights_gb: float = 0.0, serve_quant: str = ""): """Compute llama.cpp serve profiles (Quality/Balanced/Speed) for `model` against the detected hardware on `host` (or local). Returns concrete flags (n_gpu_layers, n_cpu_moe, cache_type, ctx) the serve UI can apply. @@ -272,8 +344,16 @@ def setup_hwfit_routes(): if nn and (nn == want or want.endswith(nn) or nn.endswith(want)): m = entry break + path_meta = _inspect_model_path(model_path or model, host=host, ssh_port=ssh_port) if m is None: - return {"system": system, "profiles": [], "error": "model not in catalog"} + return { + "system": system, + "profiles": [], + "error": "model not in catalog", + "model_ctx_max": int(path_meta.get("model_ctx_max") or 0), + "model_weights_gb": float(path_meta.get("model_weights_gb") or 0), + "model_probe_error": path_meta.get("model_probe_error") or "", + } # Surface the model's trained context limit so the serve UI can clamp a # user-typed context down to it (asking for ctx > n_ctx_train overflows # and, with a quantized KV cache, can crash the GPU). @@ -283,6 +363,16 @@ def setup_hwfit_routes(): if isinstance(v, (int, float)) and v > 0: model_ctx_max = int(v) break + path_ctx_max = int(path_meta.get("model_ctx_max") or 0) + if path_ctx_max > 0: + model_ctx_max = max(model_ctx_max, path_ctx_max) + model_weights_gb = float(path_meta.get("model_weights_gb") or 0) + if model_weights_gb <= 0: + for k in ("min_vram_gb", "required_gb", "size_gb", "recommended_ram_gb", "min_ram_gb"): + v = m.get(k) + if isinstance(v, (int, float)) and v > 0: + model_weights_gb = float(v) + break return { "system": system, "profiles": compute_serve_profiles( @@ -291,6 +381,8 @@ def setup_hwfit_routes(): serve_quant=(serve_quant or None), ), "model_ctx_max": model_ctx_max, + "model_weights_gb": model_weights_gb, + "model_probe_error": path_meta.get("model_probe_error") or "", } @router.get("/image-models") diff --git a/routes/model_routes.py b/routes/model_routes.py index 000cd9379..69528f6dc 100644 --- a/routes/model_routes.py +++ b/routes/model_routes.py @@ -1064,9 +1064,11 @@ def setup_model_routes(model_discovery): except Exception: return 0.0 - def _failure_delay(fails: int) -> float: + def _failure_delay(fails: int, *, empty_local: bool = False) -> float: if fails <= 0: return 0.0 + if empty_local: + return min(5.0 * (2 ** max(0, fails - 1)), 30.0) return min(_REFRESH_FAILURE_BASE * (2 ** max(0, fails - 1)), _REFRESH_FAILURE_MAX) def _should_refresh_endpoint(ep: Any, now: float, force: bool = False) -> tuple[bool, Dict[str, Any]]: @@ -1097,7 +1099,12 @@ def setup_model_routes(model_discovery): fails = int(state.get("fail_count") or 0) if fails and not force: last_failure = float(state.get("last_failure") or 0.0) - if now - last_failure < _failure_delay(fails): + empty_local = ( + not cached + and category == "local" + and str(getattr(ep, "id", "") or "").startswith("local-") + ) + if now - last_failure < _failure_delay(fails, empty_local=empty_local): return False, info if cached and not force: interval = _endpoint_refresh_interval(ep, category) diff --git a/routes/shell_routes.py b/routes/shell_routes.py index 406d80bb3..245181832 100644 --- a/routes/shell_routes.py +++ b/routes/shell_routes.py @@ -330,6 +330,9 @@ def add_user_install_bins_to_path(): candidates.append(os.path.join(site.USER_BASE, 'bin')) except Exception: pass + candidates.append(os.path.expanduser('~/bin')) + candidates.append(os.path.expanduser('~/llama.cpp/build/bin')) + candidates.append(os.path.expanduser('~/llama.cpp/build-vulkan/bin')) candidates.append(os.path.expanduser('~/.local/bin')) parts = os.environ.get('PATH', '').split(os.pathsep) if os.environ.get('PATH') else [] changed = False @@ -1188,6 +1191,7 @@ def setup_shell_routes() -> APIRouter: # venv over SSH so a remote `pip install` actually reflects here. remote_status: dict = {} remote_details: dict = {} + remote_probe_error = "" remote_names = [ p["name"] for p in packages @@ -1226,8 +1230,34 @@ def setup_shell_routes() -> APIRouter: break except ValueError as e: raise HTTPException(400, str(e)) - except Exception: + except Exception as e: remote_status = {} + remote_probe_error = f"SSH package probe failed: {str(e)[:160]}" + if "llama_cpp" in remote_names: + try: + inner = ( + 'export PATH="$HOME/.local/bin:$HOME/bin:' + '$HOME/llama.cpp/build/bin:$HOME/llama.cpp/build-vulkan/bin:$PATH"; ' + "command -v llama-server 2>/dev/null || true" + ) + argv = _ssh_base_argv(host, ssh_port) + [inner] + proc = await asyncio.create_subprocess_exec( + *argv, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + out, _err = await asyncio.wait_for(proc.communicate(), timeout=8) + llama_server_path = out.decode("utf-8", errors="replace").strip().splitlines() + llama_server_path = llama_server_path[-1].strip() if llama_server_path else "" + if llama_server_path: + remote_status["llama_cpp"] = True + probe = remote_details.setdefault("llama_cpp", {}) + if isinstance(probe, dict): + probe.setdefault("binaries", {})["llama-server"] = llama_server_path + except Exception as e: + if not remote_probe_error: + remote_probe_error = f"SSH llama-server probe failed: {str(e)[:160]}" + pass # Union of system_names + every package's system_prereqs. Probing # the prereqs alongside the main system deps in a single SSH call # avoids a second round-trip per Cookbook → Dependencies refresh. @@ -1272,7 +1302,9 @@ def setup_shell_routes() -> APIRouter: target_os_id = _os_id_from_release("\n".join(_osrel_lines)) except ValueError as e: raise HTTPException(400, str(e)) - except Exception: + except Exception as e: + if not remote_probe_error: + remote_probe_error = f"SSH system probe failed: {str(e)[:160]}" pass elif not host: # Local target — probe in-process so the inline install command @@ -1290,7 +1322,12 @@ def setup_shell_routes() -> APIRouter: on_remote = bool(host and pkg.get("target") == "remote") probe = None if on_remote: - pkg["installed"] = bool(remote_status.get(pkg["name"], False)) + if remote_probe_error and pkg["name"] not in remote_status: + pkg["installed"] = None + pkg["probe_error"] = remote_probe_error + pkg["status_note"] = remote_probe_error + else: + pkg["installed"] = bool(remote_status.get(pkg["name"], False)) probe = remote_details.get(pkg["name"]) if isinstance(probe, dict): pkg["details"] = probe @@ -1353,9 +1390,19 @@ def setup_shell_routes() -> APIRouter: # reads "ready" green while inference runs at 3 tok/s on GPU # silicon — actively misleading. if pkg["name"] == "llama_cpp" and pkg.get("installed"): + _native_llama_server = bool( + isinstance(probe, dict) + and isinstance(probe.get("binaries"), dict) + and probe["binaries"].get("llama-server") + ) _gpu_capable = False _has_nvidia_target = False - if on_remote and host: + if _native_llama_server: + # Native llama-server is the launcher path Cookbook now + # prefers. Do not mark this as a CPU-only Python wheel just + # because llama-cpp-python is absent from the selected venv. + _gpu_capable = True + elif on_remote and host: try: # Activate the configured venv FIRST so the probe # runs against the same python the launch script @@ -1609,7 +1656,8 @@ def setup_shell_routes() -> APIRouter: return {"ok": False, "error": f"Unsupported engine: {engine}"} host = str(body.get("remote_host") or "").strip() ssh_port = body.get("ssh_port") - cmd = _llama_cpp_rebuild_cmd() + update_source = bool(body.get("update_source")) + cmd = _llama_cpp_rebuild_cmd(update_source=update_source) try: argv = ( (_ssh_base_argv(host, ssh_port) + [cmd]) diff --git a/src/agent_loop.py b/src/agent_loop.py index e574a42b6..40e4232bb 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -751,6 +751,17 @@ def _extract_last_user_message(messages: List[Dict]) -> str: _LOW_SIGNAL_RE = re.compile(r"^[\W_]*$", re.UNICODE) +_CASUAL_OPENING_RE = re.compile( + r"^\s*(?:h+i+|hey+|hello+|yo+|sup+|what'?s up|wass?up|hiya|howdy|" + r"lol|lmao|haha+|hehe+|thanks?|thank you|ty|idk|dunno|meh|bruh|bro)\b(?P.*)$", + re.IGNORECASE, +) +_CASUAL_BLOCKLIST_RE = re.compile( + r"\b(?:cookbook|serve|serving|launch|start|vllm|sglang|llama\.?cpp|ollama|" + r"download|model|email|document|doc|note|calendar|task|search|web|research|" + r"file|folder|repo|git|settings?|endpoint|api|token|mcp)\b", + re.IGNORECASE, +) _EXPLICIT_CONTINUATION_RE = re.compile( r"^\s*(?:" r"yes|y|yeah|yep|ok|okay|sure|do it|go ahead|continue|carry on|" @@ -760,6 +771,17 @@ _EXPLICIT_CONTINUATION_RE = re.compile( r")\s*[.!?]*\s*$", re.IGNORECASE, ) +_RETRY_CONTINUATION_RE = re.compile( + r"\b(?:try again|retry|again|rerun|re-run|run it again|launch it again|" + r"start it again|failed|fails?|died|crashed|broke|insta|instantly)\b", + re.IGNORECASE, +) +_COOKBOOK_CONTEXT_RE = re.compile( + r"\b(?:cookbook|serve|serving|served|launch|start|preset|vllm|sglang|" + r"llama\.?cpp|ollama|download|cached models?|model servers?|running models?|" + r"gpu box|ajax|qwen|gemma|llama|mistral|minimax)\b", + re.IGNORECASE, +) def _is_explicit_continuation(text: str) -> bool: @@ -767,6 +789,37 @@ def _is_explicit_continuation(text: str) -> bool: return bool(_EXPLICIT_CONTINUATION_RE.match(str(text or "").strip())) +def _is_casual_low_signal(text: str) -> bool: + """True for short greetings/slang that should not inherit stale context.""" + s = str(text or "").strip() + m = _CASUAL_OPENING_RE.match(s) + if not m: + return False + tail = m.group("tail") or "" + if _CASUAL_BLOCKLIST_RE.search(tail): + return False + # Allow a short vocative/address after the opener without hardcoding the + # address term itself: "hey man", "yo dude", "sup ". Longer tails are + # more likely to be an actual request and should get normal context/tooling. + tail_words = re.findall(r"[A-Za-z0-9_'-]+", tail) + return len(tail_words) <= 2 + + +def _is_contextual_retry_continuation(messages: List[Dict], text: str) -> bool: + """Treat "try again / it failed" as a continuation only for active tool work. + + These follow-ups are common after Cookbook launches: the latest user turn + says only "try again it failed", while the actionable model/host/command + details live one or two turns back. Keep this intentionally narrow so + ordinary chat does not inherit stale Cookbook context. + """ + latest = str(text or "").strip() + if not latest or not _RETRY_CONTINUATION_RE.search(latest): + return False + recent = _recent_context_for_retrieval(messages, max_user=5, max_chars=1200) + return bool(_COOKBOOK_CONTEXT_RE.search(recent)) + + def _assistant_requested_followup(messages: List[Dict]) -> bool: """True when the previous assistant turn asked for missing task details. @@ -808,11 +861,12 @@ def _classify_agent_request(messages: List[Dict], last_user: str) -> Dict[str, o which domain rule packs get appended to the system prompt. """ text = str(last_user or "").strip() - continuation = _is_explicit_continuation(text) or _assistant_requested_followup(messages) + retry_continuation = _is_contextual_retry_continuation(messages, text) + continuation = _is_explicit_continuation(text) or _assistant_requested_followup(messages) or retry_continuation retrieval_query = _recent_context_for_retrieval(messages) if continuation else text q = retrieval_query.lower() - if not text or bool(_LOW_SIGNAL_RE.match(text)): + if not text or bool(_LOW_SIGNAL_RE.match(text)) or _is_casual_low_signal(text): return { "low_signal": True, "continuation": False, @@ -907,6 +961,7 @@ def _build_system_prompt( compact: bool = False, owner: Optional[str] = None, suppress_local_context: bool = False, + suppress_skills: bool = False, active_email: Optional[Dict[str, str]] = None, ) -> List[Dict]: """Build agent system prompt, inject MCP/document context, merge consecutive system msgs.""" @@ -924,7 +979,7 @@ def _build_system_prompt( _ov_sig = _hl.sha256(_json.dumps(get_builtin_overrides() or {}, sort_keys=True).encode()).hexdigest() except Exception: _ov_sig = "" - cache_key = (frozenset(disabled_tools or []), bool(mcp_mgr), needs_admin, _rt_key, compact, _ov_sig, owner, suppress_local_context) + cache_key = (frozenset(disabled_tools or []), bool(mcp_mgr), needs_admin, _rt_key, compact, _ov_sig, owner, suppress_local_context, suppress_skills) if _cached_base_prompt and _cached_base_prompt_key == cache_key and not active_document: agent_prompt = _cached_base_prompt # Skill index is user-editable (name + description), so it must never @@ -934,6 +989,7 @@ def _build_system_prompt( disabled_tools, mcp_mgr, needs_admin, relevant_tools, mcp_disabled_map=mcp_disabled_map, compact=compact, owner=owner, suppress_local_context=suppress_local_context, + suppress_skills=suppress_skills, ) else: agent_prompt, _skill_index_block = _build_base_prompt( @@ -945,6 +1001,7 @@ def _build_system_prompt( compact=compact, owner=owner, suppress_local_context=suppress_local_context, + suppress_skills=suppress_skills, ) if not active_document: _cached_base_prompt = agent_prompt @@ -1228,7 +1285,7 @@ def _build_system_prompt( # few. If the teacher wrote a procedure for "open my X chat" last # time the student failed, this is where the student finds it # before deciding which tool to call. - if not suppress_local_context: + if not suppress_local_context and not suppress_skills: try: last_user = _extract_last_user_message(messages) # Respect the user's skills-enabled toggle (mirrors memory_enabled). @@ -1395,6 +1452,7 @@ def _build_base_prompt( compact: bool = False, owner: Optional[str] = None, suppress_local_context: bool = False, + suppress_skills: bool = False, ): """Build the agent prompt with only relevant tools included. @@ -1447,7 +1505,7 @@ def _build_base_prompt( # The caller wraps it in untrusted_context_message and ships it as a # user-role message — same treatment as the matched-skills block. skill_index_block = "" - if not suppress_local_context: + if not suppress_local_context and not suppress_skills: try: from services.memory.skills import SkillsManager from src.constants import DATA_DIR @@ -1866,6 +1924,7 @@ async def stream_agent_loop( approved_plan: Optional[str] = None, tool_policy: Optional[ToolPolicy] = None, workspace: Optional[str] = None, + forced_tools: Optional[Set[str]] = None, _is_teacher_run: bool = False, ) -> AsyncGenerator[str, None]: """Streaming agent loop generator. @@ -1905,6 +1964,18 @@ async def stream_agent_loop( _needs_admin = _detect_admin_intent(messages) _last_user = _extract_last_user_message(messages) _intent = _classify_agent_request(messages, _last_user) + _low_signal_turn = bool(_intent.get("low_signal")) + _casual_low_signal_turn = _is_casual_low_signal(_last_user) + _direct_low_signal = ( + _low_signal_turn + and not bool(_intent.get("continuation")) + and not plan_mode + and not approved_plan + and (_casual_low_signal_turn or active_document is None) + and (_casual_low_signal_turn or not active_email) + and (_casual_low_signal_turn or not workspace) + and not forced_tools + ) # Tool retrieval uses the latest message by default. It may inherit recent # user turns only for explicit continuations ("yes", "do it", "1"). _retrieval_query = str(_intent.get("retrieval_query") or _last_user) @@ -1912,11 +1983,86 @@ async def stream_agent_loop( "[agent-intent] latest=%r continuation=%s low_signal=%s domains=%s retrieval_query=%r", _last_user[:120], bool(_intent.get("continuation")), - bool(_intent.get("low_signal")), + _low_signal_turn, sorted(_intent.get("domains") or []), _retrieval_query[:200], ) _mcp_disabled_map = _load_mcp_disabled_map() if mcp_mgr else {} + if _direct_low_signal: + logger.info("[agent] direct low-signal reply path for latest=%r", _last_user[:80]) + direct_messages = [{"role": "user", "content": _last_user}] + direct_response = "" + direct_start = time.time() + direct_actual_model = model + real_input_tokens = 0 + real_output_tokens = 0 + try: + async for chunk in stream_llm_with_fallback( + [(endpoint_url, model, headers)] + list(fallbacks or []), + direct_messages, + temperature=temperature, + max_tokens=min(max_tokens or 128, 128), + prompt_type=None, + tools=None, + timeout=int(get_setting("agent_stream_timeout_seconds", 300) or 300), + session_id=session_id, + ): + if chunk.startswith("data: ") and not chunk.startswith("data: [DONE]"): + try: + data = json.loads(chunk[6:]) + except json.JSONDecodeError: + yield chunk + continue + if data.get("type") == "usage": + usage = data.get("data", {}) or {} + direct_actual_model = usage.get("model") or direct_actual_model + real_input_tokens += usage.get("input_tokens", 0) or 0 + real_output_tokens += usage.get("output_tokens", 0) or 0 + continue + if data.get("type") == "model_actual": + direct_actual_model = data.get("model") or direct_actual_model + data["requested_model"] = model + yield f"data: {json.dumps(data)}\n\n" + continue + if data.get("type") == "fallback": + direct_actual_model = data.get("answered_by") or direct_actual_model + yield chunk + continue + if "delta" in data: + if not data.get("thinking"): + direct_response += data.get("delta", "") + yield chunk + continue + yield chunk + elif chunk.startswith("event: "): + yield chunk + except Exception as _direct_err: + logger.warning("[agent] direct low-signal path failed: %s", _direct_err) + fallback = "Hey." + direct_response += fallback + yield f"data: {json.dumps({'delta': fallback})}\n\n" + + if not direct_response.strip(): + fallback = "Hey." + direct_response = fallback + yield f"data: {json.dumps({'delta': fallback})}\n\n" + + duration = time.time() - direct_start + metrics = { + "model": direct_actual_model, + "requested_model": model, + "input_tokens": real_input_tokens or estimate_tokens(direct_messages), + "output_tokens": real_output_tokens or max(len(direct_response) // 4, 1), + "total_time": round(duration, 2), + "response_time": round(duration, 2), + "agent_rounds": 0, + "tool_calls": 0, + "direct_low_signal": True, + } + yield f"data: {json.dumps({'type': 'metrics', 'data': metrics})}\n\n" + yield "data: [DONE]\n\n" + return + if plan_mode and mcp_mgr: # Allow read-only MCP tools to investigate, block write/unknown ones: # hide them from the schemas AND reject them at runtime by qualified name. @@ -1932,7 +2078,7 @@ async def stream_agent_loop( _t1 = time.time() if _relevant_tools: logger.info(f"[tool-rag] Using caller-provided relevant_tools ({len(_relevant_tools)} tools)") - if not guide_only and not _relevant_tools and bool(_intent.get("low_signal")): + if not guide_only and not _relevant_tools and _low_signal_turn: from src.tool_index import ALWAYS_AVAILABLE if workspace: # An active workspace IS the file-work signal: a vague "look at the @@ -2023,6 +2169,15 @@ async def stream_agent_loop( if _relevant_tools is not None and active_document is not None: _relevant_tools.update({"edit_document", "update_document", "suggest_document"}) + # Per-request UI toggles are stronger than retrieval. If the user turns on + # Search, the model must see the search tools even when the latest text is a + # typo or otherwise low-signal for tool RAG. + if not guide_only and forced_tools: + if _relevant_tools is None: + from src.tool_index import ALWAYS_AVAILABLE + _relevant_tools = set(ALWAYS_AVAILABLE) + _relevant_tools.update(t for t in forced_tools if t not in disabled_tools) + # The skill index injected by _build_system_prompt tells the model to # call `manage_skills action=view`, and Jaccard-matched skills are pasted # into the prompt as procedures to follow — but neither path goes through @@ -2030,7 +2185,7 @@ async def stream_agent_loop( # (grep, read_file, ...) that aren't in its schema list. Keep the schemas # in lockstep: manage_skills is callable whenever any skill is indexed, # and a matched skill's declared requires_toolsets ride along with it. - if not guide_only and _relevant_tools is not None: + if not guide_only and _relevant_tools is not None and not _low_signal_turn: try: from services.memory.skills import SkillsManager from src.constants import DATA_DIR @@ -2147,6 +2302,7 @@ async def stream_agent_loop( compact=_compact_agent_prompt, owner=owner, suppress_local_context=guide_only, + suppress_skills=_low_signal_turn, active_email=active_email, ) if plan_mode and not guide_only: @@ -2753,6 +2909,15 @@ async def stream_agent_loop( _intent_nudge_count += 1 _matched_phrase = _intent_match.group(0).strip() logger.info(f"[agent] intent-without-action nudge #{_intent_nudge_count} on round {round_num}: {_matched_phrase!r}") + _lower_phrase = _matched_phrase.lower() + _cookbook_log_hint = "" + if any(_word in _lower_phrase for _word in ("log", "logs", "output", "tail", "status")): + _cookbook_log_hint = ( + " If this is about a Cookbook/model serve, the concrete calls are: " + "`list_served_models` first, then `tail_serve_output` with the " + "session_id from the serve/list result. Never answer with " + "\"check logs\" when those tools are available." + ) messages.append({ "role": "system", "content": ( @@ -2761,6 +2926,7 @@ async def stream_agent_loop( "see you announced the action but didn't run it, which " "is the most frustrating thing you can do. " "DO IT NOW: emit the actual function call this turn. " + f"{_cookbook_log_hint}" "If you decided not to do it after all, say so plainly in " "one sentence instead of restating the plan." ), diff --git a/src/agent_tools/web_tools.py b/src/agent_tools/web_tools.py index 87a4b697f..f1410e18e 100644 --- a/src/agent_tools/web_tools.py +++ b/src/agent_tools/web_tools.py @@ -7,6 +7,7 @@ from src.constants import MAX_OUTPUT_CHARS class WebSearchTool: async def execute(self, content: str, ctx: dict) -> dict: from src.search import comprehensive_web_search + progress_cb = ctx.get("progress_cb") if isinstance(ctx, dict) else None raw = content.strip() query = raw time_filter = None @@ -37,18 +38,39 @@ class WebSearchTool: elif " news" in q_lc or q_lc.startswith("news ") or q_lc.endswith(" news"): time_filter = "week" loop = asyncio.get_running_loop() - text, sources = await asyncio.wait_for( - loop.run_in_executor( - None, - lambda: comprehensive_web_search( - query, - max_pages=max_pages, - time_filter=time_filter, - return_sources=True, + if progress_cb: + await progress_cb({ + "elapsed_s": 0, + "tail": f"Searching web for: {query[:160]}", + }) + try: + text, sources = await asyncio.wait_for( + loop.run_in_executor( + None, + lambda: comprehensive_web_search( + query, + max_pages=max_pages, + time_filter=time_filter, + return_sources=True, + ), ), - ), - timeout=30, - ) + timeout=30, + ) + except asyncio.TimeoutError: + return { + "error": f"web_search timed out after 30s: {query[:200]}", + "exit_code": 1, + } + except Exception as e: + return { + "error": f"web_search failed: {type(e).__name__}: {str(e) or 'no details'}", + "exit_code": 1, + } + if progress_cb: + await progress_cb({ + "elapsed_s": 30, + "tail": "Search completed; preparing sources.", + }) output = text[:MAX_OUTPUT_CHARS] if len(text) > MAX_OUTPUT_CHARS else text if sources: output += "\n\n" diff --git a/src/builtin_actions.py b/src/builtin_actions.py index a598cb652..bf4ddd950 100644 --- a/src/builtin_actions.py +++ b/src/builtin_actions.py @@ -76,8 +76,7 @@ async def action_consolidate_memory(owner: str, **kwargs) -> Tuple[str, bool]: import json import re from src.constants import DATA_DIR - from src.endpoint_resolver import resolve_endpoint - from src.llm_core import llm_call_async + from src.llm_core import llm_call_async_with_fallback from src.memory import MemoryManager manager = MemoryManager(DATA_DIR) @@ -116,10 +115,9 @@ async def action_consolidate_memory(owner: str, **kwargs) -> Tuple[str, bool]: if len(group_memories) < 2: return False - url, model, headers = resolve_endpoint("utility", owner=group_owner or None) - if not url or not model: - url, model, headers = resolve_endpoint("default", owner=group_owner or None) - if not url or not model: + from src.task_endpoint import resolve_task_candidates + candidates = resolve_task_candidates(owner=group_owner or None) + if not candidates: return False try: @@ -147,13 +145,11 @@ async def action_consolidate_memory(owner: str, **kwargs) -> Tuple[str, bool]: "\"drop\":[{\"id\":\"existing id\",\"reason\":\"short reason\"}]}\n\n" f"MEMORIES:\n{json.dumps(items, ensure_ascii=False)}" ) - raw = await llm_call_async( - url=url, - model=model, + raw = await llm_call_async_with_fallback( + candidates, messages=[{"role": "user", "content": prompt}], temperature=0.0, max_tokens=4096, - headers=headers, timeout=120, ) from src.text_helpers import strip_think @@ -604,8 +600,7 @@ async def action_classify_events(owner: str, **kwargs) -> Tuple[str, bool]: try: from datetime import timedelta from core.database import SessionLocal, CalendarEvent - from src.endpoint_resolver import resolve_endpoint - from src.llm_core import llm_call_async + from src.llm_core import llm_call_async_with_fallback import re as _re, json as _json db = SessionLocal() @@ -620,10 +615,9 @@ async def action_classify_events(owner: str, **kwargs) -> Tuple[str, bool]: if not events: return "No upcoming events to classify", True - llm_url, llm_model, llm_headers = resolve_endpoint("utility", owner=owner) - if not llm_url: - llm_url, llm_model, llm_headers = resolve_endpoint("default", owner=owner) - llm_available = bool(llm_url and llm_model) + from src.task_endpoint import resolve_task_candidates + llm_candidates = resolve_task_candidates(owner=owner) + llm_available = bool(llm_candidates) # Pull user memories so the LLM has personal context (relationships, # job, hobbies). Helps it know e.g. " is your spouse" so their @@ -699,11 +693,11 @@ async def action_classify_events(owner: str, **kwargs) -> Tuple[str, bool]: f"EVENTS: {_json.dumps(items)}" ) try: - raw = await llm_call_async( - url=llm_url, model=llm_model, + raw = await llm_call_async_with_fallback( + llm_candidates, messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=16384, - headers=llm_headers, timeout=180, + timeout=180, ) from src.text_helpers import strip_think as _st raw = _st(raw or "", prose=False, prompt_echo=False) @@ -810,8 +804,7 @@ async def action_learn_sender_signatures(owner: str, **kwargs) -> Tuple[str, boo import asyncio as _aio from datetime import datetime as _dt, timedelta as _td from routes.email_helpers import _email_cache_owner_clause, _imap_connect, SCHEDULED_DB - from src.endpoint_resolver import resolve_endpoint - from src.llm_core import llm_call_async + from src.llm_core import llm_call_async_with_fallback # 1. Pull recent UIDs + From headers cheaply (header-only fetch). def _pull_headers(): @@ -891,11 +884,11 @@ async def action_learn_sender_signatures(owner: str, **kwargs) -> Tuple[str, boo if not eligible: return "All sender sigs already cached (or no eligible senders)", True - url, model, headers = resolve_endpoint("utility", owner=owner) - if not url or not model: - url, model, headers = resolve_endpoint("default", owner=owner) - if not url or not model: + from src.task_endpoint import resolve_task_candidates + candidates = resolve_task_candidates(owner=owner) + if not candidates: return "No LLM endpoint available", False + model = candidates[0][1] analyzed = 0 no_sig = 0 @@ -949,11 +942,11 @@ async def action_learn_sender_signatures(owner: str, **kwargs) -> Tuple[str, boo ) try: - raw = await llm_call_async( - url=url, model=model, + raw = await llm_call_async_with_fallback( + candidates, messages=[{"role": "user", "content": prompt}], temperature=0.0, max_tokens=600, - headers=headers, timeout=60, + timeout=60, ) from src.text_helpers import strip_think as _st sig = _st(raw or "", prose=False, prompt_echo=False).strip() @@ -1137,7 +1130,6 @@ async def action_test_skills(owner: str, **kwargs) -> Tuple[str, bool]: from services.memory.skills import SkillsManager from src.constants import DATA_DIR from routes.skills_routes import _run_skill_test_once, _skill_test_task - from src.endpoint_resolver import resolve_endpoint # #3 SCOPE GUARD: refuse to run on a None/empty owner — otherwise # `sm.load(owner=None)` returns every user's skills and we'd cross- @@ -1152,27 +1144,40 @@ async def action_test_skills(owner: str, **kwargs) -> Tuple[str, bool]: if not names: raise TaskNoop("no skills to test") - url, model, headers = resolve_endpoint("default", owner=owner) - if not url or not model: + from src.task_endpoint import resolve_task_candidates + candidates = resolve_task_candidates(owner=owner) + if not candidates: return "No Default/Utility model configured — set one in Settings.", False # #2 NO SILENT MODEL SWAP: if the configured model isn't served by the # endpoint, try a basename match — but fail loudly instead of grabbing # `avail[0]` which could be an embedding-only model and produce 36 # garbage transcripts → 36 'unknown' verdicts with no hint why. + url, model, headers = candidates[0] try: from src.llm_core import list_model_ids - avail = list_model_ids(url, headers=headers) - if avail and model not in avail: - import os as _os - base = _os.path.basename((model or "").rstrip("/")) - m = next((a for a in avail if _os.path.basename(a.rstrip("/")) == base), None) - if m: - model = m - else: - return (f"Default model '{model}' not served by endpoint {url}. " - f"Available: {', '.join(avail[:8])}{'…' if len(avail) > 8 else ''}. " - "Set a valid Default model in Settings."), False + import os as _os + + selected = None + mismatch_notes = [] + for cand_url, cand_model, cand_headers in candidates: + avail = list_model_ids(cand_url, headers=cand_headers) + if not avail or cand_model in avail: + selected = (cand_url, cand_model, cand_headers) + break + base = _os.path.basename((cand_model or "").rstrip("/")) + matched = next((a for a in avail if _os.path.basename(a.rstrip("/")) == base), None) + if matched: + selected = (cand_url, matched, cand_headers) + break + mismatch_notes.append( + f"{cand_model} not served by {cand_url}; available: " + f"{', '.join(avail[:8])}{'...' if len(avail) > 8 else ''}" + ) + if selected: + url, model, headers = selected + elif mismatch_notes: + return "No configured task fallback model is served. " + " | ".join(mismatch_notes[:3]), False except Exception as _e: logger.warning(f"test_skills model resolve check failed (continuing): {_e}") @@ -1483,7 +1488,6 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: from pathlib import Path as _P from core.database import SessionLocal as _SL, EmailAccount as _EA from routes.email_helpers import _imap_connect, _decode_header - from src.endpoint_resolver import resolve_endpoint, resolve_utility_fallback_candidates from src.llm_core import llm_call_async_with_fallback # Per-owner state file so multi-user runs don't clobber each other's @@ -1505,12 +1509,10 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: # ── 1. Resolve LLM candidates (utility primary + utility fallbacks; fall # through to default chat as a last resort). - url, model, headers = resolve_endpoint("utility", owner=owner) - if not url or not model: - url, model, headers = resolve_endpoint("default", owner=owner) - if not url or not model: + from src.task_endpoint import resolve_task_candidates + candidates = resolve_task_candidates(owner=owner) + if not candidates: return "No LLM endpoint available", False - candidates = [(url, model, headers)] + resolve_utility_fallback_candidates(owner=owner) # ── 2. Enumerate enabled accounts. Match this task's owner AND fall # back to the legacy "unowned account whose imap_user / from_address diff --git a/src/endpoint_resolver.py b/src/endpoint_resolver.py index 34d451d9c..ac5a6b7ad 100644 --- a/src/endpoint_resolver.py +++ b/src/endpoint_resolver.py @@ -396,6 +396,9 @@ def resolve_utility_fallback_candidates(owner: Optional[str] = None) -> list: settings = load_settings() utility_ep = (get_user_setting("utility_endpoint_id", owner or "", settings.get("utility_endpoint_id", "")) or "").strip() if not utility_ep: + utility_chain = get_user_setting("utility_model_fallbacks", owner or "", settings.get("utility_model_fallbacks") or []) or [] + if utility_chain: + return _resolve_fallback_candidates("utility_model_fallbacks", owner=owner) return _resolve_fallback_candidates("default_model_fallbacks", owner=owner) except Exception: pass diff --git a/src/llm_core.py b/src/llm_core.py index 1338ef91a..ba567ed81 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -2130,6 +2130,8 @@ async def stream_llm(url: str, model: str, messages: List[Dict], temperature: fl yield _stream_delta_event(reasoning, thinking=True) content = delta.get("content") or "" if content: + content = re.sub(r"]*)?>", r"", content, flags=re.IGNORECASE) + content = re.sub(r"", "", content, flags=re.IGNORECASE) stripped = content.lstrip() # gpt-oss harmony format (<|channel|>analysis/final): route via the harmony # stream router. Sticky once the first marker appears — distinct from the diff --git a/src/task_endpoint.py b/src/task_endpoint.py index 6e477a3ec..6f9a27c09 100644 --- a/src/task_endpoint.py +++ b/src/task_endpoint.py @@ -1,6 +1,11 @@ -"""Shared resolver for background-task AI endpoint (auto-naming, memory, sorting).""" +"""Shared resolver for background-task AI endpoints.""" -from src.endpoint_resolver import resolve_endpoint +from src.endpoint_resolver import ( + resolve_chat_fallback_candidates, + resolve_endpoint, + resolve_utility_fallback_candidates, +) +from src.llm_core import llm_call_async_with_fallback def resolve_task_endpoint(fallback_url=None, fallback_model=None, fallback_headers=None, owner=None): @@ -11,3 +16,60 @@ def resolve_task_endpoint(fallback_url=None, fallback_model=None, fallback_heade endpoint cannot be resolved. """ return resolve_endpoint("task", fallback_url, fallback_model, fallback_headers, owner=owner) + + +def resolve_task_candidates( + fallback_url=None, + fallback_model=None, + fallback_headers=None, + owner=None, +): + """Return ordered background-task LLM candidates. + + Order: + 1. configured Background Tasks endpoint/model, or caller fallback + 2. Utility endpoint/model + 3. Default endpoint/model + 4. Utility fallback chain + 5. Default fallback chain + """ + candidates = [] + + def _append(url, model, headers): + if not url or not model: + return + key = (url, model) + if any((u, m) == key for u, m, _ in candidates): + return + candidates.append((url, model, headers or {})) + + _append(*resolve_task_endpoint(fallback_url, fallback_model, fallback_headers, owner=owner)) + _append(*resolve_endpoint("utility", owner=owner)) + _append(*resolve_endpoint("default", owner=owner)) + for url, model, headers in resolve_utility_fallback_candidates(owner=owner): + _append(url, model, headers) + for url, model, headers in resolve_chat_fallback_candidates(owner=owner): + _append(url, model, headers) + + return candidates + + +async def task_llm_call_async( + messages, + *, + fallback_url=None, + fallback_model=None, + fallback_headers=None, + owner=None, + **kwargs, +): + """Call the shared background-task LLM candidate chain.""" + candidates = resolve_task_candidates( + fallback_url=fallback_url, + fallback_model=fallback_model, + fallback_headers=fallback_headers, + owner=owner, + ) + if not candidates: + raise RuntimeError("No LLM endpoint available for background task") + return await llm_call_async_with_fallback(candidates, messages=messages, **kwargs) diff --git a/src/task_scheduler.py b/src/task_scheduler.py index 6c8ab148a..b9ff51b6b 100644 --- a/src/task_scheduler.py +++ b/src/task_scheduler.py @@ -833,6 +833,14 @@ class TaskScheduler: owner=task.owner, body=run.result if output == "notification" else None, ) + elif run.status == "error": + self.add_notification( + task.name, + "error", + task_id, + owner=task.owner, + body=run.error or run.result, + ) # Log result to the assistant chat so all task activity is visible. # Skip skipped/error rows — user shouldn't see "skipped: …" noise @@ -1406,12 +1414,18 @@ class TaskScheduler: ) except Exception as e: logger.warning(f"Agent loop failed for task '{task.name}', falling back to simple call: {e}") - from src.llm_core import llm_call_async + from src.task_endpoint import task_llm_call_async messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": task.prompt}, ] - result = await llm_call_async(url=endpoint_url, model=model, messages=messages, timeout=120) + result = await task_llm_call_async( + messages, + fallback_url=endpoint_url, + fallback_model=model, + owner=task.owner, + timeout=120, + ) # Strip the model's chain-of-thought before saving/delivering. Task # output is LLM-only, so prose=True (which also removes untagged @@ -1636,13 +1650,17 @@ class TaskScheduler: # Honor per-task max_steps (defense against runaway agent loops). # Falls back to 20 if not set — the historical default. _task_max_rounds = task.max_steps if task.max_steps and task.max_steps > 0 else 20 - # Tasks are background workloads — they share the Utility model's - # fallback chain (Settings → Utility Model → Fallbacks). A downed - # primary endpoint won't silently yield `(no output)` — same recipe - # chat uses but with the utility list (`utility_model_fallbacks`). + # Tasks are background workloads: use the shared task fallback chain + # behind the primary endpoint so a downed primary won't silently yield + # `(no output)`. try: - from src.endpoint_resolver import resolve_utility_fallback_candidates - _task_fallbacks = resolve_utility_fallback_candidates(owner=task.owner or None) + from src.task_endpoint import resolve_task_candidates + _task_fallbacks = resolve_task_candidates( + fallback_url=endpoint_url, + fallback_model=model, + fallback_headers=headers, + owner=task.owner or None, + )[1:] except Exception: _task_fallbacks = [] async for event_str in stream_agent_loop( @@ -1679,21 +1697,22 @@ class TaskScheduler: # asking it to summarize what it did. Guarantees output. if not full_text.strip(): try: - from src.llm_core import llm_call_async_with_fallback - from src.endpoint_resolver import resolve_utility_fallback_candidates + from src.task_endpoint import task_llm_call_async grace_context = "You ran out of steps. " if tool_results: grace_context += "Here's what your tools returned:\n" + "\n".join(tool_results[-5:]) else: grace_context += "No tool results were captured." grace_context += "\n\nSummarize what you accomplished and what's still pending. Be concise." - _grace_candidates = [(endpoint_url, model, headers)] + resolve_utility_fallback_candidates(owner=task.owner or None) - full_text = await llm_call_async_with_fallback( - _grace_candidates, + full_text = await task_llm_call_async( messages=[ {"role": "system", "content": system_content}, {"role": "user", "content": grace_context}, ], + fallback_url=endpoint_url, + fallback_model=model, + fallback_headers=headers, + owner=task.owner or None, timeout=30, ) full_text = (full_text or "").strip() diff --git a/src/tool_implementations.py b/src/tool_implementations.py index ae7246ec6..f1ac33007 100644 --- a/src/tool_implementations.py +++ b/src/tool_implementations.py @@ -1119,8 +1119,8 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict: _ALIASES = { "shell": ["bash"], "terminal": ["bash"], - "search": ["web_search"], - "web": ["web_search"], + "search": ["web_search", "web_fetch"], + "web": ["web_search", "web_fetch"], "browser": ["builtin_browser"], "documents": ["create_document", "edit_document", "update_document", "suggest_document"], "doc": ["create_document", "edit_document", "update_document", "suggest_document"], @@ -1132,7 +1132,7 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict: "notes": ["manage_notes"], "calendar": ["manage_calendar"], "email": ["mcp__email__list_emails", "mcp__email__read_email", "mcp__email__send_email"], - "research": ["web_search"], # research is a per-request flag, not a tool — closest analog + "research": ["web_search", "web_fetch"], # research is a per-request flag, not a tool — closest analog } if action == "list_tools": @@ -2714,13 +2714,25 @@ async def do_serve_model(content: str, owner: Optional[str] = None) -> Dict: endpoint_added=endpoint_added, endpoint_id=endpoint_id or "", ) note = "" if registered else " (state-write failed — task may not show in UI)" + where = host or "local" + log_path = f"/tmp/odysseus-tmux/{sid}.log" return { - "output": f"Serving {repo_id} (session: {sid}){note}", + "output": ( + f"Serving {repo_id} on {where} (session: {sid}){note}\n" + f"Next required check: call list_served_models. If this task is not ready, " + f"call tail_serve_output with session_id={sid} and tail=400 before answering. " + f"Do not tell the user to check logs; you have the log tool." + ), "session_id": sid, "task_type": "serve", "phase": "running", "host": host, "endpoint_id": endpoint_id, + "log_path": log_path, + "next_tools": [ + {"name": "list_served_models", "arguments": {}}, + {"name": "tail_serve_output", "arguments": {"session_id": sid, "tail": 400}}, + ], "exit_code": 0, } # FastAPI HTTPException puts the message under `detail`, not `error`. @@ -3057,8 +3069,17 @@ async def do_tail_serve_output(content: str, owner: Optional[str] = None) -> Dic MAX_CHARS = 8000 if len(output_text) > MAX_CHARS: output_text = "…(earlier output truncated)…\n" + output_text[-MAX_CHARS:] + if not output_text: + output_text = ( + f"No log output captured yet for {session_id} on {host_label}. " + "This usually means the tmux wrapper has started but the model process " + "has not printed anything yet. Do not stop here: call list_served_models " + "again to check whether it is still loading, ready, or crashed; if it is " + "still not ready, call tail_serve_output again with a larger tail after " + "the next status check." + ) return { - "output": output_text or "(empty pane)", + "output": output_text, "session_id": session_id, "host": host_label, "tail_lines": tail, diff --git a/static/icons/ollama-mark-crop.png b/static/icons/ollama-mark-crop.png new file mode 100644 index 0000000000000000000000000000000000000000..2554b38a14bf90287c29b2fcdadd2705c87fa148 GIT binary patch literal 7498 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
Hi3b2#_y5_ho<%06=vJU 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 A=>Px# literal 0 HcmV?d00001 diff --git a/static/index.html b/static/index.html index 89b0ebb34..e0e84bd0b 100644 --- a/static/index.html +++ b/static/index.html @@ -879,7 +879,7 @@ Library
@@ -1005,7 +1005,12 @@ `; // /#cookbook-dl-tab-fold-body (whole Download card body) @@ -2884,6 +3072,7 @@ const shared = { _getPort, _sshPrefix, _serverByVal, + _serverKey, _selectedServer, _getPlatform, _isWindows, diff --git a/static/js/cookbookRunning.js b/static/js/cookbookRunning.js index a390e11ad..7672edfd2 100644 --- a/static/js/cookbookRunning.js +++ b/static/js/cookbookRunning.js @@ -27,6 +27,9 @@ function _statusLabel(status, type) { // "cookbook-task-status" ('' = the neutral loading style). function _taskBadge(task) { if (task._unreachable && task.status === 'running') return { text: 'unreachable', cls: 'cookbook-task-error' }; + if (task.type === 'download' && task.status === 'running') { + return { text: _statusLabel(task.status, task.type), cls: 'cookbook-task-downloading' }; + } if (task.type === 'serve' && task.status === 'running' && task.progress) { // Same green "running" pill — just with dynamic phase text, so it doesn't // read as a different status while the server is coming up. @@ -52,13 +55,13 @@ function _downloadOutputLooksActive(task) { function _canClearTask(task) { if (!task || task.status === 'running') return false; - if (task.type === 'serve' && (task.status === 'ready' || task._serveReady)) return false; + if (task.type === 'serve' && (task.status === 'ready' || (task._serveReady && !['stopped', 'error', 'crashed', 'failed', 'completed'].includes(task.status)))) return false; // If the tmux output still shows an in-flight download, the task isn't // actually finished — hide the clear/check pill so it doesn't show on a // task that's still doing work. (The next render will reflect this and // ideally the self-heal flips status back to running.) if (_downloadOutputLooksActive(task)) return false; - return ['done', 'stopped', 'error', 'crashed', 'failed'].includes(task.status); + return ['done', 'completed', 'stopped', 'error', 'crashed', 'failed'].includes(task.status); } function _clearPillLabel(task) { @@ -66,6 +69,13 @@ function _clearPillLabel(task) { return 'clear'; } +function _venvRootFromPath(path) { + let p = (path || '').toString().trim().replace(/\/+$/, ''); + if (!p) return ''; + p = p.replace(/\/bin\/(?:activate|python(?:3(?:\.\d+)?)?|vllm|pip(?:3)?)$/i, ''); + return p; +} + // A pip dependency/driver install (payload._dep) reports success with the // runner's "=== Process exited with code 0 ===" sentinel and pip's // "Successfully installed" line — never the HuggingFace download markers @@ -263,6 +273,7 @@ let _copyText; let _persistEnvState; let _refreshDependencies; let _serverByVal; +let _serverKey; let _selectedServer; let modelLogo; let esc; @@ -688,8 +699,10 @@ export function _saveTasks(tasks) { export function _addTask(sessionId, name, type, payload) { let tasks = _loadTasks(); const remoteHost = (payload && payload.remote_host) || _envState.remoteHost || ''; - const sshPort = (payload && payload.ssh_port) || _getPort(remoteHost) || ''; - const platform = (payload && payload.platform) || _getPlatform(remoteHost) || ''; + const remoteServerKey = (payload && payload.remote_server_key) || ''; + const remoteServerName = (payload && payload.remote_server_name) || ''; + const sshPort = (payload && payload.ssh_port) || _getPort(remoteServerKey || remoteHost) || ''; + const platform = (payload && payload.platform) || _getPlatform(remoteServerKey || remoteHost) || ''; // Serving a model supersedes its finished download — clear the matching // finished download card (covers serving directly from the Serve tab, not just // via the download card's "Serve →" button). @@ -704,7 +717,7 @@ export function _addTask(sessionId, name, type, payload) { return !(key && t.type === 'download' && t.status === 'queued' && _downloadDedupeKey(t) === key); }); } - const task = _stripTaskSecrets({ id: sessionId, sessionId, name, type, status: 'running', output: '', ts: Date.now(), payload: payload || null, remoteHost, sshPort, platform }); + const task = _stripTaskSecrets({ id: sessionId, sessionId, name, type, status: 'running', output: '', ts: Date.now(), payload: payload || null, remoteHost, remoteServerKey, remoteServerName, sshPort, platform }); tasks.push(task); _saveTasks(tasks); // New action → collapse all other cards, leave only this one open. @@ -1520,14 +1533,18 @@ function _parseServeCmdToFields(cmd) { return fields; } -export async function _launchServeTask(shortName, repo, cmd, fields, hostOverride) { +export async function _launchServeTask(shortName, repo, cmd, fields, hostOverride, targetMeta = null) { // Host resolution mirrors the download path: when the caller passes an explicit // host (resolved from the dropdown the user actually picked), use it and look // up that server's port/platform from the shared servers list. Only fall back // to _envState.remoteHost for legacy callers (diagnosis/pip-update). const _host = (hostOverride !== undefined) ? (hostOverride || '') : (_envState.remoteHost || ''); - const _hsrv = _serverByVal(_envState.remoteServerKey || _host) + const _targetKey = targetMeta?.serverKey || ''; + const _hsrv = (_targetKey && _targetKey !== 'local' ? _serverByVal(_targetKey) : null) + || (hostOverride === undefined ? _serverByVal(_envState.remoteServerKey || _host) : null) || _envState.servers.find(s => s.host === _host) || {}; + const _serverMetaKey = _targetKey || (_hsrv && _serverKey ? _serverKey(_hsrv) : '') || (_host || 'local'); + const _serverMetaName = targetMeta?.serverName || _hsrv.name || (_host ? _host : 'Local'); const _hplatform = _host ? (_hsrv.platform || '') : (_envState.platform || ''); // Replace any serve already targeting this same host:port — you can't run two @@ -1572,7 +1589,7 @@ export async function _launchServeTask(shortName, repo, cmd, fields, hostOverrid } } else { if (_envState.env === 'venv' && _envState.envPath) { - const p = _envState.envPath; + const p = _venvRootFromPath(_envState.envPath); envPrefix = 'source ' + (p.endsWith('/bin/activate') ? p : p + '/bin/activate'); } else if (_envState.env === 'conda' && _envState.envPath) { envPrefix = 'eval "$(conda shell.bash hook)" && conda activate ' + _envState.envPath; @@ -1583,7 +1600,7 @@ export async function _launchServeTask(shortName, repo, cmd, fields, hostOverrid repo_id: repo, cmd: cmd, remote_host: _host || undefined, - ssh_port: _getPort(_host) || undefined, + ssh_port: _getPort(_serverMetaKey || _host) || undefined, env_prefix: envPrefix || undefined, hf_token: _envState.hfToken || undefined, gpus: _envState.gpus || undefined, @@ -1607,11 +1624,11 @@ export async function _launchServeTask(shortName, repo, cmd, fields, hostOverrid return; } - const _sp = _getPort(_host); + const _sp = _getPort(_serverMetaKey || _host); // _fields = the exact structured serve-form values used for this launch, // so the "Edit / relaunch" button can re-open the Serve panel pre-filled // with these precise settings (not just the last-used-for-repo state). - const payload = { repo_id: repo, remote_host: _host || undefined, ssh_port: _sp || undefined, _cmd: cmd, _fields: fields || undefined, _env: _usedEnv, _envPath: _usedEnvPath, _gpus: _usedGpus }; + const payload = { repo_id: repo, remote_host: _host || undefined, remote_server_key: _serverMetaKey || undefined, remote_server_name: _serverMetaName || undefined, ssh_port: _sp || undefined, _cmd: cmd, _fields: fields || undefined, _env: _usedEnv, _envPath: _usedEnvPath, _gpus: _usedGpus }; _addTask(data.session_id, shortName, 'serve', payload); uiModule.showToast(`Serving ${shortName}...`); // Auto-register may have enabled an existing (offline) endpoint for this @@ -1760,16 +1777,25 @@ export function _renderRunningTab() { } // Group tasks by server - const _serverName = (host) => { - if (!host) return 'Local'; - const srv = _serverByVal(_envState.remoteServerKey || host) - || _envState.servers.find(s => s.host === host); - return srv?.name || host; + const _taskServerKey = (task) => task?.remoteServerKey || task?.remoteHost || ''; + const _serverName = (keyOrTask) => { + if (keyOrTask && typeof keyOrTask === 'object') { + const task = keyOrTask; + if (task.remoteServerName) return task.remoteServerName; + const srv = task.remoteServerKey ? _serverByVal(task.remoteServerKey) : null; + if (srv?.name) return srv.name; + if (!task.remoteHost) return 'Local'; + return (_envState.servers.find(s => s.host === task.remoteHost)?.name) || task.remoteHost; + } + const key = keyOrTask || ''; + if (!key || key === 'local') return 'Local'; + const srv = _serverByVal(key); + return srv?.name || key; }; const serverGroups = {}; for (const t of tasks) { - const key = t.remoteHost || ''; - if (!serverGroups[key]) serverGroups[key] = { name: _serverName(key), serve: [], download: [] }; + const key = _taskServerKey(t); + if (!serverGroups[key]) serverGroups[key] = { name: _serverName(t), serve: [], download: [] }; serverGroups[key][t.type === 'serve' ? 'serve' : 'download'].push(t); } @@ -1816,12 +1842,12 @@ export function _renderRunningTab() { e.stopPropagation(); // don't toggle the section collapse (was an inline onclick, blocked by CSP) const host = btn.dataset.clearServer; const allTasks = _loadTasks(); - const toRemove = allTasks.filter(t => (t.remoteHost || '') === host && _canClearTask(t)); + const toRemove = allTasks.filter(t => _taskServerKey(t) === host && _canClearTask(t)); // Bail with a clear message instead of silently doing nothing when // every task on this server is still running (nothing finished to // clear yet) — the previous behavior looked like the button was dead. if (!toRemove.length) { - const stillRunning = allTasks.filter(t => (t.remoteHost || '') === host && t.status === 'running').length; + const stillRunning = allTasks.filter(t => _taskServerKey(t) === host && t.status === 'running').length; const _msg = stillRunning ? `No finished tasks on ${_serverName(host)} — ${stillRunning} still running. Stop them first to clear.` : `No finished tasks on ${_serverName(host)}.`; @@ -1830,7 +1856,7 @@ export function _renderRunningTab() { return; } if (!await window.styledConfirm(`Clear ${toRemove.length} finished task${toRemove.length === 1 ? '' : 's'} on ${_serverName(host)}?`, { confirmText: 'Clear' })) return; - const remaining = allTasks.filter(t => (t.remoteHost || '') !== host || !_canClearTask(t)); + const remaining = allTasks.filter(t => _taskServerKey(t) !== host || !_canClearTask(t)); _saveTasks(remaining); // Fade/slide each finished card out (same exit as the per-card clear) // instead of yanking them instantly. @@ -1864,7 +1890,7 @@ export function _renderRunningTab() { btn.addEventListener('click', async (e) => { e.stopPropagation(); // don't toggle the section collapse const host = btn.dataset.stopServer; - const running = _loadTasks().filter(t => (t.remoteHost || '') === host && t.status === 'running'); + const running = _loadTasks().filter(t => _taskServerKey(t) === host && t.status === 'running'); if (!running.length) { uiModule.showToast(`Nothing running on ${_serverName(host)}`); return; } if (!await window.styledConfirm(`Stop ${running.length} running task${running.length > 1 ? 's' : ''} on ${_serverName(host)}?`, { confirmText: 'Stop all' })) return; // Mark every task as user-stopped BEFORE firing the kills so that the @@ -2177,9 +2203,6 @@ export function _renderRunningTab() { if (task.status !== 'running' && task.status !== 'queued') { items.push({ group: 'run', label: 'Reconnect tmux', action: 'reconnect' }); } - if (task.status === 'running') { - items.push({ group: 'run', label: 'Stop', action: 'stop', danger: true }); - } items.push({ group: 'run', label: 'Restart', action: 'retry' }); // ── Edit section ──────────────────────────────────────────── // Merged "Edit & relaunch" — opens the structured serve panel @@ -2539,7 +2562,7 @@ export function _renderRunningTab() { }); // Route to the right server section body - const serverBodyId = `server-body-${(task.remoteHost || 'local').replace(/[^a-zA-Z0-9-]/g, '_')}`; + const serverBodyId = `server-body-${(_taskServerKey(task) || 'local').replace(/[^a-zA-Z0-9-]/g, '_')}`; const targetBody = document.getElementById(serverBodyId); if (targetBody) targetBody.appendChild(el); else group.appendChild(el); @@ -3393,7 +3416,8 @@ function _refreshServerDots() { let tasks; try { tasks = _loadTasks(); } catch { return; } const byKey = {}; - for (const t of tasks) { (byKey[t.remoteHost || ''] = byKey[t.remoteHost || ''] || []).push(t); } + const _taskServerKeyForDot = (task) => task?.remoteServerKey || task?.remoteHost || ''; + for (const t of tasks) { (byKey[_taskServerKeyForDot(t)] = byKey[_taskServerKeyForDot(t)] || []).push(t); } document.querySelectorAll('.cookbook-section-header').forEach(header => { const dot = header.querySelector('.cookbook-srv-status'); if (!dot) return; @@ -3798,6 +3822,7 @@ export function initRunning(shared) { _persistEnvState = shared._persistEnvState; _refreshDependencies = shared._refreshDependencies; _serverByVal = shared._serverByVal; + _serverKey = shared._serverKey; _selectedServer = shared._selectedServer; modelLogo = shared.modelLogo; esc = shared.esc; diff --git a/static/js/cookbookServe.js b/static/js/cookbookServe.js index aba3f7926..da48507f7 100644 --- a/static/js/cookbookServe.js +++ b/static/js/cookbookServe.js @@ -18,6 +18,7 @@ let _sshCmd; let _getPort; let _sshPrefix; let _serverByVal; +let _serverKey; let _getPlatform; let _isWindows; let _isMetal; @@ -41,9 +42,40 @@ let _nextAvailablePort; // Storage keys const SERVE_STATE_KEY = 'cookbook-serve-state'; +const SERVE_FAVORITES_KEY = 'cookbook-serve-favorite-models'; let _cachedAllModels = []; +function _loadServeFavorites() { + try { + const raw = JSON.parse(localStorage.getItem(SERVE_FAVORITES_KEY) || '[]'); + return new Set(Array.isArray(raw) ? raw.filter(Boolean).map(String) : []); + } catch { + return new Set(); + } +} + +function _saveServeFavorites(favorites) { + try { + localStorage.setItem(SERVE_FAVORITES_KEY, JSON.stringify(Array.from(favorites || []))); + } catch {} +} + +function _isServeFavorite(repo) { + return _loadServeFavorites().has(String(repo || '')); +} + +function _toggleServeFavorite(repo) { + const key = String(repo || ''); + if (!key) return false; + const favorites = _loadServeFavorites(); + const next = !favorites.has(key); + if (next) favorites.add(key); + else favorites.delete(key); + _saveServeFavorites(favorites); + return next; +} + function _repoLooksAwqLike(model, repo) { const q = String(model?.quant || '').toUpperCase(); const n = `${repo || ''} ${model?.repo_id || ''} ${model?.name || ''} ${model?.path || ''}`.toLowerCase(); @@ -53,7 +85,9 @@ function _repoLooksAwqLike(model, repo) { function _repoLooksGgufLike(model, repo) { const q = String(model?.quant || '').toUpperCase(); const n = `${repo || ''} ${model?.repo_id || ''} ${model?.name || ''} ${model?.path || ''}`.toLowerCase(); - return !!model?.is_gguf || /^Q[2-8]/.test(q) || /^IQ/.test(q) || q === 'GGUF' || n.includes('gguf'); + const hasGgufFile = Array.isArray(model?.gguf_files) + && model.gguf_files.some(f => f && typeof f.rel_path === 'string' && /\.gguf$/i.test(f.rel_path)); + return !!model?.is_gguf || hasGgufFile || /^Q[2-8]/.test(q) || /^IQ/.test(q) || q === 'GGUF' || n.includes('gguf'); } function _serveBackendWarning(model, repo, backend, fields = {}) { @@ -96,6 +130,352 @@ function _allGpuIds(count) { return Array.from({ length: Math.floor(n) }, (_, i) => String(i)).join(','); } +function _shellSplitForPreview(cmd) { + const s = String(cmd || ''); + const out = []; + let cur = ''; + let quote = ''; + let escNext = false; + for (const ch of s) { + if (escNext) { + cur += ch; + escNext = false; + continue; + } + if (ch === '\\') { + cur += ch; + escNext = true; + continue; + } + if (quote) { + cur += ch; + if (ch === quote) quote = ''; + continue; + } + if (ch === '"' || ch === "'") { + quote = ch; + cur += ch; + continue; + } + if (/\s/.test(ch)) { + if (cur) { + out.push(cur); + cur = ''; + } + continue; + } + cur += ch; + } + if (cur) out.push(cur); + return out; +} + +function _formatServeCmdPreview(cmd) { + const raw = String(cmd || ''); + if (raw.startsWith('MODEL_FILE=$({')) { + const marker = /&&\s+([A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)?(?:llama-server|python3?\s+-m\s+llama_cpp\.server)\b/; + const match = raw.match(marker); + if (match && match.index > 0) { + const prelude = raw.slice(0, match.index).replace(/\s+/g, ' ').trim(); + const rest = raw.slice(match.index).replace(/^\s*&&\s*/, ''); + return `${prelude}\n&&\n${_formatServeCmdPreview(rest)}`; + } + } + const tokens = _shellSplitForPreview(cmd); + if (tokens.length <= 4) return String(cmd || ''); + const lines = []; + let i = 0; + while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) { + lines.push(tokens[i]); + i++; + } + if (tokens[i]) { + const head = [tokens[i++]]; + if (tokens[i] && !tokens[i].startsWith('--') && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) head.push(tokens[i++]); + if (tokens[i] && !tokens[i].startsWith('--') && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) head.push(tokens[i++]); + lines.push(head.join(' ')); + } + while (i < tokens.length) { + const t = tokens[i++]; + if (t.startsWith('--')) { + const vals = []; + while (i < tokens.length && !tokens[i].startsWith('--') && !/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) { + vals.push(tokens[i++]); + } + lines.push([t, ...vals].join(' ')); + } else { + lines.push(t); + } + } + return lines.join('\n'); +} + +function _normalizeServeCmdForLaunch(cmd) { + return String(cmd || '') + .replace(/MODEL_FILE=\$\(\{\s+/g, 'MODEL_FILE=$({ ') + .replace(/\s+\}\s+\|\s+head\s+-1\)/g, ' } | head -1)') + .replace(/\s*;\s*/g, '; ') + .replace(/\s*\|\|\s*/g, ' __ODY_OR__ ') + .replace(/\s*\|\s*/g, ' | ') + .replace(/\s+__ODY_OR__\s+/g, ' || ') + .replace(/\s+/g, ' ') + .trim(); +} + +function _modelSizeGb(model, explicitGb = 0) { + const explicit = Number(explicitGb || 0); + if (Number.isFinite(explicit) && explicit > 0) return explicit; + const bytes = Number(model?.size_bytes || 0); + if (Number.isFinite(bytes) && bytes > 0) return bytes / (1024 ** 3); + const gb = Number( + model?.size_gb + || model?.required_gb + || model?.vram_needed + || model?.min_vram_gb + || model?.recommended_ram_gb + || model?.min_ram_gb + || 0 + ); + if (Number.isFinite(gb) && gb > 0) return gb; + if (_isMiniMaxM3Model(model)) return 240; + return 0; +} + +function _parseParamsB(text) { + const s = String(text || ''); + const m = s.match(/(\d+(?:\.\d+)?)\s*([bBmMtT])\b/); + if (!m) return 0; + const n = parseFloat(m[1]); + if (!Number.isFinite(n) || n <= 0) return 0; + const unit = m[2].toLowerCase(); + if (unit === 't') return n * 1000; + if (unit === 'b') return n; + if (unit === 'm') return n / 1000; + return 0; +} + +function _knownModelContextMax(model) { + if (_isMiniMaxM3Model(model)) return 1048576; + return 0; +} + +function _modelIdentityText(model) { + return [ + model?.repo_id, + model?.quant_repo, + model?.name, + model?.id, + model?.path, + model?.model_path, + model?.served_model_name, + model?.quant, + model?.format, + ].filter(Boolean).join(' ').toLowerCase(); +} + +function _isMiniMaxM3Model(model) { + const name = _modelIdentityText(model); + return ( + (/minimax/.test(name) && /\bm3\b/.test(name)) + || /minimax-m3/.test(name) + || /models--cyankiwi--minimax-m3-awq-int4/.test(name) + || /cyankiwi\/minimax-m3-awq-int4/.test(name) + ); +} + +function _isMiniMaxM2Model(model) { + const name = _modelIdentityText(model); + return /minimax/.test(name) && /\bm2(?:\.\d+)?\b/.test(name); +} + +function _modelContextMaxForServe(model, explicitMax) { + const explicit = Number(explicitMax || 0); + if (Number.isFinite(explicit) && explicit > 0) return explicit; + const known = _knownModelContextMax(model); + if (known > 0) return known; + for (const key of ['context_length', 'max_position_embeddings', 'n_ctx_train', 'model_max_length', 'max_seq_len']) { + const value = Number(model?.[key] || 0); + if (Number.isFinite(value) && value > 0) return value; + } + const catalogCtx = Number(model?.context || 0); + if (Number.isFinite(catalogCtx) && catalogCtx > 0) return catalogCtx; + return 131072; +} + +function _estimateVllmContextFit(model, fields, modelCtxMax, modelWeightsGb = 0, fitSystem = null) { + const sys = fitSystem || _hwfitCache?.system || {}; + const isMiniMaxM3 = _isMiniMaxM3Model(model); + const gpuIds = String(fields.gpus || '').split(',').map(s => parseInt(s.trim(), 10)).filter(Number.isFinite); + const tp = Math.max(1, parseInt(fields.tp, 10) || gpuIds.length || 1); + const selectedCount = Math.max(1, gpuIds.length || tp); + const groups = Array.isArray(sys.gpu_groups) ? sys.gpu_groups : []; + const activeGroup = sys.active_group || groups[0] || null; + const perGpuGb = Number(activeGroup?.vram_each) + || (Number(sys.gpu_vram_gb) / Math.max(1, Number(sys.gpu_count) || selectedCount)) + || 0; + if (!perGpuGb) { + return { needsHardwareScan: true, reason: 'scan hardware first to estimate context from VRAM' }; + } + + const gpuUtil = Math.min(0.99, Math.max(0.1, parseFloat(fields.gpu_mem) || 0.90)); + const budgetGb = perGpuGb * selectedCount * gpuUtil; + const modelGb = _modelSizeGb(model, modelWeightsGb); + if (!modelGb) return { needsModelSize: true, reason: 'model weight size unknown; scan model files or enter context manually' }; + const modelMax = Math.max(1024, _modelContextMaxForServe(model, modelCtxMax)); + + if (isMiniMaxM3) { + const perGpuBudgetGb = perGpuGb * gpuUtil; + const modelShardGb = modelGb / Math.max(1, tp); + const fixedOverheadGb = Math.max(1.5, perGpuBudgetGb * 0.035); + const freeForKv = perGpuBudgetGb - modelShardGb - fixedOverheadGb; + const kvGbPerToken = (29.25 / 1048576) * (String(fields.vllm_kv_cache_dtype || '').toLowerCase() === 'fp8' ? 1 : 1.8); + if (freeForKv <= 0) { + return { + ctx: 1024, + budgetGb, + modelGb, + kvGbPerToken, + reason: `model shard ${modelShardGb.toFixed(1)}G exceeds per-GPU usable ${perGpuBudgetGb.toFixed(1)}G before KV`, + }; + } + const raw = Math.floor((freeForKv / kvGbPerToken) * 0.99); + const rounded = Math.max(1024, Math.floor(raw / 128) * 128); + const ctx = Math.min(modelMax, rounded); + return { + ctx, + budgetGb, + modelGb, + kvGbPerToken, + reason: `~${ctx.toLocaleString()} tokens fits per-GPU KV (${freeForKv.toFixed(1)}G free)`, + }; + } + + const name = `${model?.repo_id || ''} ${model?.name || ''} ${model?.quant || ''}`; + const lower = name.toLowerCase(); + const isMoE = /\bmoe\b|a\d+b|minimax|deepseek|mixtral|kimi-k2|glm-4\.5/.test(lower); + const totalParams = _parseParamsB(name) || Math.max(1, modelGb / 0.58); + const activeFromName = (() => { + const m = lower.match(/\ba(\d+(?:\.\d+)?)b\b/); + return m ? parseFloat(m[1]) : 0; + })(); + const activeParams = activeFromName || (isMoE ? Math.min(totalParams, 32) : totalParams); + const effectiveActiveParams = (/minimax/.test(lower) && /\bm3\b/.test(lower)) ? 23 : activeParams; + const kvDtype = String(fields.vllm_kv_cache_dtype || '').toLowerCase(); + const kvFactor = kvDtype === 'fp8' ? 0.55 : 1; + const kvGbPerTokenTotal = Math.max(0.00002, 0.000008 * effectiveActiveParams * kvFactor); + const kvGbPerToken = kvGbPerTokenTotal / Math.max(1, tp); + const perGpuBudgetGb = perGpuGb * gpuUtil; + const modelShardGb = modelGb / Math.max(1, tp); + const fixedOverheadGb = Math.max(1.5, perGpuBudgetGb * 0.035); + const freeForKv = perGpuBudgetGb - modelShardGb - fixedOverheadGb; + if (freeForKv <= 0) { + return { + ctx: 1024, + budgetGb, + modelGb, + kvGbPerToken, + reason: `model shard ${modelShardGb.toFixed(1)}G exceeds per-GPU usable ${perGpuBudgetGb.toFixed(1)}G before KV`, + }; + } + const raw = Math.floor(freeForKv / kvGbPerToken); + const rounded = Math.max(1024, Math.floor(raw / 1024) * 1024); + const ctx = Math.min(modelMax, rounded); + return { + ctx, + budgetGb, + modelGb, + kvGbPerToken, + reason: `~${ctx.toLocaleString()} tokens fits per-GPU KV (${freeForKv.toFixed(1)}G free)`, + }; +} + +function _estimateLlamaContextFit(model, fields, modelCtxMax, modelWeightsGb = 0, fitSystem = null, profileData = null) { + const profiles = Array.isArray(profileData?.profiles) ? profileData.profiles : []; + const preferred = profiles.find(p => String(p?.key || '').toLowerCase() === 'balanced') + || profiles.find(p => Number(p?.ctx) > 0) + || null; + const modelMax = Math.max(1024, _modelContextMaxForServe(model, modelCtxMax)); + if (preferred && Number(preferred.ctx) > 0) { + const ctx = Math.min(modelMax, Number(preferred.ctx)); + return { + ctx, + reason: `profile ${preferred.label || preferred.key || 'fit'} fits scanned hardware`, + }; + } + + const sys = fitSystem || _hwfitCache?.system || {}; + const modelGb = _modelSizeGb(model, modelWeightsGb); + const backend = String(fields.backend || '').toLowerCase(); + const llamaMode = String(fields.llama_mode || '').toLowerCase(); + const isCpuMode = backend === 'llamacpp' && llamaMode === 'cpu'; + const isUnifiedMode = backend === 'llamacpp' && (llamaMode === 'unified' || fields.unified_mem); + if (!modelGb) { + return { + ctx: Math.min(modelMax, 32768), + needsModelSize: true, + reason: 'model weight size unknown; using model limit fallback', + }; + } + + if (isCpuMode) { + return { + ctx: Math.min(modelMax, 131072), + modelGb, + reason: 'CPU mode uses system RAM; capped to trained limit', + }; + } + + const gpuIds = String(fields.gpus || '').split(',').map(s => parseInt(s.trim(), 10)).filter(Number.isFinite); + const selectedCount = Math.max(1, gpuIds.length || parseInt(fields.tp, 10) || 1); + const groups = Array.isArray(sys.gpu_groups) ? sys.gpu_groups : []; + const activeGroup = sys.active_group || groups[0] || null; + const totalVramGb = Number(activeGroup?.vram_each) + ? Number(activeGroup.vram_each) * selectedCount + : (Number(sys.gpu_vram_gb) || 0); + if (!totalVramGb) { + return { + ctx: Math.min(modelMax, 32768), + modelGb, + needsHardwareScan: true, + reason: 'scan hardware first; using model limit fallback', + }; + } + + const totalRamGb = Number(sys.total_ram_gb) || 0; + const availableRamGb = Number(sys.available_ram_gb) || 0; + const unifiedPoolGb = isUnifiedMode + ? Math.max( + totalVramGb, + availableRamGb, + totalRamGb > 0 ? totalRamGb * 0.85 : 0 + ) + : totalVramGb; + const usableGb = isUnifiedMode + ? Math.max(1, unifiedPoolGb - Math.max(2.0, unifiedPoolGb * 0.08)) + : Math.max(1, totalVramGb - Math.max(1.0, selectedCount * 0.6)); + const freeForKv = usableGb - modelGb; + const kv = String(fields.cache_type || '').toLowerCase(); + const kvFactor = kv === 'q4_0' ? 0.55 : (kv === 'q8_0' ? 1 : (kv === 'f16' ? 1.9 : 1)); + const kvGbPerToken = Math.max(0.00008, (modelGb / 7.5) * 0.0007 * kvFactor); + if (freeForKv <= 0) { + return { + ctx: Math.min(modelMax, 8192), + modelGb, + kvGbPerToken, + reason: `model ${modelGb.toFixed(1)}G exceeds usable ${isUnifiedMode ? 'unified memory' : 'VRAM'} ${usableGb.toFixed(1)}G before KV`, + }; + } + const raw = Math.floor(freeForKv / kvGbPerToken); + const rounded = Math.max(1024, Math.floor(raw / 1024) * 1024); + const ctx = Math.min(modelMax, rounded); + return { + ctx, + modelGb, + kvGbPerToken, + reason: `~${ctx.toLocaleString()} tokens fits llama.cpp KV (${freeForKv.toFixed(1)}G free ${isUnifiedMode ? 'unified' : 'VRAM'})`, + }; +} + function _selectedServeTarget(panel) { const select = document.getElementById('hwfit-server-select') || document.getElementById('hwfit-dl-server'); const servers = Array.isArray(_envState.servers) ? _envState.servers : []; @@ -117,6 +497,8 @@ function _selectedServeTarget(panel) { : (server?.name || 'local server'); return { host, + serverKey: server ? (_serverKey?.(server) || '') : (select?.value || ''), + serverName: server?.name || '', port: host ? (_getPort(host) || server?.port || '') : '', venv, platform: server?.platform || _envState.platform || '', @@ -152,6 +534,9 @@ function _runtimeNoteText(backend, pkg, target) { const label = labels[backend] || backend; if (!pkg) return `${label} readiness unavailable for ${target.label}.`; const note = pkg.status_note || pkg.update_note || ''; + if (pkg.installed === null || pkg.probe_error) { + return note ? `${label} readiness unavailable for ${target.label}: ${note}` : `${label} readiness unavailable for ${target.label}.`; + } if (pkg.installed) { return note ? `${label} ready on ${target.label}: ${note}` : `${label} ready on ${target.label}.`; } @@ -226,6 +611,13 @@ function _runnableGgufFiles(model) { return primary.length ? primary : files; } +function _selectedGgufSizeGb(model, relPath) { + const file = _runnableGgufFiles(model).find(f => f.rel_path === relPath); + const bytes = Number(file?.size_bytes || 0); + if (!Number.isFinite(bytes) || bytes <= 0) return 0; + return bytes / (1024 ** 3); +} + function _ggufFileLabel(file) { const base = (file.name || file.rel_path || '').split('/').pop(); const size = _formatGgufSize(file.size_bytes); @@ -281,6 +673,12 @@ function _rerenderCachedModels() { else if (sortVal === 'size-desc') allModels.sort((a, b) => _parseSize(b.size) - _parseSize(a.size)); else if (sortVal === 'size-asc') allModels.sort((a, b) => _parseSize(a.size) - _parseSize(b.size)); else if (sortVal === 'recent') allModels.sort((a, b) => (b.mtime || 0) - (a.mtime || 0)); + const favorites = _loadServeFavorites(); + allModels.sort((a, b) => { + const af = favorites.has(String(a.repo_id || '')) ? 1 : 0; + const bf = favorites.has(String(b.repo_id || '')) ? 1 : 0; + return bf - af; + }); let html = ''; let visibleCount = 0; @@ -303,8 +701,9 @@ function _rerenderCachedModels() { // living on the same line as the model name. const _isDownloading = m.status === 'downloading'; const _isDlActive = _isDownloading ? _isActivelyDownloading(m.repo_id) : false; + const _isFavorite = favorites.has(String(m.repo_id || '')); const isSelectMode = document.getElementById('hwfit-cache-select')?.classList.contains('active'); - html += `
`; + html += `
`; html += ``; html += `
`; const _mc = modelColor(m.repo_id) || ''; @@ -314,7 +713,8 @@ function _rerenderCachedModels() { const _downloadingPill = _isDownloading ? ` ${_isDlActive ? 'downloading' : 'stalled'}` : ''; - html += `
${modelLogo(m.repo_id)}${esc(shortName)}${hfLink ? ` HF ↗` : ''}${_runningPill}${_downloadingPill}
`; + const _favoritePill = _isFavorite ? ' pinned' : ''; + html += `
${modelLogo(m.repo_id)}${esc(shortName)}${_favoritePill}${hfLink ? ` HF ↗` : ''}${_runningPill}${_downloadingPill}
`; html += `
${metaParts.join(' \u00b7 ')}
`; html += `
`; const _bk = _detectBackend(m).backend; @@ -397,7 +797,12 @@ function _rerenderCachedModels() { const _deleteIco = ''; const _selectIco = ''; const _schedIco = ''; + const _favNow = _isServeFavorite(repo); + const _favIco = _favNow + ? '' + : ''; const items = []; + items.push({ label: _favNow ? 'Unfavorite' : 'Favorite', icon: _favIco, action: 'favorite' }); if (m && m.status === 'ready') items.push({ label: 'Serve', icon: _serveIco, action: 'serve' }); if (m && m.status === 'downloading') items.push({ label: 'Retry', icon: _retryIco, action: 'retry' }); if (m && m.status === 'ready') items.push({ label: 'Schedule…', icon: _schedIco, action: 'schedule' }); @@ -410,6 +815,11 @@ function _rerenderCachedModels() { div.addEventListener('click', () => { closeDropdown(); if (opt.action === 'serve') item.click(); + else if (opt.action === 'favorite') { + const favored = _toggleServeFavorite(repo); + uiModule.showToast(favored ? 'Favorited — pinned to top' : 'Unfavorited'); + _rerenderCachedModels(); + } else if (opt.action === 'delete') _deleteCachedModel(repo, item, false, m); else if (opt.action === 'retry') _retryCachedModel(repo, m); else if (opt.action === 'schedule') { @@ -532,16 +942,22 @@ function _rerenderCachedModels() { const ss = (_byRepo[repo] && typeof _byRepo[repo] === 'object') ? _byRepo[repo] : (_lastUsed || (_isLegacyFlat ? _allSs : {})); + const _modelSs = (_byRepo[repo] && typeof _byRepo[repo] === 'object') ? _byRepo[repo] : null; + const _repoForcedBackend = !!(_modelSs && _modelSs._forceBackend); + const _isMiniMaxM3 = _isMiniMaxM3Model({ ...m, repo_id: repo }); + const _isMiniMaxM2 = _isMiniMaxM2Model({ ...m, repo_id: repo }); + const _isMiniMaxMSeries = _isMiniMaxM3 || _isMiniMaxM2; + const svm = (k, def) => (_modelSs && _hasOwn(_modelSs, k)) ? _modelSs[k] : def; const detectedBackend = _detectBackend(m).backend; const _allowedBackends = new Set(_isWindows() ? ['llamacpp', 'diffusers'] : (_isMetal() ? ['llamacpp', 'ollama'] : ['vllm', 'sglang', 'llamacpp', 'ollama', 'diffusers'])); - const defaultBackend = (ss._forceBackend && ss.backend && _allowedBackends.has(ss.backend)) + const defaultBackend = (_repoForcedBackend && ss.backend && _allowedBackends.has(ss.backend)) ? ss.backend : detectedBackend; - const savedMatchesBackend = !!ss._forceBackend || (ss.backend || 'vllm') === detectedBackend; + const savedMatchesBackend = _repoForcedBackend || (ss.backend || 'vllm') === detectedBackend; const sv = (k, def) => (ss[k] !== undefined && savedMatchesBackend) ? ss[k] : def; - const defaultTp = defaultBackend === 'llamacpp' ? '1' : sv('tp', '1'); + const defaultTp = defaultBackend === 'llamacpp' ? '1' : sv('tp', _isMiniMaxMSeries ? '8' : '1'); const detectedGpuIds = _allGpuIds(_getGpuToggleTotal?.()); const defaultGpus = defaultBackend === 'llamacpp' ? '0' @@ -555,7 +971,7 @@ function _rerenderCachedModels() { // OOMs. _detectModelOptimizations seeds opts.kvCacheDtype for // those families; honour it unless the user has a saved override. const _kvOptsCheck = _detectModelOptimizations(repo); - const _kvAutoDefault = (_kvOptsCheck && _kvOptsCheck.kvCacheDtype) || 'auto'; + const _kvAutoDefault = (_kvOptsCheck && _kvOptsCheck.kvCacheDtype) || (_isMiniMaxMSeries ? 'fp8' : 'auto'); const _kvSelected = sv('vllm_kv_cache_dtype', _kvAutoDefault); const vllmKvCacheOpts = ['auto','fp8'].map(d => ``).join(''); const _l = (name, tip) => `${name}?`; @@ -567,6 +983,11 @@ function _rerenderCachedModels() { const _ggufOptions = _ggufChoices.map(f => `` ).join(''); + const _minimaxM3Snapshot = '/home/pewds/.cache/huggingface/hub/models--cyankiwi--MiniMax-M3-AWQ-INT4/snapshots/4082acbbec1236d21828d55b6bb0fe02ade4ab5b'; + const _defaultServeModel = _isMiniMaxM3 ? _minimaxM3Snapshot : (m.is_local_dir && m.path ? `${m.path}/${repo}` : repo); + const _savedModelPath = String(svm('model_path', _defaultServeModel) || '').trim(); + const _modelPathValue = _isMiniMaxM3 && (!_savedModelPath || _savedModelPath === repo) ? _minimaxM3Snapshot : _savedModelPath; + const _defaultServedModelName = _isMiniMaxM3 ? repo : ''; // Build save slots const _allPresets = _loadPresets(); const _repoShort = repo.split('/').pop(); @@ -583,15 +1004,10 @@ function _rerenderCachedModels() { const _arrowTitle = _modelPresets.length > 0 ? `${_modelPresets.length} saved launch config${_modelPresets.length === 1 ? '' : 's'} for ${_repoShort} — click ▾ to load or delete` : `No saved launch configs for ${_repoShort} yet — click Save to add one`; - // Wrap the Save split in a
+