# ESLM-B: RAG/CANON Runtime-Fakta-Injektion — Design **Datum:** 2026-06-06 **Status:** Design + stub (EJ deployad) **Utfört av:** Bernt (subagent ESLM-B) **Kod-stub:** `/home/bernt/.openclaw/workspace/build/eslm-rag-injection.mjs` --- ## 0. Verifierat vs Antagande | Symbol | Betydelse | |--------|-----------| | ✅ VERIFIERAT | Läst ur faktisk körande kod/fil | | ⚠️ ANTAGANDE | Logisk slutledning, ej källverifierad | --- ## 1. NULÄGE — Exakt karta (verifierad mot körande kod) ### 1.1 Produktionsserver — var den kör ✅ **Tjänst:** `amos-core.service` via systemd ✅ **Fil:** `/opt/amos/services/amos-core/server.mjs` ✅ **Port:** 3100 (bind 0.0.0.0) ✅ **AMOS_ROOT:** `/opt/amos/` (resolve av `services/amos-core/../..`) ✅ **Node.js:** via `ExecStart=/usr/bin/node --max-old-space-size=2048` ### 1.2 Hur systemprompten byggs — chat-flödet Det finns **två chat-endpoints** som båda använder samma systemprompt-logik: #### `/api/aamos/chat` (huvud-AAMOS-chat) ``` [request] POST /api/aamos/chat ↓ [routes.mjs] /opt/amos/api/aamos/routes.mjs:77 ↓ import getEnrichedPrompt [system-prompt.mjs] /opt/amos/api/dynasty-counsel/system-prompt.mjs ├── getFactsContext() ← HÅRDKODAD fil: wavult-facts.mjs └── getToolsContextBlock() + buildPersonaBlock() ↓ systemPrompt = getEnrichedPrompt(baseContext) + aamosContext [rad 109] ↓ [model-router.mjs] selectModelCompliant() → Qwen3 via AWS Bedrock ↓ [qwenStream()] SSE-svar till klient ``` ✅ **Verifierat:** `api/aamos/routes.mjs` importerar `getEnrichedPrompt` från `api/dynasty-counsel/system-prompt.mjs` ✅ **Verifierat:** `getEnrichedPrompt` anropar `getFactsContext()` som läser `wavult-facts.mjs` #### `/api/dynasty-counsel/chat` (juridisk assistent) ``` [request] POST /api/dynasty-counsel/chat ↓ [routes.mjs] /opt/amos/api/dynasty-counsel/routes.mjs ↓ const systemPrompt = getEnrichedPrompt(enrichedContext) [rad ~240] ↓ [system-prompt.mjs] → getFactsContext() → wavult-facts.mjs ↓ [runToolLoop()] med tool-loop.mjs ↓ SSE-stream till klient ``` ✅ **Verifierat:** Samma `getEnrichedPrompt` används ✅ **Verifierat:** `WAVULT_FACTS` importeras direkt i routes.mjs rad ~20 #### `/api/chat` (generisk Qwen3-endpoint) ✅ **Verifierat:** `server.mjs` rad 1389 — `app.post('/api/chat', ...)` använder **INTE** `getEnrichedPrompt`. Minimal hårdkodad prompt: `"Du är AAMOS — Wavult Groups AI-plattform (Qwen3 via AWS Bedrock). Svara precist."` — **ingen faktainjektion alls**. ### 1.3 Var injiceras getFactsContext() ``` getEnrichedPrompt(context) [system-prompt.mjs] ├─ BASE_SYSTEM_PROMPT (statisk, ~4KB) ├─ getFactsContext() ← HÅRDKODAD import: wavult-facts.mjs │ └─ formaterar WAVULT_FACTS till ~1KB kontextblock ├─ getToolsContextBlock() (tool-katalog) ├─ buildPersonaBlock() (user-specifik persona) └─ session context (mode, tenant, user_email...) ``` ✅ **Verifierat:** `system-prompt.mjs` rad ~1: `import { getFactsContext } from './knowledge/wavult-facts.mjs';` ### 1.4 wavult-facts.mjs — nuläge ✅ **Fil:** `/opt/amos/api/dynasty-counsel/knowledge/wavult-facts.mjs` ✅ **Version:** 1.1.0, uppdaterad 2026-06-06 ✅ **Innehåll:** Hardkodad JavaScript-modul med WAVULT_FACTS objekt ✅ **Storlek:** ~5KB av produkt/entitets-/teamfakta ✅ **Backup-filer:** `.bak-canon-20260606-161310`, `.bak-cleanup-20260606-163506`, `.bak-purge` — indikerar nyliga ändringar **Obs:** `wavult-facts.mjs` innehåller AAMOS_CANON-data som har duplicerats manuellt hit. Den är *inte* automatgenererad från CANON. ### 1.5 rag.mjs — nuläge ✅ **Fil:** `/opt/amos/api/rag.mjs` ✅ **RAG-index:** `/opt/amos/data/rag_index.json` — byggt 2026-06-06T15:57:39Z ✅ **Embeddings:** `/opt/amos/data/rag_embeddings.json` — **94/94 chunks har faktiska embeddings** ✅ **Embedding-modell:** OpenAI `text-embedding-3-small` ✅ **Retrieval:** Primär semantisk (cosine similarity), fallback BM25 ✅ **Export:** `retrieveAsync()`, `retrieve()`, `formatContext()`, `buildIndex()` **AAMOS_CANON.md är tier-1 i rag.mjs:** ```javascript // /opt/amos/api/rag.mjs — KNOWLEDGE_SOURCES { id: 'aamos_canon', label: 'AAMOS_CANON.md', tier: 1, // ENDA KANONISKA SANNINGSKÄLLAN — Erik-låst 2026-06-06 path: join(DATA_DIR, 'AAMOS_CANON.md'), } ``` ✅ **Verifierat:** AAMOS_CANON är tier-1 med 11 chunks i nuvarande index **Kritisk observation:** ✅ **VERIFIERAT:** `rag.mjs` används **INTE** av `/api/aamos/chat` eller `/api/dynasty-counsel/chat`. Varken `api/aamos/routes.mjs` eller `api/dynasty-counsel/routes.mjs` importerar `rag.mjs` eller anropar `retrieveAsync()`. RAG-systemet existerar och är fullt byggt med embeddings, men är **kopplat bort** från huvud-chat-flödet. ### 1.6 Index/embeddings-status per källa | Källa | Tier | Chunks | Embeddings | |-------|------|--------|-----------| | AAMOS_CANON.md | 1 | 11 | 11/11 ✅ | | WAVULT_TRUTH.md | 1 | 17 | 17/17 ✅ | | AMOS_KNOWLEDGE.md | 1 | 56 | 56/56 ✅ | | AMOS_CONSTITUTION.md | 1 | 10 | 10/10 ✅ | | **Totalt** | | **94** | **94/94** | ✅ **RAG-indexet är byggt och komplett.** ### 1.7 ESLM-modellen ⚠️ **ANTAGANDE:** ESLM (v5) = `aamos-eslm` modell på vLLM-endpoint `172.31.36.61:8000`. ✅ **Verifierat:** `eslm-judge.mjs` refererar till `ESLM_ENDPOINT = 'http://172.31.36.61:8000/v1'` och `ESLM_MODEL = 'aamos-eslm'`. ⚠️ **Okänt:** Om ESLM-endpointen är live (curl timeout). ESLM kan vara under driftsättning. --- ## 2. PROBLEMANALYS — Gap mellan nuläge och målarkitektur ``` NULÄGE: /api/aamos/chat → getEnrichedPrompt() → wavult-facts.mjs (hårdkodad JS) /api/dynasty-counsel/chat → samma rag.mjs (med CANON tier-1) → ANVÄNDS INTE av chatten ESLM-modell (172.31.36.61:8000) → ingen faktainjektion alls PROBLEM: 1. wavult-facts.mjs är en kopia av CANON-data, ej härledd från CANON 2. RAG/CANON-systemet existerar men är frånkopplat 3. Generell ESLM saknar ALL faktainjektion 4. /api/chat (generisk) = ingen injektion alls ``` --- ## 3. INJEKTIONSDESIGN ### 3.1 Principer (Claude×Siemens-robust) 1. **En källa (CANON):** AAMOS_CANON.md är enda auktoritativa källan. wavult-facts.mjs deprecas eller autogenereras från CANON. 2. **Intelligent routing:** Klassificera frågan → injicera bara vid behov → generella frågor = ren generell modell. 3. **Ingen single point of failure:** RAG → statisk CANON-fallback → minimal hardkodad fallback. 4. **Latency-gräns:** RAG-anrop max 2s (timeout) → chat-svar ska ej fördröjas märkbart. 5. **Sekretess:** CANON-data injiceras bara i system-rollen, aldrig tillbaka till klienten. ### 3.2 Flödesdiagram ``` [User query] ↓ classifyQuery(query) ├─ 'generic' → systemPrompt = basePrompt (ingen injektion) └─ 'company' ↓ retrieveAsync(query, {topK:5, minScore:0.15}) ├─ OK (chunks > 0) → formatContext() → RAG-block └─ FAIL/timeout → getStaticCanonFallback() ↓ systemPrompt = CANON-block + '---' + basePrompt ↓ [LLM anrop: ESLM eller Qwen3] ↓ [Svar till klient] ``` ### 3.3 Klassificering — COMPANY vs GENERIC **Company-keywords (komplett lista i kod-stub):** - Bolagsnamn: `landvex`, `quixzoom`, `aamos`, `wavult`, `ouroboros`, `vyra` - Teamnamn: `erik`, `winston`, `dennis`, `johan`, `bernt`, `sven`, `kjell`, `rufus` - Produktspecifikt: `zoomer`, `kontrollintelligens`, `mission`, `uppdrag`, `gecl`, `stammregister` - Org-nr: `559141-7042`, `dmcc` **Generic-mönster (ingen injektion):** - Frågor om GDPR/MOMS/bokföring som inte rör Wavult specifikt - Kodfrågor (JavaScript, Python, SQL) - Allmänna "explain X"-frågor **Heuristik:** Svenska frågor med "vi/vår/våra/bolaget" → company ### 3.4 buildContextForQuery() — Huvud-API Se kod-stub för komplett implementation. Signatur: ```javascript /** * @param {string} query - Användarens fråga * @param {object} opts * @returns {Promise<{ * contextBlock: string, // Injicera i systemPrompt ('' om generic) * source: 'rag'|'static'|'none', * classification: 'company'|'generic' * }>} */ async function buildContextForQuery(query, { timeoutMs=2000, topK=5, minScore=0.15 } = {}) ``` ### 3.5 Var hookar den in i chat-flödet **Alternativ A — Express Middleware (rekommenderat):** ```javascript // I api/aamos/routes.mjs: import { ragInjectionMiddleware } from '../aamos/rag-injection.mjs'; router.post('/chat', requireAuth, ragInjectionMiddleware, async (req, res) => { // req.ragContextBlock sätts av middleware const systemPrompt = req.ragContextBlock ? req.ragContextBlock + '\n\n---\n\n' + getEnrichedPrompt(baseContext) + aamosContext : getEnrichedPrompt(baseContext) + aamosContext; // ...resten oförändrat... }); ``` **Alternativ B — Inline i chat-handler:** ```javascript // I api/aamos/routes.mjs, inuti POST /chat: const { buildSystemPromptWithRAG } = await import('../aamos/rag-injection.mjs'); const { systemPrompt } = await buildSystemPromptWithRAG( getEnrichedPrompt(baseContext) + aamosContext, lastUserMsg ); ``` **Alternativ C — I buildSystemPrompt (direkt ersätt getEnrichedPrompt):** ```javascript // system-prompt.mjs — modifiering av getEnrichedPrompt: export async function getEnrichedPromptWithRAG(context = {}, query = '') { const { buildContextForQuery } = await import('./rag-injection.mjs'); const { contextBlock } = await buildContextForQuery(query); let out = contextBlock ? contextBlock + '\n\n---\n\n' : ''; out += BASE_SYSTEM_PROMPT + '\n\n'; out += getFactsContext({ verbose: false }) + '\n\n'; // behåll som fallback tills CANON är enda källa // ...resten... return out; } ``` **Rekommendation:** Alternativ A (middleware) ger minst kod-ändring och isolerar logiken. ### 3.6 ESLM-specifik injektion ESLM-endpointen (`172.31.36.61:8000`) har ingen injektion idag. För ESLM gäller: ```javascript // Vid anrop till ESLM-endpointen (vLLM OpenAI API): const { contextBlock } = await buildContextForQuery(userQuery); const systemMessage = contextBlock ? contextBlock + '\n\nDu är en AI-assistent för Wavult/AAMOS. Svara precist.' : 'Du är en AI-assistent. Svara precist.'; const response = await fetch('http://172.31.36.61:8000/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'aamos-eslm', messages: [ { role: 'system', content: systemMessage }, ...userMessages ], temperature: 0.1, max_tokens: 2048, }), }); ``` --- ## 4. RISKER + FALLBACK ### 4.1 RAG är nere (OpenAI-api, disk, process-crash) **Fallback-kedja (3 nivåer):** 1. **RAG OK** → semantisk retrieval, CANON tier-1 chunks → bäst 2. **RAG timeout/fel** → `getStaticCanonFallback()` — läser CANON.md direkt och extraherar bolagsstruktur + produktsektioner + förbjudna fakta (~600 tokens) 3. **CANON.md oläsbar** → minimal hardkodad sträng i minnet (bolag/produkt/team/regler) **Timeout:** 2000ms (konfigurerbart via `opts.timeoutMs`). RAG-fel är aldrig fatala — chatten fortsätter. ### 4.2 Fakta hamnar i fel kontext (säkerhet) **Risk:** CANON-data läcker till ej autentiserade endpoints. **Mitigation:** - Injektionen sker alltid **server-side**, i system-rollen, aldrig i user/assistant-rollen - Klassificeringen sker per fråga — generella frågor får ingen injektion - CANON innehåller inga hemligheter (org-nr, namn, produktfakta är semi-publik affärssanning) - `/api/chat` (generisk, publik) ska **INTE** injiceras — skapa separat endpoint för ESLM om nödvändigt - Auth krävs redan på `/api/aamos/chat` och `/api/dynasty-counsel/chat` **CANON innehåller ALDRIG:** - API-nycklar, lösenord, tokens - Personuppgifter (personnummer, bankuppgifter) - Intern infrastruktur-IP/portar (WAVULT_TRUTH.md hanteras separat) ### 4.3 Injektion av felaktig data (hallucination via RAG) **Risk:** RAG hämtar irrelevant chunk och "hittar på" koppling. **Mitigation:** - `minScore = 0.15` (semantisk) eller BM25 threshold — låga scores ignoreras - CANON är tier-1 (E4 evidens) — data är verifierad av Erik - `formatContext()` märker varje chunk med källa + radnummer → auditerbart - CANON uppdateras via kontrollerad process (Erik-godkännande), inte automatiskt ### 4.4 Latency-påverkan **Risk:** RAG-anrop (OpenAI embedding) tar 200-800ms och fördröjer chat-svar. **Mitigation:** - Timeout 2000ms — timeout → statisk fallback (0ms extra) - Embeddings cachas i minnet (`_embeddingsCache`, 1h TTL) - Index cachas i minnet (5 min TTL), disk-cache (1h TTL) - Klassificering är synkron (0ms) — generella frågor skippar RAG helt ### 4.5 wavult-facts.mjs — dupliceringsrisk **Risk:** wavult-facts.mjs och AAMOS_CANON.md divergerar (uppdatering i en men inte båda). **Mitigation (kort sikt):** behåll båda under migrationsfasen — RAG-CANON ersätter gradvis. **Mitigation (lång sikt):** generera wavult-facts.mjs automatiskt från CANON: ```bash # CI-script: auto-generera wavult-facts.mjs från CANON node scripts/canon-to-wavult-facts.mjs \ --input /opt/amos/data/AAMOS_CANON.md \ --output /opt/amos/api/dynasty-counsel/knowledge/wavult-facts.mjs ``` --- ## 5. DEPLOY-PLAN (vilka filer ändras, i vilken ordning) > **OBS:** Inget i denna plan muterar produktionskod idag. Alla steg kräver explicit godkännande. ### Steg 1 — Placera injektionsfilen (låg risk) ``` KÄLLA: /home/bernt/.openclaw/workspace/build/eslm-rag-injection.mjs MÅL: /opt/amos/api/aamos/rag-injection.mjs ÄNDRAR: Inget befintligt RISK: Noll (ny fil) ``` ### Steg 2 — Verifiera RAG-hälsa (ingen mutation) ```bash node --input-type=module << 'EOF' import rag from '/opt/amos/api/aamos/rag-injection.mjs'; console.log(await rag.ragHealthCheck()); // Förväntat: canonExists: true, ragModuleAccessible: true, indexLoaded: true EOF ``` ### Steg 3 — Integrera i /api/aamos/chat (middleware-approach) **Fil att ändra:** `/opt/amos/api/aamos/routes.mjs` ```diff import { getEnrichedPrompt } from '../dynasty-counsel/system-prompt.mjs'; +import { ragInjectionMiddleware } from './rag-injection.mjs'; -router.post('/chat', requireAuth, aamosRateLimit, express.json({ limit: '256kb' }), async (req, res) => { +router.post('/chat', requireAuth, aamosRateLimit, express.json({ limit: '256kb' }), ragInjectionMiddleware, async (req, res) => { // ... (existerande kod) ... - const systemPrompt = getEnrichedPrompt(baseContext) + aamosContext; + const ragBlock = req.ragContextBlock || ''; + const systemPrompt = ragBlock + ? ragBlock + '\n\n---\n\n' + getEnrichedPrompt(baseContext) + aamosContext + : getEnrichedPrompt(baseContext) + aamosContext; ``` ### Steg 4 — Integrera i /api/dynasty-counsel/chat **Fil att ändra:** `/opt/amos/api/dynasty-counsel/routes.mjs` Samma pattern som steg 3 — lägg till `ragInjectionMiddleware` och modifiera `systemPrompt`-konstruktionen. ### Steg 5 — ESLM-endpoint (framtida) **Fil att skapa/ändra:** `/opt/amos/api/aamos/eslm-proxy.mjs` (ny fil) Wrappa ESLM-anropet med `buildContextForQuery()` innan forward till `172.31.36.61:8000`. ### Steg 6 — Rebuild RAG-index med CANON som primär ```bash # Trigga rebuild av index (kör mot körande server) curl -X POST http://localhost:3100/api/rag/rebuild 2>/dev/null || \ node -e "import('/opt/amos/api/rag.mjs').then(m => m.buildIndex(true))" ``` ### Steg 7 — (Lång sikt) Deprecera wavult-facts.mjs 1. Skapa `scripts/canon-to-wavult-facts.mjs` som autogenererar filen från CANON 2. Kör scriptet i CI vid varje CANON-ändring 3. `wavult-facts.mjs` blir en genererad fil, ej manuellt underhållen ### Steg 8 — Monitoring Logga per request: ```json { "ts": "2026-06-06T23:00:00Z", "component": "rag-injection", "query_classification": "company", "rag_source": "rag", // "rag" | "static" | "none" "rag_chunks": 4, "rag_latency_ms": 312, "rag_coverage": "E4" } ``` --- ## 6. SAMMANFATTNING | Aspekt | Nuläge | Målarkitektur | |--------|--------|---------------| | Faktakälla | `wavult-facts.mjs` (hårdkodad JS-fil) | `AAMOS_CANON.md` via RAG | | Injektionspunkt | Alltid, alla frågor | Intelligent klassificering (company vs generic) | | RAG-status | Byggt + embeddings komplett — men frånkopplat | Kopplat till chat-flödet | | ESLM-injektion | Ingen | Via `buildContextForQuery()` | | Fallback | Ingen (kraschar tyst) | 3-nivå: RAG → CANON statisk → hardkodad | | Duplikering | CANON + wavult-facts divergerar | CANON = enda källa, wavult-facts autogenereras | **Kod-stub:** `/home/bernt/.openclaw/workspace/build/eslm-rag-injection.mjs` **Exporterar:** `classifyQuery`, `buildContextForQuery`, `buildSystemPromptWithRAG`, `ragInjectionMiddleware`, `ragHealthCheck` --- *Producerat av agent Bernt (ESLM-B subagent) 2026-06-06. Mutera ingenting i prod utan Erik-godkännande.*