Skip to content

Commit 1fa4f42

Browse files
committed
docs: add ARCHITECTURE.md — technical overview of FaultLine design
- Write-validated knowledge graph pipeline - Short-term (Qdrant) and long-term (PostgreSQL) memory layers - Fact classification (Class A/B/C) and validation pipeline - Graph + hierarchy traversal systems - Metadata-driven ontology with self-building rel_types - UUID-based deduplication and entity type inference - Production hardening and deployment guide Suitable for operators, integrators, and contributors understanding the system.
1 parent b6238b6 commit 1fa4f42

1 file changed

Lines changed: 338 additions & 0 deletions

File tree

‎ARCHITECTURE.md‎

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
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

Comments
 (0)