Files
boc/build/eslm-rag-injection.mjs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

323 lines
13 KiB
JavaScript

/**
* 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<object>}
*/
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,
};