/** * ESLM RAG Injection Layer — Kod-stub (EJ DEPLOYAD) * ════════════════════════════════════════════════════════════════════════════ * Design: ESLM-B / BERNT 2026-06-06 * Fil: /home/bernt/.openclaw/workspace/build/eslm-rag-injection.mjs * * ARKITEKTURPRINCIP (Erik-låst): * Modellvikter = generell evidensmotor. * Företagsfakta = ALDRIG i vikter. Injiceras vid RUNTIME från CANON. * * DETTA ÄR EN STUB — INTE KÖRBAR I PRODUKTION UTAN DEPLOY-STEG. * Se intelligence/eslm-b-rag-injektion-design.md för deploy-plan. * ════════════════════════════════════════════════════════════════════════════ */ import { existsSync, readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; // ── Sökvägar ────────────────────────────────────────────────────────────────── const __dirname = dirname(fileURLToPath(import.meta.url)); // PROD: /opt/amos/api/rag.mjs (den befintliga RAG-motorn) // STUB: dynamisk import för att undvika hårdkodad sökväg const RAG_MODULE_PATH = process.env.RAG_MODULE_PATH || '/opt/amos/api/rag.mjs'; const CANON_STATIC_PATH = process.env.CANON_STATIC_PATH || '/opt/amos/data/AAMOS_CANON.md'; const WAVULT_TRUTH_PATH = process.env.WAVULT_TRUTH_PATH || '/opt/amos/data/WAVULT_TRUTH.md'; // ── Kategoriserings-nyckelord ───────────────────────────────────────────────── // Frågor som MATchar dessa → company-context-injektion triggas const COMPANY_KEYWORDS = [ // Bolag / produkter 'landvex', 'quixzoom', 'aamos', 'wavult', 'ouroboros', 'vyra', 'homo deus', 'homo_deus', 'gecl', 'stammregister', // Roller / personer 'erik', 'winston', 'dennis', 'johan', 'bernt', 'sven', 'kjell', 'rufus', 'vincento', 'zoomer', 'zoomers', 'cfo', 'cto', 'ceo', 'coo', // Bolagsfakta '559141-7042', 'dmcc', 'org.nr', 'orgnr', 'bolag', 'juridisk', // Produktspecifikt 'kontrollintelligens', 'infrastrukturkontroll', 'mission', 'uppdrag', 'fältdata', 'credits', 'stripe', 'bedrock', 'payout', 'dac7', // Domäner 'aamos.systems', 'wavult.com', 'quixzoom.com', 'landvex.', ]; // Frågor som ALDRIG ska triggra injektion (generella domäner) const GENERIC_PATTERNS = [ /^(vad är|what is|explain|hur funkar|how does) (gdpr|moms|vat|bokföring|accounting|javascript|python|sql|http|rest api)\b/i, /^(skriv|write|generate|skapa) (kod|code|function|klass|class)\b/i, ]; // ── Statisk CANON-fallback (läses en gång, cachas i minnet) ────────────────── let _staticFallback = null; let _staticFallbackTs = 0; /** * Hämta en komprimerad statisk sammanfattning av CANON för fallback. * Används när RAG-motorn är nere eller tar >2s. * * @returns {string} - Komprimerad CANON-sammanfattning (~600 tokens) */ function getStaticCanonFallback() { const now = Date.now(); // Cache 10 min if (_staticFallback && now - _staticFallbackTs < 600_000) return _staticFallback; if (existsSync(CANON_STATIC_PATH)) { try { const raw = readFileSync(CANON_STATIC_PATH, 'utf8'); // Extrahera de viktigaste sektionerna (bolagsstruktur + produkter + förbjudna fakta) const sections = raw.match( /## (1\. BOLAGSSTRUKTUR|2\. PRODUKTER|5\. FÖRBJUDNA)[^\n]*\n[\s\S]*?(?=\n## |\n---|\s*$)/g ) || []; _staticFallback = sections.length > 0 ? `## AAMOS CANON (statisk fallback — RAG otillgänglig)\n\n${sections.join('\n\n').slice(0, 3000)}` : `## AAMOS CANON (fallback)\n\nLandveX AB org.nr 559141-7042, sovereign-grade infrastrukturkontroll. quiXzoom = crowdsourcad missions-/kameraplattform. AAMOS = enterprise AI-plattform. LandveX ≠ mäklare.`; _staticFallbackTs = now; return _staticFallback; } catch (e) { console.warn('[RAG-INJECT] CANON static read failed:', e.message); } } // Hårdkodad minimal fallback (sista utväg) _staticFallback = `## Wavult Ground Truth (minimal hardcoded fallback) - LandveX AB, 559141-7042, säljer infrastrukturkontroll (INTE bostäder/mäklare) - quiXzoom = crowdsourcad missions-/kameraplattform, utförare kallas ZOOMERS (ej fotografer) - AAMOS = enterprise OS/AI-plattform (EJ "AI" i produktnamn — "optical analysis" / "vision engine") - Holding: Wavult Group DMCC (UAE/Dubai DMCC) - Erik Svensson = Chairman/Group CEO. Johan Berglund = CTO. Winston Bjarnemark = CFO. Dennis Bjarnemark = COO/Legal.`; _staticFallbackTs = now; return _staticFallback; } // ── Klassificera query ──────────────────────────────────────────────────────── /** * Avgör om en fråga kräver företagsfakta-injektion. * * @param {string} query * @returns {'company' | 'generic'} */ export function classifyQuery(query) { if (!query || typeof query !== 'string') return 'generic'; const lower = query.toLowerCase(); // Generella mönster → ingen injektion for (const pattern of GENERIC_PATTERNS) { if (pattern.test(lower)) return 'generic'; } // Nyckelord → injektion for (const kw of COMPANY_KEYWORDS) { if (lower.includes(kw)) return 'company'; } // Heuristik: svenska affärsfrågor som rör "vår" verksamhet if (/\b(vi|vår|våra|bolaget|koncernen|gruppen|produkten|plattformen)\b/.test(lower)) { return 'company'; } return 'generic'; } // ── Huvud-API: buildContextForQuery ────────────────────────────────────────── /** * Bygg ett kontextblock med företagsfakta för en given fråga. * * Flöde: * 1. Klassificera frågan (company vs generic) * 2. Om generic → returnera '' (ingen injektion) * 3. Om company → försök RAG (retrieveAsync med timeout) * 4. Om RAG misslyckas → fallback till statisk CANON-sammanfattning * 5. Returnera formatterat kontextblock * * @param {string} query - Användarens senaste meddelande * @param {object} opts * @param {number} opts.timeoutMs - Max ms att vänta på RAG (default 2000) * @param {number} opts.topK - Antal RAG-chunks (default 5) * @param {number} opts.minScore - Minimipoäng för RAG-chunk (default 0.15) * @param {boolean} opts.forceCompany - Tvinga company-kontext (ignorera klassificering) * * @returns {Promise<{contextBlock: string, source: 'rag'|'static'|'none', classification: string}>} */ export async function buildContextForQuery(query, { timeoutMs = 2000, topK = 5, minScore = 0.15, forceCompany = false, } = {}) { const classification = forceCompany ? 'company' : classifyQuery(query); if (classification === 'generic') { return { contextBlock: '', source: 'none', classification }; } // ── Försök RAG-retrieval med timeout ───────────────────────────────────── try { const ragModule = await import(RAG_MODULE_PATH); const { retrieveAsync, formatContext } = ragModule; const ragResult = await Promise.race([ retrieveAsync(query, { topK, minScore }), new Promise((_, reject) => setTimeout(() => reject(new Error('RAG timeout')), timeoutMs) ), ]); if (ragResult && ragResult.chunks && ragResult.chunks.length > 0) { const contextBlock = formatContext(ragResult); return { contextBlock: `## INJICERAD FÖRETAGSKONTEXT (RAG — CANON tier-1)\n\n${contextBlock}`, source: 'rag', classification, ragMeta: { chunks: ragResult.chunks.length, coverage: ragResult.coverage, sources: ragResult.sources, }, }; } // RAG gav inga resultat → fallback console.warn('[RAG-INJECT] RAG returned 0 chunks for query, using static fallback'); return { contextBlock: getStaticCanonFallback(), source: 'static', classification, }; } catch (e) { // RAG-fel eller timeout → fallback const isTimeout = e.message === 'RAG timeout'; console.warn(`[RAG-INJECT] RAG ${isTimeout ? 'timeout' : 'error'}: ${e.message}. Static fallback.`); return { contextBlock: getStaticCanonFallback(), source: 'static', classification, }; } } // ── Hook-in i chat-flöde ────────────────────────────────────────────────────── /** * Express middleware-funktion som hookar in RAG-injektion i chat-flödet. * * ANVÄNDNING i routes.mjs: * import { ragInjectionMiddleware } from '/opt/amos/api/aamos/rag-injection.mjs'; * router.post('/chat', requireAuth, ragInjectionMiddleware, async (req, res) => { ... }) * * Sätter req.ragContextBlock och req.ragSource som kan läsas i chat-handleren. * * @param {import('express').Request} req * @param {import('express').Response} res * @param {import('express').NextFunction} next */ export async function ragInjectionMiddleware(req, res, next) { try { const messages = req.body?.messages || []; const lastUser = messages.filter(m => m.role === 'user').pop(); const query = lastUser?.content || ''; const { contextBlock, source, classification } = await buildContextForQuery(query, { timeoutMs: 1800, // Snäv timeout — chat-svar ska ej fördröjas märkbart }); req.ragContextBlock = contextBlock; req.ragSource = source; req.ragClassification = classification; } catch (e) { // Middleware-fel är aldrig fatala — chatten fortsätter utan injektion console.warn('[RAG-INJECT] Middleware error (non-fatal):', e.message); req.ragContextBlock = ''; req.ragSource = 'error'; req.ragClassification = 'unknown'; } next(); } /** * Bygg den slutliga systemprompten med injicerad CANON-kontext. * * ANVÄNDNING i chat-handler (ersätter nuvarande getEnrichedPrompt-anrop): * * import { buildSystemPromptWithRAG } from '/opt/amos/api/aamos/rag-injection.mjs'; * const systemPrompt = await buildSystemPromptWithRAG(getEnrichedPrompt(baseContext), query); * * @param {string} basePrompt - Existerande systemprompt (från getEnrichedPrompt) * @param {string} query - Användarens fråga * @param {object} opts - Samma som buildContextForQuery * @returns {Promise<{systemPrompt: string, ragSource: string}>} */ export async function buildSystemPromptWithRAG(basePrompt, query, opts = {}) { const { contextBlock, source } = await buildContextForQuery(query, opts); if (!contextBlock) { // Generell fråga — ingen injektion, ren generell modell return { systemPrompt: basePrompt, ragSource: 'none' }; } // Injicera CANON-kontext INNAN basprompten för att säkerställa prioritet const systemPrompt = contextBlock + '\n\n---\n\n' + basePrompt; return { systemPrompt, ragSource: source }; } // ── Hälsokontroll / diagnostik ──────────────────────────────────────────────── /** * Kontrollera RAG-infrastrukturens status. * Returnerar diagnostikinfo utan att faktiskt anropa OpenAI. * * @returns {Promise} */ export async function ragHealthCheck() { const health = { canonExists: existsSync(CANON_STATIC_PATH), wavultTruthExists: existsSync(WAVULT_TRUTH_PATH), ragModuleAccessible: false, indexLoaded: false, embeddingsLoaded: false, error: null, }; try { const ragModule = await import(RAG_MODULE_PATH); health.ragModuleAccessible = true; // Försök bygga/ladda index (synkron version — påverkar ej produktion) const idx = ragModule.buildIndexSync(false); if (idx && idx.total_chunks > 0) { health.indexLoaded = true; health.totalChunks = idx.total_chunks; health.indexBuiltAt = idx.built_at; health.sources = idx.sources; } // Kolla embeddings const { existsSync: exists } = await import('fs'); const EMBEDDINGS_FILE = '/opt/amos/data/rag_embeddings.json'; health.embeddingsFileExists = exists(EMBEDDINGS_FILE); } catch (e) { health.error = e.message; } return health; } // ── Exportera allt ──────────────────────────────────────────────────────────── export default { classifyQuery, buildContextForQuery, buildSystemPromptWithRAG, ragInjectionMiddleware, ragHealthCheck, getStaticCanonFallback, };