Changeset 0.18.0 (#79)
This commit is contained in:
235
docs/DESIGN-0.18.0.md
Normal file
235
docs/DESIGN-0.18.0.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# DESIGN-0.18.0: Memory System
|
||||
|
||||
**Version:** 0.18.0
|
||||
**Status:** Phase 1 Complete
|
||||
**Depends on:** compaction (v0.15.0), knowledge bases (v0.14.0), persona-KB binding (v0.17.0), SQLite backend (v0.17.1)
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Memory provides long-term fact persistence across conversations. Unlike
|
||||
compaction (within-conversation context) or knowledge bases (static
|
||||
documents), memory captures preferences, facts, and context that
|
||||
accumulate over time from natural conversation.
|
||||
|
||||
**Key differentiator: Persona-scoped memory.** Each Persona builds
|
||||
its own memory independently, preventing cross-context contamination.
|
||||
A helpdesk Persona accumulates FAQ knowledge; a tutoring Persona
|
||||
tracks per-student progress; a user's personal preferences stay
|
||||
separate from any Persona context.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope Model
|
||||
|
||||
Three memory scopes with clear ownership:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ user scope │
|
||||
│ owner_id = user_id │
|
||||
│ "Jeff prefers Go over Python" │
|
||||
│ "Deployment uses Kubernetes + Traefik" │
|
||||
│ Visible in ALL conversations for this user │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ persona scope │
|
||||
│ owner_id = persona_id │
|
||||
│ "Common question: how to reset password" │
|
||||
│ "Policy: refunds within 30 days only" │
|
||||
│ Shared across ALL users of this Persona │
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ persona_user scope │
|
||||
│ owner_id = persona_id, user_id = user_id │
|
||||
│ "Student struggles with recursion" │
|
||||
│ "Customer prefers email over phone" │
|
||||
│ Per-user within a specific Persona │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Scope resolution at recall time:**
|
||||
|
||||
When a Persona is active, all three scopes are queried and merged
|
||||
with priority ordering: persona_user > persona > user. When no
|
||||
Persona is active, only user scope is queried.
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Model
|
||||
|
||||
### 3.1 Table: `memories`
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `id` | UUID / TEXT | Primary key |
|
||||
| `scope` | TEXT | `user`, `persona`, `persona_user` |
|
||||
| `owner_id` | UUID / TEXT | user_id (user scope) or persona_id (persona/persona_user) |
|
||||
| `user_id` | UUID / TEXT | Only for persona_user scope — identifies which user |
|
||||
| `key` | TEXT | Short label (2-6 words) |
|
||||
| `value` | TEXT | The fact/detail |
|
||||
| `source_channel_id` | UUID / TEXT | Provenance — which conversation |
|
||||
| `confidence` | REAL | 0.0-1.0, LLM-provided |
|
||||
| `status` | TEXT | active, pending_review, archived |
|
||||
| `embedding` | vector(3072) / TEXT | For semantic recall (Phase 2) |
|
||||
| `created_at` | TIMESTAMPTZ / TEXT | |
|
||||
| `updated_at` | TIMESTAMPTZ / TEXT | |
|
||||
|
||||
### 3.2 Indexes
|
||||
|
||||
- **Primary lookup:** `(scope, owner_id, status)` — filtered to `status = 'active'`
|
||||
- **Persona+user:** `(owner_id, user_id)` — filtered to `scope = 'persona_user'`
|
||||
- **Dedup:** unique on `(scope, owner_id, COALESCE(user_id, nil_uuid), key)`
|
||||
- **FTS:** GIN on `to_tsvector(key || ' ' || value)` (Postgres) / LIKE fallback (SQLite)
|
||||
- **Provenance:** `(source_channel_id)` — "where did this memory come from?"
|
||||
|
||||
### 3.3 Model Struct
|
||||
|
||||
```go
|
||||
type Memory struct {
|
||||
ID string `json:"id"`
|
||||
Scope string `json:"scope"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
UserID *string `json:"user_id,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
SourceChannelID *string `json:"source_channel_id,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Store Interface
|
||||
|
||||
```go
|
||||
type MemoryStore interface {
|
||||
Upsert(ctx, *Memory) error // create or update (matched on scope+owner+user+key)
|
||||
GetByID(ctx, id) (*Memory, error)
|
||||
List(ctx, MemoryFilter) ([]Memory, error)
|
||||
Delete(ctx, id) error // hard delete
|
||||
Archive(ctx, id) error // soft delete (status → archived)
|
||||
Recall(ctx, userID, *personaID, query, limit) ([]Memory, error) // scope-merged search
|
||||
CountByOwner(ctx, scope, ownerID) (int, error)
|
||||
}
|
||||
```
|
||||
|
||||
**Key design decisions:**
|
||||
|
||||
- `Upsert` uses the unique index for conflict resolution — same key = update value
|
||||
- `Recall` does a UNION across all applicable scopes with priority ordering
|
||||
- Both Postgres and SQLite implementations exist (same pattern as all other stores)
|
||||
- SQLite falls back to LIKE for text search (no tsvector)
|
||||
|
||||
---
|
||||
|
||||
## 5. Tools
|
||||
|
||||
### 5.1 `memory_save`
|
||||
|
||||
The LLM calls this to explicitly store a fact. Scope is automatically
|
||||
determined by whether a Persona is active:
|
||||
|
||||
- No Persona → `user` scope, `owner_id = user_id`
|
||||
- Persona active → `persona_user` scope, `owner_id = persona_id`, `user_id = user_id`
|
||||
|
||||
Parameters: `key` (required), `value` (required), `confidence` (optional, default 1.0)
|
||||
|
||||
### 5.2 `memory_recall`
|
||||
|
||||
The LLM calls this to search stored memories. Results merge across
|
||||
all applicable scopes.
|
||||
|
||||
Parameters: `query` (optional), `max_results` (optional, default 20)
|
||||
|
||||
### 5.3 Registration
|
||||
|
||||
Late registration pattern (same as notes, KB search, conversation search):
|
||||
|
||||
```go
|
||||
// main.go
|
||||
tools.RegisterMemoryTools(stores)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Memory Injection
|
||||
|
||||
At completion time, `loadConversation()` injects relevant memories
|
||||
into the system prompt — same position as KB hints and compaction
|
||||
summaries:
|
||||
|
||||
```
|
||||
[admin system prompt]
|
||||
[persona/channel system prompt]
|
||||
[KB hint]
|
||||
[memory injection] ← NEW
|
||||
[compaction summary OR message history]
|
||||
```
|
||||
|
||||
`BuildMemoryHint()` calls `Recall()` with an empty query (returns
|
||||
top memories by confidence/recency), formats as bullet points, and
|
||||
caps at ~1500 tokens (~6000 chars) to preserve context budget.
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Phases
|
||||
|
||||
### Phase 1: Foundation & Tools ✅ (this delivery)
|
||||
- Migrations (Postgres + SQLite)
|
||||
- Memory model struct
|
||||
- MemoryStore interface + both implementations
|
||||
- memory_save + memory_recall tools
|
||||
- Memory injection in loadConversation
|
||||
- 9 new files, ~5 edits to existing files
|
||||
|
||||
### Phase 2: Automatic Extraction & Embeddings
|
||||
- Background job: post-conversation analysis extracts facts
|
||||
- Configurable extraction prompt per Persona
|
||||
- Extracted memories start as `pending_review`
|
||||
- Embedding generation for semantic recall
|
||||
- Recall upgraded to use vector similarity + keyword hybrid
|
||||
|
||||
### Phase 3: Review Pipeline & User Controls
|
||||
- Admin review queue for pending memories
|
||||
- Persona memory review for team admins
|
||||
- User Settings → Memory: view, edit, delete
|
||||
- Per-Persona memory toggle
|
||||
- Retention policies
|
||||
- Frontend memory management UI
|
||||
|
||||
---
|
||||
|
||||
## 8. Persona Memory Saves (Future: Phase 2)
|
||||
|
||||
Phase 1 only supports `user` and `persona_user` scopes via the tool
|
||||
(the LLM saves about the user, scoped appropriately). Phase 2 adds
|
||||
the ability for admins/team leads to configure Persona-scope memory
|
||||
extraction — the Persona "learns" from conversations:
|
||||
|
||||
```
|
||||
Persona system prompt addition (Phase 2):
|
||||
"After each conversation, identify key facts, common questions,
|
||||
and useful patterns. Save these as persona-scope memories for
|
||||
future reference by all users."
|
||||
```
|
||||
|
||||
This runs as a background job, not during the live conversation,
|
||||
to avoid latency impact.
|
||||
|
||||
---
|
||||
|
||||
## 9. Privacy & Security
|
||||
|
||||
- User memories never cross team boundaries
|
||||
- Persona memories respect group access controls
|
||||
- persona_user memories are only visible to the specific user + admins
|
||||
- Users can view/delete their own memories (Phase 3 UI)
|
||||
- Admin can disable memory system globally or per-team
|
||||
- No cross-persona memory leakage — scope isolation is enforced at the query level
|
||||
Reference in New Issue
Block a user