Files
boc/intelligence/eslm-b-rag-injektion-design.md
T

462 lines
17 KiB
Markdown
Raw Normal View History

# 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.*