#!/usr/bin/env python3 """ vault_rag_expand.py — Atlas Mind expansion. Walks selected vault folders + batch uploads .md files to Open WebUI KB so ALL cluster models query against the same knowledge graph. Curated set (signal > noise): - 00_Atlas_Wiki/* — operator-curated canonical pages - 01_Clients/* — client briefs + relationship history - 04_Sales/* — pipeline, pricing, templates - 06_Marketing/* — brand, positioning, campaigns - 07_Finance/*.md — money facts + decisions Skip: - 99_Quarantine/ + 99_Archive/ (dead) - .git/ .obsidian/ (config) - Files > 100 KB (template-heavy, low signal) """ import os import json import time import hashlib import urllib.request import urllib.error from pathlib import Path OPENWEBUI = "http://:8082" KB_ID = "c2b9f985-69f6-4dee-b216-73d15bdfb8bd" TOKEN_PATH = Path(r"C:\Users\Gebruiker\AppData\Local\Atlas\openwebui-token.json") VAULT_ROOT = Path(r"C:\Users\Gebruiker\MEGA\Atlas Corporation") INDEX_LOG = Path(r"C:\Users\Gebruiker\AppData\Local\Atlas\analyses\rag_index.json") INCLUDE_FOLDERS = [ "00_Atlas_Wiki", "01_Clients", "04_Sales", "06_Marketing", "07_Finance", ] EXCLUDE_KEYWORDS = ["99_Quarantine", "99_Archive", "_archive", ".git", ".obsidian", "backup", "_backup", "draft-202", "old_"] MAX_FILE_SIZE = 100_000 # 100 KB MAX_FILES_PER_RUN = 200 # rate-limit per cron tick def load_token(): with open(TOKEN_PATH) as f: return json.load(f)["token"] def load_index(): if INDEX_LOG.exists(): try: return json.loads(INDEX_LOG.read_text(encoding="utf-8")) except: pass return {"uploaded": {}, "errors": []} def save_index(idx): INDEX_LOG.parent.mkdir(parents=True, exist_ok=True) INDEX_LOG.write_text(json.dumps(idx, indent=2), encoding="utf-8") def file_hash(p): return hashlib.sha1(p.read_bytes()).hexdigest()[:12] def list_candidates(): """Walk include-folders, yield (Path, relpath_str, hash).""" out = [] for folder in INCLUDE_FOLDERS: root = VAULT_ROOT / folder if not root.exists(): print(f"skip (not found): {root}") continue for p in root.rglob("*.md"): rel = p.relative_to(VAULT_ROOT).as_posix() if any(kw in rel for kw in EXCLUDE_KEYWORDS): continue if p.stat().st_size > MAX_FILE_SIZE: continue if p.stat().st_size < 100: continue # near-empty try: h = file_hash(p) out.append((p, rel, h)) except Exception: pass return out def upload_file(token, path: Path, relpath: str): """Upload single file to Open WebUI via multipart, then attach to KB.""" # Build multipart manually (urllib doesn't have it natively, use boundary) import uuid boundary = "----atlas-" + uuid.uuid4().hex filename = path.name file_bytes = path.read_bytes() body_parts = [ f"--{boundary}".encode(), f'Content-Disposition: form-data; name="file"; filename="{filename}"'.encode(), b"Content-Type: text/markdown", b"", file_bytes, f"--{boundary}--".encode(), b"", ] body = b"\r\n".join(body_parts) # Step 1: upload file req = urllib.request.Request( f"{OPENWEBUI}/api/v1/files/", data=body, method="POST", ) req.add_header("Authorization", f"Bearer {token}") req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}") try: with urllib.request.urlopen(req, timeout=30) as r: file_info = json.loads(r.read().decode()) file_id = file_info.get("id") if not file_id: return False, "no file_id returned" except urllib.error.HTTPError as e: return False, f"upload HTTP {e.code}: {e.read().decode()[:120]}" except Exception as e: return False, f"upload err: {type(e).__name__}" # Step 2: attach to KB req2 = urllib.request.Request( f"{OPENWEBUI}/api/v1/knowledge/{KB_ID}/file/add", data=json.dumps({"file_id": file_id}).encode(), method="POST", ) req2.add_header("Authorization", f"Bearer {token}") req2.add_header("Content-Type", "application/json") try: with urllib.request.urlopen(req2, timeout=30) as r: r.read() return True, file_id except urllib.error.HTTPError as e: return False, f"attach HTTP {e.code}: {e.read().decode()[:120]}" except Exception as e: return False, f"attach err: {type(e).__name__}" def main(): token = load_token() idx = load_index() cands = list_candidates() print(f"Found {len(cands)} candidate files in {len(INCLUDE_FOLDERS)} folders") new_to_upload = [] skipped = 0 for p, rel, h in cands: prev = idx["uploaded"].get(rel) if prev and prev.get("hash") == h: skipped += 1 continue new_to_upload.append((p, rel, h)) new_to_upload = new_to_upload[:MAX_FILES_PER_RUN] print(f"Already uploaded (skip): {skipped}") print(f"To upload this run: {len(new_to_upload)} (cap {MAX_FILES_PER_RUN})") ok_count = 0 err_count = 0 t_start = time.time() for i, (p, rel, h) in enumerate(new_to_upload, 1): success, info = upload_file(token, p, rel) if success: idx["uploaded"][rel] = {"file_id": info, "hash": h, "ts": time.strftime("%Y-%m-%d %H:%M:%S")} ok_count += 1 print(f" [{i}/{len(new_to_upload)}] OK {rel}") else: idx["errors"].append({"rel": rel, "err": str(info), "ts": time.strftime("%Y-%m-%d %H:%M:%S")}) err_count += 1 print(f" [{i}/{len(new_to_upload)}] FAIL {rel} -> {info}") # Save state every 10 uploads in case we crash if i % 10 == 0: save_index(idx) time.sleep(0.3) # gentle pacing save_index(idx) elapsed = round(time.time() - t_start, 1) print(f"\nDone in {elapsed}s") print(f" uploaded: {ok_count}") print(f" errors: {err_count}") print(f" total in index: {len(idx['uploaded'])}") if __name__ == "__main__": main()