mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:27:01 +01:00
Indexes the arch-wiki-docs dump that is already on disk and exposes it
to the knowledge-rag MCP server, so Arch questions can be asked by
symptom ("why doesn't my systemd user unit see DISPLAY under i3")
rather than by page title, with no network round-trip at query time.
The one Arch-specific MCP server in the wild fetches pages over the
network and bundles pacman operations that duplicate the local yay-mcp.
This does neither: it only searches, against a local corpus.
install.sh installs its own dependencies, wires the MCP config and
registers a pacman hook so the index follows package updates. The
generated index is machine-specific and regenerable, so it is
gitignored rather than committed.
_reindex_entry runs under the knowledge-rag interpreter, where
mcp_server is importable and this repo's packages are not. Resolving
that import with importlib.import_module states the deferral outright
instead of a function-level import statement that reads as an oversight,
and the backend failure path now re-raises as SystemExit, so the
exit-code translation is explicit rather than a swallowed exception.
Both replace lint suppressions; the package carries none.
pylint runs in its own venv, so beautifulsoup4 and markdownify are
declared there too — being in meta/requirements.txt does not make them
resolvable to the hook.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
132 lines
4.3 KiB
Python
132 lines
4.3 KiB
Python
"""Trigger a knowledge-rag reindex safely from outside the MCP server.
|
|
|
|
Two guards wrap the reindex, both of which exist because this runs unattended
|
|
from a pacman hook:
|
|
|
|
* **Instance lock.** knowledge-rag's single-instance lock is taken only by its
|
|
``main()``. Importing the orchestrator directly bypasses it, so a reindex
|
|
fired while a ``claude-archwiki`` session is live would have two processes
|
|
writing one Chroma collection. The lock file is checked explicitly instead.
|
|
* **Machine load.** Embedding is CPU-bound -- knowledge-rag ships the CPU-only
|
|
``onnxruntime`` build, so despite the 3090 in this box the pass saturates
|
|
roughly every core for minutes. If the machine is already working hard,
|
|
defer rather than compete: the converted Markdown stays on disk and the next
|
|
run picks up exactly where this one stopped.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
|
|
from python_pkg.archwiki_rag.constants import (
|
|
DATA_SUBDIR,
|
|
KNOWLEDGE_RAG_PYTHON,
|
|
LOAD_PER_CORE_THRESHOLD,
|
|
LOADAVG_PATH,
|
|
LOCK_FILENAME,
|
|
)
|
|
|
|
_ENTRY_MODULE = "python_pkg.archwiki_rag._reindex_entry"
|
|
|
|
|
|
def read_lock_pid(store_dir: Path) -> int | None:
|
|
"""Read the pid recorded in the knowledge-rag instance lock.
|
|
|
|
Parameters:
|
|
store_dir (Path): Root of the knowledge-rag store.
|
|
|
|
Returns:
|
|
int | None: The pid, or None when there is no readable lock file.
|
|
"""
|
|
lock_path = store_dir / DATA_SUBDIR / LOCK_FILENAME
|
|
try:
|
|
first_line = lock_path.read_text(encoding="utf-8").strip().splitlines()[0]
|
|
return int(first_line)
|
|
except (IndexError, OSError, ValueError):
|
|
return None
|
|
|
|
|
|
def is_server_running(store_dir: Path) -> bool:
|
|
"""Report whether a knowledge-rag MCP server currently owns this store.
|
|
|
|
Parameters:
|
|
store_dir (Path): Root of the knowledge-rag store.
|
|
|
|
Returns:
|
|
bool: True when the lock names a live process. A lock left behind by a
|
|
crashed server is treated as free.
|
|
"""
|
|
pid = read_lock_pid(store_dir)
|
|
return pid is not None and Path(f"/proc/{pid}").exists()
|
|
|
|
|
|
def load_per_core() -> float:
|
|
"""Current 1-minute load average, divided by the CPU count.
|
|
|
|
A single read of ``/proc/loadavg`` -- no forks, per the repo's polling
|
|
guidance.
|
|
|
|
Returns:
|
|
float: Load per core, or 0.0 when ``/proc/loadavg`` is unreadable (a
|
|
machine that cannot report load is never treated as busy).
|
|
"""
|
|
try:
|
|
first_field = LOADAVG_PATH.read_text(encoding="utf-8").split(maxsplit=1)[0]
|
|
load = float(first_field)
|
|
except (IndexError, OSError, ValueError):
|
|
return 0.0
|
|
return load / (os.cpu_count() or 1)
|
|
|
|
|
|
def blocking_reason(store_dir: Path, *, ignore_load: bool = False) -> str | None:
|
|
"""Explain why a reindex should not run right now.
|
|
|
|
Parameters:
|
|
store_dir (Path): Root of the knowledge-rag store.
|
|
ignore_load (bool): Skip the load check, for an explicitly requested run.
|
|
|
|
Returns:
|
|
str | None: Human-readable reason to defer, or None when clear to proceed.
|
|
"""
|
|
if is_server_running(store_dir):
|
|
return "a knowledge-rag server is already using this store"
|
|
if not ignore_load:
|
|
load = load_per_core()
|
|
if load > LOAD_PER_CORE_THRESHOLD:
|
|
return (
|
|
f"machine is busy (load {load:.2f}/core); deferring to avoid contention"
|
|
)
|
|
return None
|
|
|
|
|
|
def run_reindex(store_dir: Path) -> int:
|
|
"""Run the reindex under the knowledge-rag interpreter.
|
|
|
|
Parameters:
|
|
store_dir (Path): Root of the knowledge-rag store, passed through as
|
|
``KNOWLEDGE_RAG_DIR`` so the child targets the right corpus.
|
|
|
|
Returns:
|
|
int: Exit status of the child process; 127 when its interpreter is absent.
|
|
"""
|
|
if not KNOWLEDGE_RAG_PYTHON.exists():
|
|
return 127
|
|
|
|
env = dict(os.environ)
|
|
env["KNOWLEDGE_RAG_DIR"] = str(store_dir)
|
|
# The entry module imports from python_pkg, which is not installed into the
|
|
# knowledge-rag venv, so point that interpreter back at this repo.
|
|
repo_root = Path(__file__).resolve().parents[2]
|
|
env["PYTHONPATH"] = os.pathsep.join(
|
|
[str(repo_root), *([env["PYTHONPATH"]] if env.get("PYTHONPATH") else [])],
|
|
)
|
|
|
|
completed = subprocess.run(
|
|
[str(KNOWLEDGE_RAG_PYTHON), "-m", _ENTRY_MODULE],
|
|
env=env,
|
|
check=False,
|
|
)
|
|
return completed.returncode
|