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.
189 lines
6.1 KiB
Python
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()
|