atlas-os/scripts/vault_rag_expand.py
Chaib Aarab 301cc75de9 init: Atlas OS — open-source patterns + tools for solo founder LLM cluster
3 browser-native chat surfaces (atlas-chat / atlas-hud / atlas-command, no build step)
+ 11 Python automation scripts (cluster, tray, finance, reminders, autonomy loops)
+ 2 patterns documented (cluster-then-polish, anti-impulse R1-R10)
MIT licensed, anonymized from production stack.
2026-05-26 16:21:16 +02:00

189 lines
6.1 KiB
Python

#!/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://<YOUR_VPS_IP>: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()