feat: Add Oghma RAG Proxy for SkyrimNet lore injection
RAG proxy that intercepts SkyrimNet LLM requests and enriches them with relevant Tamrielic lore from CHIM's Oghma Infinium database. Features: - FastAPI proxy compatible with OpenAI API - ChromaDB semantic search for lore retrieval - NPC profile extraction from SkyrimNet prompts - Google Sheets ingestion for CHIM's Oghma data - Kubernetes deployment manifests - Debug endpoint for RAG operation monitoring Collections ingested to iris-dev ChromaDB: - oghma_lore: 1951 entries (scholar knowledge) - oghma_basic: 1949 entries (commoner knowledge) - oghma_visual: 1151 entries (Omnisight perception) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
497
oghma-proxy/TECHNICAL-SPEC.md
Normal file
497
oghma-proxy/TECHNICAL-SPEC.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# 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<name>\w+) Bio\n- Gender: (?P<gender>\w+)\n- Race: (?P<race>\w+)"
|
||||
|
||||
# From system context
|
||||
r"You are (?P<name>[^,]+), a (?P<race>\w+) (?P<profession>\w+)"
|
||||
|
||||
# From faction mentions
|
||||
r"member of (?:the )?(?P<faction>[\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
|
||||
Reference in New Issue
Block a user