# Oghma RAG Proxy — Technical Specification **Project:** SkyrimNet Lore Enhancement via RAG Proxy **Status:** Design Phase **Author:** Chrysalis + dafit **Created:** 2026-03-30 --- ## 1. Problem Statement SkyrimNet currently relies on: 1. LLM's baked-in Skyrim knowledge (incomplete, potentially wrong) 2. Dynamic memories (what the NPC witnessed) **Missing:** Authoritative lore retrieval filtered by what each NPC *should* know. **Result:** - Knowledge bleedover (guard knows Telvanni secrets) - Lore inaccuracies (mixing up timelines, factions) - No grounding in canon --- ## 2. Solution: Oghma RAG Proxy A transparent proxy that sits between SkyrimNet and the LLM inference endpoint. ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ OGHMA RAG PROXY │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐ │ │ │ SkyrimNet │ │ Oghma Proxy │ │ OpenRouter │ │ │ │ SKSE Plugin │────────▶│ (FastAPI) │────────▶│ / vLLM │ │ │ │ │ │ │ │ │ │ │ │ Port: N/A │ │ Port: 8100 │ │ Upstream │ │ │ └──────────────┘ └────────┬─────────┘ └──────────────┘ │ │ │ │ │ │ Query │ │ ▼ │ │ ┌──────────────────┐ │ │ │ iris-dev │ │ │ │ ChromaDB │ │ │ │ Port: 35000 │ │ │ │ │ │ │ │ Collections: │ │ │ │ - oghma_lore │ │ │ │ - oghma_basic │ │ │ └──────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## 3. Core Principles 1. **Zero SkyrimNet Changes** — Only change the endpoint URL in config 2. **Transparent Passthrough** — Unknown requests forward unchanged 3. **NPC-Aware Filtering** — Lore filtered by extracted NPC profile 4. **Two-Tier Content** — Scholars get deep lore, commoners get basics 5. **Nimmerverse Native** — Runs on existing infrastructure (iris-dev) --- ## 4. Architecture Components ### 4.1 Oghma Proxy Service **Location:** VM on nimmerverse (could run on phoebe-dev or dedicated) **Tech Stack:** Python 3.11+, FastAPI, httpx, chromadb-client **Port:** 8100 (configurable) **Responsibilities:** - Intercept OpenRouter-compatible API requests - Parse prompts to extract NPC context - Query ChromaDB for relevant lore - Inject lore into system prompt - Forward to upstream LLM - Stream response back to SkyrimNet ### 4.2 Oghma ChromaDB Collection **Location:** iris-dev.eachpath.local:35000 **Collections:** | Collection | Content | Use Case | |------------|---------|----------| | `oghma_lore` | Full `topic_desc` entries | Scholars, mages, priests | | `oghma_basic` | Simplified `topic_desc_basic` | Commoners, guards, peasants | **Metadata Schema:** ```json { "topic": "Akatosh", "category": "Figures/Gods", "knowledge_classes": ["priest", "scholar", "dragon", "snowelf"], "knowledge_classes_basic": ["nord", "imperial", "breton"], "tags": ["divine", "time", "dragon-god"], "source_sheet": "Figures/Gods" } ``` ### 4.3 NPC Profile Extractor Parses SkyrimNet prompts to extract: - NPC name - Race - Profession/class (from bio or context) - Factions - Location (hold) - Special traits **Extraction Patterns:** ```python # From character bio section r"## (?P\w+) Bio\n- Gender: (?P\w+)\n- Race: (?P\w+)" # From system context r"You are (?P[^,]+), a (?P\w+) (?P\w+)" # From faction mentions r"member of (?:the )?(?P[\w\s]+)" ``` --- ## 5. Data Flow ### 5.1 Request Interception ``` 1. SkyrimNet sends POST /chat/completions to proxy 2. Proxy extracts NPC profile from messages 3. Proxy extracts conversation context/topic 4. Proxy queries ChromaDB: - Collection: oghma_lore or oghma_basic (based on NPC education) - Filter: knowledge_classes intersects NPC's classes - Query: conversation context (semantic search) - Limit: 3-5 most relevant entries 5. Proxy injects lore block into system message 6. Proxy forwards enriched request to upstream 7. Proxy streams response back to SkyrimNet ``` ### 5.2 Lore Injection Format Injected after character bio, before conversation: ``` ## Relevant Lore Knowledge Based on your background as a Nord priest in Whiterun, you would know: - **Talos**: [Condensed lore about Talos worship, appropriate to character] - **Whiterun**: [Local knowledge about the hold] - **Companions**: [If character has connection] Remember: This is knowledge your character possesses. Reference it naturally in conversation, don't recite it. ``` --- ## 6. NPC Knowledge Classification ### 6.1 Profile → Knowledge Classes Mapping ```python RACE_CLASSES = { "Nord": ["nord"], "Dunmer": ["darkelf", "dunmer"], "Altmer": ["highelf", "altmer"], "Bosmer": ["woodelf", "bosmer"], "Argonian": ["argonian"], "Khajiit": ["khajiit"], "Breton": ["breton"], "Redguard": ["redguard"], "Orsimer": ["orc", "orsimer"], "Imperial": ["imperial"], } PROFESSION_CLASSES = { "priest": ["priest"], "mage": ["mage", "scholar"], "scholar": ["scholar"], "blacksmith": ["blacksmith"], "guard": ["guard", "warrior"], "thief": ["thief"], "merchant": ["merchant"], "innkeeper": ["innkeeper"], "hunter": ["hunter"], "farmer": ["peasant"], "noble": ["noble"], "bard": ["bard"], } LOCATION_CLASSES = { "Whiterun": ["whiterun"], "Windhelm": ["eastmarch"], "Solitude": ["haafingar"], "Riften": ["rift"], "Markarth": ["reach"], "Morthal": ["hjaalmarch"], "Dawnstar": ["pale"], "Winterhold": ["winterhold"], "Falkreath": ["falkreath"], "Solstheim": ["solstheim"], } FACTION_CLASSES = { "Companions": ["companions"], "College of Winterhold": ["college", "mage"], "Thieves Guild": ["thieves"], "Dark Brotherhood": ["darkbrotherhood"], "Stormcloaks": ["stormcloak"], "Imperial Legion": ["imperial"], "Thalmor": ["thalmor"], "Dawnguard": ["dawnguard"], "Volkihar": ["vampire", "volkihar"], } ``` ### 6.2 Education Level Detection ```python def get_education_level(npc_profile: NPCProfile) -> str: """Determine if NPC gets full lore or basic summaries.""" educated_professions = {"mage", "scholar", "priest", "noble", "bard"} educated_factions = {"College of Winterhold", "Thalmor"} if npc_profile.profession in educated_professions: return "scholar" if any(f in educated_factions for f in npc_profile.factions): return "scholar" return "commoner" ``` --- ## 7. API Specification ### 7.1 Proxy Endpoints **POST /v1/chat/completions** (OpenRouter compatible) - Intercepts, enriches, forwards - Supports streaming **POST /v1/completions** (Legacy) - Same enrichment logic **GET /health** - Returns proxy + ChromaDB status **GET /stats** - Lore injection statistics - Cache hit rates - Average latency added ### 7.2 Configuration ```yaml # oghma-proxy.yaml proxy: host: 0.0.0.0 port: 8100 upstream: # OpenRouter url: https://openrouter.ai/api/v1 api_key: ${OPENROUTER_API_KEY} # Or local vLLM # url: http://localhost:8000/v1 chromadb: host: iris-dev.eachpath.local port: 35000 collection_lore: oghma_lore collection_basic: oghma_basic retrieval: max_results: 5 min_score: 0.6 injection: enabled: true position: after_bio # after_bio | before_conversation | system_suffix logging: level: INFO log_injections: true log_to_phoebe: true # Log to nimmerverse decision table ``` --- ## 8. Deployment Architecture ### 8.1 Host Options **Option A: Dedicated VM (Recommended)** ``` VM ID: 109 Hostname: oghma.eachpath.local IP: 10.0.40.109 Resources: 2 vCPU, 4GB RAM OS: Rocky Linux 10 ``` **Option B: Co-locate on phoebe-dev** ``` Hostname: phoebe-dev.eachpath.local Port: 8100 (alongside PostgreSQL 35432) Pros: No new VM, shared resources Cons: Resource contention ``` ### 8.2 Service Configuration ```ini # /etc/systemd/system/oghma-proxy.service [Unit] Description=Oghma RAG Proxy for SkyrimNet After=network.target [Service] Type=simple User=chrysalis WorkingDirectory=/opt/oghma-proxy ExecStart=/opt/oghma-proxy/venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8100 Restart=always RestartSec=5 Environment=OPENROUTER_API_KEY=your-key-here [Install] WantedBy=multi-user.target ``` ### 8.3 SkyrimNet Configuration Change In `config/OpenRouter.yaml`: ```yaml # Before openrouter: base_url: https://openrouter.ai/api/v1 # After openrouter: base_url: http://oghma.eachpath.local:8100/v1 # Or if running locally: # base_url: http://localhost:8100/v1 ``` --- ## 9. Data Pipeline: Oghma Ingestion ### 9.1 Google Sheets → ChromaDB Pipeline ```python # ingest_oghma.py import pandas as pd import chromadb from chromadb.config import Settings SHEET_ID = "1dcfctU-iOqprwy2BOc7___4Awteczgdlv8886KalPsQ" SHEETS = [ ("Figures/Gods", 0), ("Artifacts", 1), ("Locations - Whiterun", 2), # ... etc ] def ingest_sheet(sheet_name: str, gid: int, chroma_client): url = f"https://docs.google.com/spreadsheets/d/{SHEET_ID}/export?format=csv&gid={gid}" df = pd.read_csv(url) collection_lore = chroma_client.get_or_create_collection("oghma_lore") collection_basic = chroma_client.get_or_create_collection("oghma_basic") for _, row in df.iterrows(): topic = row['topic'] # Full lore for educated NPCs if pd.notna(row.get('topic_desc')): collection_lore.add( documents=[row['topic_desc']], ids=[f"{sheet_name}:{topic}"], metadatas=[{ "topic": topic, "category": sheet_name, "knowledge_classes": row.get('knowledge_class', ''), "tags": row.get('tags', ''), }] ) # Basic lore for commoners if pd.notna(row.get('topic_desc_basic')): collection_basic.add( documents=[row['topic_desc_basic']], ids=[f"{sheet_name}:{topic}:basic"], metadatas=[{ "topic": topic, "category": sheet_name, "knowledge_classes": row.get('knowledge_class_basic', ''), "tags": row.get('tags', ''), }] ) ``` ### 9.2 Embedding Model Use same embedding model as SkyrimNet memories for consistency: - **Model:** `all-MiniLM-L6-v2` (384 dimensions) - **Or:** Match whatever SkyrimNet uses in Memory.yaml --- ## 10. Implementation Phases ### Phase 1: Foundation (Week 1) - [ ] Set up oghma-proxy repository - [ ] Implement basic FastAPI proxy (passthrough mode) - [ ] Test with SkyrimNet (verify transparent forwarding) - [ ] Deploy on phoebe-dev for initial testing ### Phase 2: Oghma Ingestion (Week 1-2) - [ ] Write Google Sheets ingestion script - [ ] Ingest all Oghma sheets into iris-dev ChromaDB - [ ] Verify embeddings and metadata - [ ] Test semantic queries manually ### Phase 3: NPC Profile Extraction (Week 2) - [ ] Implement prompt parser for NPC context - [ ] Build knowledge class mapper - [ ] Test with various NPC types - [ ] Handle edge cases (unnamed NPCs, generic guards) ### Phase 4: RAG Integration (Week 2-3) - [ ] Implement ChromaDB query logic - [ ] Build lore injection formatter - [ ] Add education-level routing (scholar vs commoner) - [ ] Test end-to-end with SkyrimNet ### Phase 5: Polish & Deploy (Week 3) - [ ] Add streaming support - [ ] Implement caching (avoid re-querying same context) - [ ] Add metrics/logging to phoebe - [ ] Create dedicated VM or finalize deployment - [ ] Write operational runbook ### Phase 6: Iteration (Ongoing) - [ ] Tune retrieval parameters based on gameplay - [ ] Add more knowledge sources beyond Oghma - [ ] Consider contributing upstream to SkyrimNet --- ## 11. Success Metrics | Metric | Target | How to Measure | |--------|--------|----------------| | Lore accuracy | NPCs reference correct lore | Manual spot-checks | | Knowledge scoping | Guards don't know mage secrets | Test with various NPC types | | Latency overhead | < 200ms added | Proxy metrics | | Stability | 99.9% uptime | Service monitoring | | User experience | Immersion improved | Subjective gameplay testing | --- ## 12. Risks & Mitigations | Risk | Impact | Mitigation | |------|--------|------------| | Latency too high | Breaks dialogue flow | Cache aggressively, async prefetch | | Wrong lore injected | Immersion broken | Strict knowledge filtering, fallback to no injection | | ChromaDB down | No lore enrichment | Graceful degradation (passthrough mode) | | Prompt parsing fails | NPC profile unknown | Default to basic/generic lore | | Oghma data incomplete | Missing topics | Supplement with UESP scraping | --- ## 13. Future Enhancements 1. **Player profile tracking** — Remember what player has learned, NPCs can reference shared knowledge 2. **Gossip propagation** — Lore spreads through NPC network with degradation 3. **Dynamic lore updates** — Events in-game add to lore corpus 4. **Multi-source RAG** — Combine Oghma + UESP + custom worldbuilding 5. **Upstream contribution** — Propose RAG API to SkyrimNet author --- **Version:** 1.0 | **Created:** 2026-03-30 | **Updated:** 2026-03-30