|
| 1 | +# FaultLine Architecture |
| 2 | + |
| 3 | +FaultLine is a **write-validated knowledge graph** pipeline that intercepts OpenWebUI conversations, extracts named entities and relationships, validates them against an ontology, and persists them to PostgreSQL. Qdrant serves as a derived vector index — facts flow from PostgreSQL → Qdrant via the background re-embedder for memory recall during the query phase. |
| 4 | + |
| 5 | +## System Overview |
| 6 | + |
| 7 | +``` |
| 8 | +OpenWebUI Function |
| 9 | + ├─ Extract facts from conversation |
| 10 | + ├─ Call /ingest (async) |
| 11 | + └─ Call /query (sync, before LLM sees message) |
| 12 | + ├─ PostgreSQL: baseline facts, graph traversal, hierarchy expansion |
| 13 | + ├─ Qdrant: vector similarity search |
| 14 | + └─ Inject ranked facts as system context |
| 15 | +``` |
| 16 | + |
| 17 | +## Memory Architecture: Short-term & Long-term |
| 18 | + |
| 19 | +**Short-term Memory (Qdrant — Vector Search)** |
| 20 | +- Fast, fuzzy semantic matching on recent/similar context |
| 21 | +- Class B (behavioral) facts: promoted after 3 confirmations |
| 22 | +- Class C (ephemeral) facts: expire after 30 days if unconfirmed |
| 23 | +- Immediate retrieval without confirmation waiting period |
| 24 | +- Useful for: "What was I working on?", "Who did I mention?" |
| 25 | + |
| 26 | +**Long-term Memory (PostgreSQL — Relational)** |
| 27 | +- Immutable, validated facts locked in forever |
| 28 | +- Class A (identity/structural) facts: name, relationships, properties |
| 29 | +- Non-destructive corrections via user-driven retractions |
| 30 | +- Query-backed by graph and hierarchy traversal |
| 31 | +- Useful for: "Where do I work?", "Who's my spouse?", "What are my kids' names?" |
| 32 | + |
| 33 | +**Both work together:** Short-term catches loose threads; long-term locks in truth. |
| 34 | + |
| 35 | +## Data Flow |
| 36 | + |
| 37 | +### Ingest Pipeline |
| 38 | + |
| 39 | +``` |
| 40 | +LLM extract → WGM gate → semantic conflict detection |
| 41 | + → bidirectional validation → Class A/B/C → commit |
| 42 | +``` |
| 43 | + |
| 44 | +1. **Extraction**: LLM converts conversation into typed relationship triples (subject, rel_type, object) |
| 45 | +2. **WGM Gate**: Validates against ontology (`rel_types` table) and type constraints |
| 46 | +3. **Conflict Detection**: Auto-supersedes facts when objects are already defined as types (e.g., can't own a type) |
| 47 | +4. **Bidirectional Validation**: Prevents impossible relationships (e.g., child_of + parent_of for same pair) |
| 48 | +5. **Classification**: Assigns to Class A (identity), Class B (behavioral), or Class C (novel/ephemeral) |
| 49 | +6. **Commit**: Writes to PostgreSQL and/or stages facts for promotion |
| 50 | + |
| 51 | +### Query Pipeline |
| 52 | + |
| 53 | +``` |
| 54 | +baseline facts → graph traversal → hierarchy expansion |
| 55 | + → Qdrant search → entity metadata → deduplication → rank & inject |
| 56 | +``` |
| 57 | + |
| 58 | +1. **Baseline**: Load all identity-anchored facts from PostgreSQL |
| 59 | +2. **Graph Traversal**: Single-hop connectivity across relationships (spouse, parent, works_for, etc.) |
| 60 | +3. **Hierarchy Expansion**: Walk classification chains upward (instance_of, subclass_of, part_of) |
| 61 | +4. **Vector Search**: Qdrant cosine similarity (nomic-embed-text, score ≥ 0.3) |
| 62 | +5. **Metadata**: Attach entity types and alias lists |
| 63 | +6. **Deduplication**: UUID-based merging (prevents duplicates from alias variation) |
| 64 | +7. **Injection**: Inject ranked facts as system context before model responds |
| 65 | + |
| 66 | +## Fact Classification (Phase 4) |
| 67 | + |
| 68 | +### Class A — Identity/Structural (Write-through, Immediate) |
| 69 | + |
| 70 | +Committed directly to `facts` table: |
| 71 | +- pref_name, also_known_as, same_as |
| 72 | +- parent_of, child_of, spouse, sibling_of |
| 73 | +- born_on, born_in, has_gender, nationality, occupation |
| 74 | +- instance_of, subclass_of |
| 75 | + |
| 76 | +**Confidence**: 1.0 if user-stated; 0.8 if LLM-inferred |
| 77 | +**User corrections always Class A** regardless of relationship type |
| 78 | +**Synced to Qdrant** by re-embedder after insertion |
| 79 | + |
| 80 | +### Class B — Behavioral/Contextual (Staged, Promoted on Confirmation) |
| 81 | + |
| 82 | +Staged to `staged_facts`, promoted when `confirmed_count >= 3`: |
| 83 | +- lives_at, lives_in, works_for, educated_at, owns |
| 84 | +- likes, dislikes, prefers, friend_of, knows, met |
| 85 | +- located_in, related_to, has_pet, part_of, created_by |
| 86 | + |
| 87 | +**Confidence**: 0.8 if user-stated; 0.6 if LLM-inferred |
| 88 | +**Lifecycle**: Staged → visible immediately in queries (no waiting) → promoted to facts when confirmed 3x |
| 89 | +**TTL**: Indefinite (once promoted) |
| 90 | +**Qdrant**: Synced immediately at ingest; staged point deleted after promotion |
| 91 | + |
| 92 | +### Class C — Ephemeral/Novel (Staged, Expiring) |
| 93 | + |
| 94 | +Staged with auto-expiry: |
| 95 | +- Anything not in A or B |
| 96 | +- Engine-generated relationship types |
| 97 | +- Confidence < 0.6 |
| 98 | + |
| 99 | +**Confidence**: 0.4 if LLM-inferred |
| 100 | +**Lifecycle**: Staged with `expires_at = now() + 30 days` |
| 101 | +**Expiry**: Auto-deleted by re-embedder if unconfirmed |
| 102 | +**No promotion path** — facts either confirm to B or expire |
| 103 | + |
| 104 | +## Validation Pipeline |
| 105 | + |
| 106 | +### Semantic Conflict Detection |
| 107 | + |
| 108 | +Prevents ownership/behavioral facts on type entities: |
| 109 | + |
| 110 | +``` |
| 111 | +If entity X instance_of TYPE_Y: |
| 112 | + Reject: owns(X), has_pet(X), works_for(X) |
| 113 | +``` |
| 114 | + |
| 115 | +**Principle**: If Y is a type/category, it's not a separate entity. Can't own a type. |
| 116 | + |
| 117 | +### Bidirectional Validation |
| 118 | + |
| 119 | +Prevents contradictory directional relationships: |
| 120 | + |
| 121 | +``` |
| 122 | +If child_of(A, B) exists and parent_of(A, B) attempted: |
| 123 | + Keep higher-confidence version, supersede lower |
| 124 | +``` |
| 125 | + |
| 126 | +### Metadata-Driven Validation |
| 127 | + |
| 128 | +All validation properties stored in `rel_types` table at runtime: |
| 129 | + |
| 130 | +- `is_symmetric`: Relationship goes both ways (spouse, friend_of) |
| 131 | +- `inverse_rel_type`: Opposite relationship (parent_of ↔ child_of) |
| 132 | +- `is_leaf_only`: Object can't have further relationships |
| 133 | +- `is_hierarchy_rel`: Part of classification chain |
| 134 | + |
| 135 | +**Zero hardcoded rules.** New rel_types self-describe constraints via metadata columns. |
| 136 | + |
| 137 | +## Traversal Systems (Orthogonal) |
| 138 | + |
| 139 | +### Graph Traversal (`_REL_TYPE_GRAPH`) |
| 140 | + |
| 141 | +Connectivity — who am I connected to? |
| 142 | + |
| 143 | +**Included rels**: spouse, parent_of, child_of, sibling_of, has_pet, knows, friend_of, met, works_for, lives_at, lives_in, located_in, owns, educated_at, member_of |
| 144 | + |
| 145 | +**Single-hop**: Fetches direct neighbors only |
| 146 | + |
| 147 | +### Hierarchy Traversal (`_REL_TYPE_HIERARCHY`) |
| 148 | + |
| 149 | +Composition & classification — what are they, what do they belong to? |
| 150 | + |
| 151 | +**Included rels**: instance_of, subclass_of, part_of, is_a, member_of |
| 152 | + |
| 153 | +**Bidirectional**: |
| 154 | +- `direction="up"`: Entity → class chain (fraggle → instance_of → dog → subclass_of → animal) |
| 155 | +- `direction="down"`: Class → members |
| 156 | + |
| 157 | +**Cycle protection** via depth tracking |
| 158 | + |
| 159 | +Both systems run in parallel during query. Results merged and deduplicated on `(subject_uuid, rel_type, object_uuid)`. |
| 160 | + |
| 161 | +## Ontology & Relationship Types |
| 162 | + |
| 163 | +Triple model: `(subject_id, rel_type, object_id)` aligned to Wikidata PIDs. |
| 164 | + |
| 165 | +### Symmetric (bidirectional) |
| 166 | +spouse, sibling_of, same_as, friend_of, knows, met |
| 167 | + |
| 168 | +### Inverse Pairs |
| 169 | +- parent_of ↔ child_of |
| 170 | +- (Others defined in `rel_types.inverse_rel_type`) |
| 171 | + |
| 172 | +### Self-Building |
| 173 | + |
| 174 | +Novel `rel_type` values: |
| 175 | +1. Classified as Class C initially |
| 176 | +2. Tracked in `ontology_evaluations` table |
| 177 | +3. Re-embedder evaluates asynchronously: |
| 178 | + - Frequency ≥ 3 occurrences → approve as new rel_type |
| 179 | + - Cosine similarity > 0.85 with existing type → map to existing |
| 180 | + - Otherwise → reject and expire |
| 181 | + |
| 182 | +**No manual approval required** — system self-builds ontology from usage patterns. |
| 183 | + |
| 184 | +## Database Schema |
| 185 | + |
| 186 | +### Core Tables |
| 187 | + |
| 188 | +**facts**: Immutable, validated facts |
| 189 | +- Unique: `(user_id, subject_id, object_id, rel_type)` |
| 190 | +- Soft-delete via `superseded_at IS NOT NULL` |
| 191 | + |
| 192 | +**staged_facts**: Unconfirmed or ephemeral facts awaiting promotion/expiry |
| 193 | +- Class B: promoted when `confirmed_count >= 3` |
| 194 | +- Class C: deleted when `expires_at <= now()` |
| 195 | + |
| 196 | +**entities**: Canonical entity registry with UUIDs |
| 197 | +- `id`: UUID v5 surrogate (never display name) |
| 198 | +- `entity_type`: Inferred type (Person, Location, Organization, etc.) |
| 199 | + |
| 200 | +**entity_aliases**: Authoritative name mapping |
| 201 | +- Unique: `(user_id, alias)` (case-insensitive, lowercased) |
| 202 | +- `is_preferred`: Single preferred name per entity |
| 203 | + |
| 204 | +**rel_types**: Live ontology with validation metadata |
| 205 | +- Loaded at startup into memory cache |
| 206 | +- Queried at runtime for constraint validation |
| 207 | +- Columns: `is_symmetric`, `inverse_rel_type`, `is_leaf_only`, `is_hierarchy_rel`, `head_types`, `tail_types` |
| 208 | + |
| 209 | +**entity_name_conflicts**: Collision detection & resolution |
| 210 | +- Triggered when two entities claim same preferred name |
| 211 | +- LLM-powered disambiguation loop |
| 212 | +- Non-destructive: all names preserved, only preferred status reassigned |
| 213 | + |
| 214 | +## Deduplication Strategy |
| 215 | + |
| 216 | +UUID-based, not name-based: |
| 217 | + |
| 218 | +1. All entity IDs stored as UUIDs (`_subject_id`, `_object_id`) |
| 219 | +2. Display names stored separately in `entity_aliases` |
| 220 | +3. Query dedup groups by `(subject_uuid, rel_type, object_uuid)` |
| 221 | +4. **Result**: Same entity with multiple aliases = single deduplicated fact |
| 222 | + |
| 223 | +Prevents hallucination from alias variation (chris/user/Christopher = one entity). |
| 224 | + |
| 225 | +## Entity Type Classification |
| 226 | + |
| 227 | +Three-layer inference: |
| 228 | + |
| 229 | +1. **GLiNER2 extraction**: Named entity recognition → types |
| 230 | +2. **Relationship semantics**: Type implied by rel_type (works_for → Person subject) |
| 231 | +3. **Descriptor context**: Inferred from conversation |
| 232 | + |
| 233 | +Type persisted to `entities.entity_type` only when current type = 'unknown'. |
| 234 | + |
| 235 | +## Relevance & Sensitivity |
| 236 | + |
| 237 | +**Identity facts always pass**: |
| 238 | +- pref_name, also_known_as, same_as |
| 239 | +- Relationship identities (spouse, parent_of, child_of, sibling_of) |
| 240 | + |
| 241 | +**Everything else** passes if `confidence ≥ 0.4` (tunable via `MIN_INJECT_CONFIDENCE`) |
| 242 | + |
| 243 | +**Sensitivity penalty** applied to PII: |
| 244 | +- born_on, lives_at, lives_in, height, weight, born_in |
| 245 | +- Penalized unless explicitly requested in query |
| 246 | + |
| 247 | +## Re-embedder Background Loop |
| 248 | + |
| 249 | +Runs continuously, polls database every `REEMBED_INTERVAL` seconds (default 10s). |
| 250 | + |
| 251 | +### Tasks |
| 252 | + |
| 253 | +1. **Promotion**: Class B facts with `confirmed_count >= 3` → facts table |
| 254 | +2. **Expiry**: Class C facts with `expires_at <= now()` → deleted |
| 255 | +3. **Embedding**: Unsynced facts → Qdrant upsert (nomic-embed-text-v1.5) |
| 256 | +4. **Ontology Evaluation**: Novel rel_types evaluated for approval |
| 257 | +5. **Name Conflict Resolution**: LLM-powered disambiguation |
| 258 | + |
| 259 | +Post-promotion, staged Qdrant point is deleted (best-effort, outside transaction). |
| 260 | + |
| 261 | +## Production Hardening |
| 262 | + |
| 263 | +**Startup Validation**: Checks POSTGRES_DSN, QDRANT_URL, LLM endpoints (non-fatal if missing) |
| 264 | + |
| 265 | +**Health Endpoint**: `/health` returns JSON with database, qdrant, llm, re-embedder status (5s cache) |
| 266 | + |
| 267 | +**Timeouts**: |
| 268 | +- HTTP: 10s (tunable `HTTPX_TIMEOUT`) |
| 269 | +- Database: 30s (tunable `DB_TIMEOUT`) |
| 270 | +- Qdrant: 10s (tunable `QDRANT_TIMEOUT`) |
| 271 | + |
| 272 | +**Rate Limiting**: Per-user_id tracking, 100 req/min default (tunable `RATE_LIMIT_PER_MIN`) |
| 273 | + |
| 274 | +**Query Fallback**: PostgreSQL-only response when embedding/Qdrant unavailable |
| 275 | + |
| 276 | +## Qdrant Collection Naming |
| 277 | + |
| 278 | +Per-user isolation: |
| 279 | +- `"anonymous"`, `""`, or `"legacy"` user_id → env `QDRANT_COLLECTION` (default `"faultline-test"`) |
| 280 | +- Any other user_id → `"faultline-{user_id}"` |
| 281 | + |
| 282 | +Ensures multi-tenant deployments remain isolated. |
| 283 | + |
| 284 | +## Key Design Principles |
| 285 | + |
| 286 | +- **LLM has no unsupervised write access** — all writes flow through WGM validation gate |
| 287 | +- **PostgreSQL is authoritative** — Qdrant is read-only derived view |
| 288 | +- **Validation is metadata-driven** — no hardcoded rules, all in `rel_types` table |
| 289 | +- **Write-time normalization** — entity IDs normalized to UUID v5 surrogates at ingestion |
| 290 | +- **UUID-based deduplication** — prevents hallucination from alias variation |
| 291 | +- **Non-destructive corrections** — user retractions soft-delete via `superseded_at`, never hard-delete |
| 292 | +- **Self-healing graph** — semantic conflicts auto-superseded, no manual intervention |
| 293 | +- **Dumb filter, smart backend** — OpenWebUI Function is stateless, backend does all ranking |
| 294 | + |
| 295 | +## Running |
| 296 | + |
| 297 | +```bash |
| 298 | +# Develop |
| 299 | +pip install -e ".[test]" |
| 300 | +uvicorn src.api.main:app --host 0.0.0.0 --port 8001 --reload |
| 301 | + |
| 302 | +# Test |
| 303 | +pytest tests/ --ignore=tests/evaluation --ignore=tests/feature_extraction \ |
| 304 | + --ignore=tests/model_inference --ignore=tests/preprocessing |
| 305 | + |
| 306 | +# Deploy |
| 307 | +docker compose up --build |
| 308 | +``` |
| 309 | + |
| 310 | +## Environment Variables |
| 311 | + |
| 312 | +``` |
| 313 | +# Database |
| 314 | +POSTGRES_DSN=postgresql://user:pass@localhost:5432/faultline |
| 315 | +POSTGRES_USER=faultline |
| 316 | +POSTGRES_PASSWORD=faultline |
| 317 | +POSTGRES_DB=faultline |
| 318 | +
|
| 319 | +# LLM (extraction & conflict resolution) |
| 320 | +QWEN_API_URL=http://localhost:11434/v1/chat/completions |
| 321 | +WGM_LLM_MODEL=qwen/qwen3.5-9b |
| 322 | +CATEGORY_LLM_MODEL=qwen2.5-coder |
| 323 | +
|
| 324 | +# Vector Search |
| 325 | +QDRANT_URL=http://qdrant:6333 |
| 326 | +QDRANT_COLLECTION=faultline-test |
| 327 | +
|
| 328 | +# Performance Tuning |
| 329 | +REEMBED_INTERVAL=10 |
| 330 | +HTTPX_TIMEOUT=10 |
| 331 | +DB_TIMEOUT=30 |
| 332 | +QDRANT_TIMEOUT=10 |
| 333 | +DB_POOL_SIZE=10 |
| 334 | +RATE_LIMIT_PER_MIN=100 |
| 335 | +
|
| 336 | +# API |
| 337 | +FAULTLINE_API_URL=http://localhost:8001 |
| 338 | +``` |
0 commit comments