Add AI knowledge exclude list

This commit is contained in:
juvdiaz 2026-06-29 21:05:30 -06:00
parent af75f2c608
commit 3c24b7fbc4
4 changed files with 26 additions and 5 deletions

View File

@ -1113,7 +1113,8 @@ Build the local homelab knowledge index separately from the main deployment:
``` ```
The index is built from the non-secret source list in The index is built from the non-secret source list in
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`. `infra/ai/knowledge-sources.txt`, skips paths in
`infra/ai/knowledge-excludes.txt`, and defaults to `/data/homelab-ai/index`.
Doctor commands use it as extra context when it exists, but infrastructure Doctor commands use it as extra context when it exists, but infrastructure
deployment does not depend on it. deployment does not depend on it.
Use `ask` as the runbook and command finder. It returns the closest indexed Use `ask` as the runbook and command finder. It returns the closest indexed

View File

@ -1113,7 +1113,8 @@ Build the local homelab knowledge index separately from the main deployment:
``` ```
The index is built from the non-secret source list in The index is built from the non-secret source list in
`infra/ai/knowledge-sources.txt` and defaults to `/data/homelab-ai/index`. `infra/ai/knowledge-sources.txt`, skips paths in
`infra/ai/knowledge-excludes.txt`, and defaults to `/data/homelab-ai/index`.
Doctor commands use it as extra context when it exists, but infrastructure Doctor commands use it as extra context when it exists, but infrastructure
deployment does not depend on it. deployment does not depend on it.
Use `ask` as the runbook and command finder. It returns the closest indexed Use `ask` as the runbook and command finder. It returns the closest indexed

View File

@ -0,0 +1,6 @@
# Files and globs excluded from the local homelab AI helper.
# Keep inactive reference material out of answers about the active lab.
archive/**
labs/**
apps/arr-stack/**
apps/mlops-platform/**

View File

@ -16,6 +16,7 @@ from collections import Counter
REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] REPO_ROOT = pathlib.Path(__file__).resolve().parents[1]
DEFAULT_SOURCES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-sources.txt" DEFAULT_SOURCES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-sources.txt"
DEFAULT_EXCLUDES_FILE = REPO_ROOT / "infra" / "ai" / "knowledge-excludes.txt"
DEFAULT_INDEX_DIR = pathlib.Path(os.environ.get("LAB_AI_KNOWLEDGE_INDEX_DIR", "/data/homelab-ai/index")) DEFAULT_INDEX_DIR = pathlib.Path(os.environ.get("LAB_AI_KNOWLEDGE_INDEX_DIR", "/data/homelab-ai/index"))
TOKEN_RE = re.compile(r"[A-Za-z0-9_./:-]{2,}") TOKEN_RE = re.compile(r"[A-Za-z0-9_./:-]{2,}")
MAX_FILE_BYTES = 512 * 1024 MAX_FILE_BYTES = 512 * 1024
@ -54,7 +55,11 @@ def path_is_secret(path: pathlib.Path) -> bool:
return any(marker in lowered for marker in secret_markers) return any(marker in lowered for marker in secret_markers)
def candidate_files(patterns: list[str]) -> list[pathlib.Path]: def path_matches_any(rel_path: str, patterns: list[str]) -> bool:
return any(fnmatch.fnmatch(rel_path, pattern) for pattern in patterns)
def candidate_files(patterns: list[str], exclude_patterns: list[str]) -> list[pathlib.Path]:
files: set[pathlib.Path] = set() files: set[pathlib.Path] = set()
all_files = [path for path in REPO_ROOT.rglob("*") if path.is_file()] all_files = [path for path in REPO_ROOT.rglob("*") if path.is_file()]
@ -69,7 +74,12 @@ def candidate_files(patterns: list[str]) -> list[pathlib.Path]:
if path.is_file(): if path.is_file():
files.add(path) files.add(path)
return sorted(path for path in files if not path_is_secret(path)) return sorted(
path
for path in files
if not path_is_secret(path)
and not path_matches_any(path.relative_to(REPO_ROOT).as_posix(), exclude_patterns)
)
def read_text(path: pathlib.Path) -> str | None: def read_text(path: pathlib.Path) -> str | None:
@ -155,6 +165,7 @@ def build_index(files: list[pathlib.Path]) -> dict[str, object]:
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sources-file", type=pathlib.Path, default=DEFAULT_SOURCES_FILE) parser.add_argument("--sources-file", type=pathlib.Path, default=DEFAULT_SOURCES_FILE)
parser.add_argument("--excludes-file", type=pathlib.Path, default=DEFAULT_EXCLUDES_FILE)
parser.add_argument("--index-dir", type=pathlib.Path, default=DEFAULT_INDEX_DIR) parser.add_argument("--index-dir", type=pathlib.Path, default=DEFAULT_INDEX_DIR)
args = parser.parse_args() args = parser.parse_args()
@ -163,7 +174,8 @@ def main() -> int:
return 1 return 1
patterns = load_patterns(args.sources_file) patterns = load_patterns(args.sources_file)
files = candidate_files(patterns) exclude_patterns = load_patterns(args.excludes_file) if args.excludes_file.is_file() else []
files = candidate_files(patterns, exclude_patterns)
index = build_index(files) index = build_index(files)
args.index_dir.mkdir(parents=True, exist_ok=True) args.index_dir.mkdir(parents=True, exist_ok=True)
@ -174,6 +186,7 @@ def main() -> int:
json.dumps( json.dumps(
{ {
"sources_file": str(args.sources_file.relative_to(REPO_ROOT)), "sources_file": str(args.sources_file.relative_to(REPO_ROOT)),
"excludes_file": str(args.excludes_file.relative_to(REPO_ROOT)) if args.excludes_file.is_file() else None,
"file_count": len(files), "file_count": len(files),
"chunk_count": index["chunk_count"], "chunk_count": index["chunk_count"],
"repo_root": str(REPO_ROOT), "repo_root": str(REPO_ROOT),