bae705aa97
- 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
323 lines
13 KiB
JavaScript
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,
|
|
};
|