From 16d222e3b4a75b56fc94b47a4e6462838bc8df77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Sat, 2 May 2026 11:22:18 +1000 Subject: [PATCH] Require local setup embedding profile selection --- .env.example | 14 ++++-- README.md | 41 +++++++++++++++++- scripts/dev/bootstrap-local.mjs | 76 +++++++++++++++++++++++++++++++++ scripts/dev/setup-local.mjs | 2 +- 4 files changed, 126 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index dc405a3..2969c76 100644 --- a/.env.example +++ b/.env.example @@ -6,13 +6,19 @@ # Get one at: https://platform.openai.com/api-keys OPENAI_API_KEY= -# AI profiles. Defaults keep RA-H on OpenAI plus sqlite-vec. -LLM_PROFILE=openai +# AI profiles are selected during setup because embedding dimensions shape +# the sqlite-vec tables. Use one: +# +# npm run setup:local -- --profile openai +# npm run setup:local -- --profile qwen-local +# +# OpenAI profile: +# LLM_PROFILE=openai # LLM_MODEL=gpt-4o-mini -EMBEDDING_PROFILE=openai +# EMBEDDING_PROFILE=openai # EMBEDDING_MODEL=text-embedding-3-small # EMBEDDING_DIMENSIONS=1536 -VECTOR_BACKEND=sqlite-vec +# VECTOR_BACKEND=sqlite-vec # Supported local profile: point RA-H at OpenAI-compatible local endpoints. # Example Ollama: diff --git a/README.md b/README.md index a0916d7..30f315c 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,43 @@ Full install details: --- +## First-Time Setup + +Pick the embedding profile before the database is created. This matters because +the readable `nodes` and `chunks` tables are normal SQLite tables, but the +derived sqlite-vec vector tables are created with a fixed embedding width. + +OpenAI: + +```bash +npm install +npm run setup:local -- --profile openai +npm run dev +``` + +Local Qwen with Ollama: + +```bash +npm install +ollama pull qwen3:4b +ollama pull qwen3-embedding:0.6b +npm run setup:local -- --profile qwen-local +npm run dev +``` + +If you run setup without a profile and `.env.local` does not already select one, +setup stops before creating vector tables and prints the two supported commands. + +If you change embedding provider, model, dimensions, or vector backend after +data exists, your source data stays intact but derived embeddings must be +rebuilt: + +```bash +npm run rebuild:embeddings +``` + +--- + ## OpenAI API Key **Optional but recommended.** Without a key, you can still create and organize nodes manually. @@ -149,7 +186,7 @@ Get a key at [platform.openai.com/api-keys](https://platform.openai.com/api-keys ## Local Model Profile -OpenAI remains the default supported path. If you want local utility LLM calls and local embeddings, run a local OpenAI-compatible model server and point RA-H at it. +OpenAI remains the default supported cloud path. If you want local utility LLM calls and local embeddings, run a local OpenAI-compatible model server and point RA-H at it. Supported local contract: @@ -228,7 +265,7 @@ If you need a clean demo without touching your normal RA-H database: git clone https://github.com/bradwmorris/ra-h_os.git ~/Desktop/ra-h_os-demo cd ~/Desktop/ra-h_os-demo npm install -SQLITE_DB_PATH="$HOME/Desktop/ra-h_os-demo-data/rah.sqlite" npm run setup:local +SQLITE_DB_PATH="$HOME/Desktop/ra-h_os-demo-data/rah.sqlite" npm run setup:local -- --profile qwen-local npm run dev npx -y ra-h-mcp-server@latest setup \ diff --git a/scripts/dev/bootstrap-local.mjs b/scripts/dev/bootstrap-local.mjs index 954e047..64f13a2 100644 --- a/scripts/dev/bootstrap-local.mjs +++ b/scripts/dev/bootstrap-local.mjs @@ -8,6 +8,7 @@ import Database from 'better-sqlite3'; const repoDir = process.cwd(); const envTemplate = path.join(repoDir, '.env.example'); const targetEnv = path.join(repoDir, '.env.local'); +const supportedProfiles = new Set(['openai', 'qwen-local']); function log(message) { console.log(`[bootstrap-local] ${message}`); @@ -89,10 +90,12 @@ function ensureEnvValue(key, value) { const nextLines = lines.map((line) => { const trimmed = line.trim(); if (trimmed.startsWith(`${key}=`)) { + if (found) return line; found = true; return `${key}=${value}`; } if (trimmed.startsWith(`# ${key}=`) || trimmed.startsWith(`#${key}=`)) { + if (found) return line; found = true; return `${key}=${value}`; } @@ -110,6 +113,74 @@ function ensureEnvValue(key, value) { log(`Set ${key} in .env.local`); } +function parseArgs(argv) { + const args = { profile: undefined }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--profile') { + args.profile = argv[index + 1]; + index += 1; + } else if (arg.startsWith('--profile=')) { + args.profile = arg.slice('--profile='.length); + } + } + return args; +} + +function normalizeSetupProfile(rawProfile) { + if (!rawProfile) return undefined; + if (rawProfile === 'qwen' || rawProfile === 'local' || rawProfile === 'ollama') { + return 'qwen-local'; + } + return rawProfile; +} + +function applySetupProfile(profile) { + if (!profile) return; + if (!supportedProfiles.has(profile)) { + throw new Error(`Unsupported setup profile "${profile}". Use "openai" or "qwen-local".`); + } + + if (profile === 'openai') { + ensureEnvValue('LLM_PROFILE', 'openai'); + ensureEnvValue('EMBEDDING_PROFILE', 'openai'); + ensureEnvValue('EMBEDDING_MODEL', 'text-embedding-3-small'); + ensureEnvValue('EMBEDDING_DIMENSIONS', '1536'); + ensureEnvValue('VECTOR_BACKEND', 'sqlite-vec'); + return; + } + + ensureEnvValue('LLM_PROFILE', 'openai-compatible'); + ensureEnvValue('LLM_BASE_URL', 'http://127.0.0.1:11434/v1'); + ensureEnvValue('LLM_MODEL', 'qwen3:4b'); + ensureEnvValue('EMBEDDING_PROFILE', 'openai-compatible'); + ensureEnvValue('EMBEDDING_BASE_URL', 'http://127.0.0.1:11434/v1'); + ensureEnvValue('EMBEDDING_MODEL', 'qwen3-embedding:0.6b'); + ensureEnvValue('EMBEDDING_DIMENSIONS', '1024'); + ensureEnvValue('VECTOR_BACKEND', 'sqlite-vec'); +} + +function assertEmbeddingProfileSelected(env) { + if (env.EMBEDDING_PROFILE) { + return; + } + + throw new Error([ + 'Choose an embedding profile before database setup.', + '', + 'The selected embedding model controls sqlite-vec table dimensions:', + ' OpenAI text-embedding-3-small -> 1536', + ' Local Qwen qwen3-embedding:0.6b -> 1024', + '', + 'Run one of:', + ' npm run setup:local -- --profile openai', + ' npm run setup:local -- --profile qwen-local', + '', + 'If you change embedding provider later, run:', + ' npm run rebuild:embeddings', + ].join('\n')); +} + function ensureCoreSchema(db) { db.pragma('foreign_keys = ON'); db.exec(` @@ -590,9 +661,14 @@ function main() { throw new Error(`Node.js 20+ required (found ${process.version})`); } + const args = parseArgs(process.argv.slice(2)); + const setupProfile = normalizeSetupProfile(args.profile); + ensureEnvFile(); + applySetupProfile(setupProfile); const env = { ...parseEnvFile(targetEnv), ...process.env }; + assertEmbeddingProfileSelected(env); if (process.env.SQLITE_DB_PATH) { ensureEnvValue('SQLITE_DB_PATH', process.env.SQLITE_DB_PATH); env.SQLITE_DB_PATH = process.env.SQLITE_DB_PATH; diff --git a/scripts/dev/setup-local.mjs b/scripts/dev/setup-local.mjs index af584cb..15bb491 100644 --- a/scripts/dev/setup-local.mjs +++ b/scripts/dev/setup-local.mjs @@ -23,7 +23,7 @@ console.log('[setup-local] Rebuilding better-sqlite3 native bindings'); run('npm', ['rebuild', 'better-sqlite3']); console.log('[setup-local] Bootstrapping local RA-H database'); -run('npm', ['run', 'bootstrap:local']); +run('npm', ['run', 'bootstrap:local', '--', ...process.argv.slice(2)]); console.log(''); console.log('[setup-local] Local app setup complete.');