@adaptic/maestro 1.1.6 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,459 @@
1
+ # RAG & Context Retrieval Setup Guide
2
+
3
+ How the agent's memory and context retrieval system works: the SQLite FTS5 search index, per-user access-scoped search, pre-draft context enrichment, post-interaction entity extraction, and the integration points with every send script.
4
+
5
+ **Prerequisites**: Complete the [Mac Mini Bootstrap](../runbooks/mac-mini-bootstrap.md). The RAG system uses only Python standard library plus optional PyYAML — no vector database or embedding API required.
6
+
7
+ ---
8
+
9
+ ## Architecture Overview
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────────────────────┐
13
+ │ INDEXING (write path) │
14
+ │ │
15
+ │ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
16
+ │ │ rag-indexer.py │───▶│ state/rag/search.db │ │
17
+ │ │ (incremental FTS5) │ │ (SQLite FTS5 full-text search) │ │
18
+ │ └──────────────────────┘ └─────────────────────────────────┘ │
19
+ │ ▲ │
20
+ │ │ sources: │
21
+ │ ├── memory/interactions/**/*.jsonl (Slack, email, calls) │
22
+ │ ├── knowledge/sources/*.yaml (public knowledge) │
23
+ │ ├── knowledge/decisions/*.yaml (leadership+) │
24
+ │ ├── knowledge/syntheses/*.yaml (strategy = ceo-only) │
25
+ │ └── state/queues/*.yaml (leadership+) │
26
+ │ │
27
+ │ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
28
+ │ │ post-interaction- │───▶│ memory/indexes/ │ │
29
+ │ │ indexer.py │ │ entity-relationships.yaml │ │
30
+ │ │ (entity extraction) │ │ (people, orgs, topics, facts) │ │
31
+ │ └──────────────────────┘ └─────────────────────────────────┘ │
32
+ ├─────────────────────────────────────────────────────────────────────┤
33
+ │ RETRIEVAL (read path) │
34
+ │ │
35
+ │ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
36
+ │ │ user-context-search │◀──│ config/caller-id-map.yaml │ │
37
+ │ │ .py │ │ (user → access level mapping) │ │
38
+ │ │ (grep or sqlite) │ └─────────────────────────────────┘ │
39
+ │ └──────────────────────┘ │
40
+ │ │
41
+ │ ┌──────────────────────┐ ┌─────────────────────────────────┐ │
42
+ │ │ pre-draft-context.py │───▶│ Entity context for recipient │ │
43
+ │ │ (fact retrieval) │ │ (< 500ms, file reads only) │ │
44
+ │ └──────────────────────┘ └─────────────────────────────────┘ │
45
+ │ │ │
46
+ │ ▼ │
47
+ │ ┌──────────────────────┐ Integrated into every send script: │
48
+ │ │ pre_draft_lookup.py │ send-email-threaded.py, slack-send.sh │
49
+ │ │ (wrapper + audit log) │ send-whatsapp.sh, send-sms.sh │
50
+ │ └──────────────────────┘ │
51
+ └─────────────────────────────────────────────────────────────────────┘
52
+ ```
53
+
54
+ **Key design choice**: The RAG system uses **SQLite FTS5** (full-text search) rather than vector embeddings. This means zero external dependencies, sub-millisecond search latency, no API calls during retrieval, and a database that's a single file you can back up with `cp`. The trade-off is keyword-based matching rather than semantic search — but for structured operational data (names, topics, dates), keyword search is more reliable.
55
+
56
+ ---
57
+
58
+ ## 1. The Search Index (`rag-indexer.py`)
59
+
60
+ ### 1.1 What It Indexes
61
+
62
+ | Source | Path Pattern | User Scope | Content |
63
+ |---|---|---|---|
64
+ | Interactions | `memory/interactions/**/*.jsonl` | Per-user (DMs) or channel-scoped | Slack messages, emails, call transcripts |
65
+ | Public knowledge | `knowledge/sources/*.yaml` | `public` | Company facts, product info |
66
+ | Decisions | `knowledge/decisions/*.yaml` | `leadership` | Strategic decisions with rationale |
67
+ | Syntheses | `knowledge/syntheses/*.yaml` | `public` (except strategy-state = `ceo`) | Analysis summaries |
68
+ | Queues | `state/queues/*.yaml` | `leadership` | Action items, follow-ups, blockers |
69
+
70
+ ### 1.2 User Scope (Ring-Fencing)
71
+
72
+ Every indexed document gets a `user_scope` that controls who can retrieve it:
73
+
74
+ | Scope | Visible To | Example Content |
75
+ |---|---|---|
76
+ | `public` | All authenticated users | Company overview, public knowledge |
77
+ | `leadership` | Leadership + CEO | Decisions, queue items, internal docs |
78
+ | `ceo` | CEO only | Strategy state, all interactions, all logs |
79
+ | `{user-slug}` | That specific user only | Their DMs, their emails |
80
+
81
+ This ensures that when the agent is on a voice call with a partner, it can't accidentally surface confidential CEO-only information.
82
+
83
+ ### 1.3 Database Schema
84
+
85
+ SQLite database at `state/rag/search.db`:
86
+
87
+ ```sql
88
+ -- Main document table
89
+ documents (
90
+ id TEXT PRIMARY KEY, -- SHA-256 hash of source_path + content
91
+ source_type TEXT NOT NULL, -- "interaction", "knowledge", "decision", etc.
92
+ source_path TEXT NOT NULL, -- Relative path to source file
93
+ user_scope TEXT NOT NULL, -- Access scope (public, leadership, ceo, user-slug)
94
+ content TEXT NOT NULL, -- Full text content
95
+ title TEXT, -- Document title (if available)
96
+ timestamp TEXT, -- ISO 8601 timestamp
97
+ metadata_json TEXT -- Additional metadata as JSON
98
+ );
99
+
100
+ -- FTS5 virtual table (full-text search)
101
+ documents_fts USING fts5(content, title);
102
+ ```
103
+
104
+ Triggers keep the FTS index in sync with the main table automatically.
105
+
106
+ ### 1.4 Running the Indexer
107
+
108
+ ```bash
109
+ # Incremental index (only changed files since last run)
110
+ python3 scripts/rag-indexer.py
111
+
112
+ # Full re-index (drop and rebuild everything)
113
+ python3 scripts/rag-indexer.py --full
114
+
115
+ # Show index statistics
116
+ python3 scripts/rag-indexer.py --stats
117
+
118
+ # Custom database path
119
+ python3 scripts/rag-indexer.py --db /path/to/custom.db
120
+ ```
121
+
122
+ ### 1.5 Incremental Indexing
123
+
124
+ The indexer tracks file modification times in `state/rag/index-state.json`. On each run, it only re-indexes files that have changed since the last run. A `--full` flag forces a complete rebuild.
125
+
126
+ ### 1.6 Performance
127
+
128
+ - WAL mode enabled (`PRAGMA journal_mode=WAL`) for concurrent read/write
129
+ - Synchronous mode set to NORMAL for faster writes
130
+ - Typical index time: < 5 seconds for incremental, < 30 seconds for full rebuild on ~10K documents
131
+
132
+ ---
133
+
134
+ ## 2. User Context Search (`user-context-search.py`)
135
+
136
+ Ring-fenced search that respects user access levels.
137
+
138
+ ### 2.1 How It Works
139
+
140
+ 1. Looks up the user in `config/caller-id-map.yaml` to determine access level
141
+ 2. Maps access level to searchable paths/scopes
142
+ 3. Executes search using one of two backends:
143
+ - **grep** (default): File-based grep with scoped directory paths
144
+ - **sqlite**: FTS5 query against the pre-indexed database
145
+
146
+ ### 2.2 Usage
147
+
148
+ ```bash
149
+ # Search as CEO (full access)
150
+ python3 scripts/user-context-search.py --user mehran --query "DFSA submission"
151
+
152
+ # Search as leadership (scoped access)
153
+ python3 scripts/user-context-search.py --user hootan --query "portal" --max-results 5
154
+
155
+ # Search as unknown user (public only)
156
+ python3 scripts/user-context-search.py --user unknown --query "adaptic"
157
+
158
+ # Use SQLite FTS5 backend
159
+ python3 scripts/user-context-search.py --user mehran --query "flight" --backend sqlite
160
+
161
+ # Text output format (vs JSON default)
162
+ python3 scripts/user-context-search.py --user mehran --query "test" --format text
163
+ ```
164
+
165
+ ### 2.3 Access Level → Searchable Paths
166
+
167
+ | Access Level | Directories Searched |
168
+ |---|---|
169
+ | `ceo` | Everything: `memory/`, `knowledge/`, `state/`, `outputs/`, `docs/`, `logs/` |
170
+ | `leadership` | `knowledge/` (company), `docs/`, research/briefs, own interaction logs |
171
+ | `partner` | `knowledge/sources/` (public), research, own interaction logs |
172
+ | `default` | `knowledge/sources/` only |
173
+
174
+ ### 2.4 User Resolution
175
+
176
+ Users can be identified by:
177
+ - Username slug (e.g. `mehran`)
178
+ - Slack ID (e.g. `U099N1JGKRQ`)
179
+ - Email address (e.g. `mehran@adaptic.ai`)
180
+ - Phone number (e.g. `+971585291799`)
181
+
182
+ All resolve to the same user entry in `caller-id-map.yaml`.
183
+
184
+ ### 2.5 No External Dependencies
185
+
186
+ The search script uses only Python standard library. PyYAML is used if available (faster), but a built-in YAML parser handles the `caller-id-map.yaml` structure if PyYAML is missing.
187
+
188
+ ---
189
+
190
+ ## 3. Pre-Draft Context (`pre-draft-context.py`)
191
+
192
+ Retrieves relevant context about a recipient **before** composing a message. This is the core of the "know before you write" principle.
193
+
194
+ ### 3.1 What It Retrieves
195
+
196
+ 1. **Entity index lookup** — Finds the recipient in `memory/indexes/entity-relationships.yaml`
197
+ 2. **User profile** — Loads `memory/profiles/users/{slug}.yaml` for standing instructions, preferences, tone
198
+ 3. **Recent interactions** — Checks `memory/interactions/` for recent conversation history
199
+ 4. **Queue context** — Checks `state/queues/` for related open items
200
+ 5. **Disclosure boundaries** — Generates per-recipient information boundaries
201
+
202
+ ### 3.2 Usage
203
+
204
+ ```bash
205
+ # Full JSON context
206
+ python3 scripts/pre-draft-context.py --recipient "Graham Syder"
207
+
208
+ # With topic focus
209
+ python3 scripts/pre-draft-context.py --recipient "Hootan" --topic "DFSA compliance"
210
+
211
+ # Brief format (3-5 lines, suitable for prompt injection)
212
+ python3 scripts/pre-draft-context.py --recipient "Nima" --topic "83b election" --format brief
213
+
214
+ # By email address
215
+ python3 scripts/pre-draft-context.py --recipient "hootan@adaptic.ai" --format brief
216
+ ```
217
+
218
+ ### 3.3 Performance
219
+
220
+ Target: **< 500ms** per lookup. Achieves this by:
221
+ - File reads only (no API calls)
222
+ - JSON cache of entity index (`.entity-relationships.json` alongside the YAML)
223
+ - PyYAML C loader when available (10x faster than pure Python)
224
+
225
+ ### 3.4 Exit Codes
226
+
227
+ | Code | Meaning |
228
+ |---|---|
229
+ | 0 | Context found |
230
+ | 1 | Recipient not found |
231
+ | 2 | Error |
232
+
233
+ ---
234
+
235
+ ## 4. Pre-Draft Lookup Wrapper (`pre_draft_lookup.py`)
236
+
237
+ Thin wrapper around `pre-draft-context.py` designed for import into send scripts.
238
+
239
+ ### 4.1 What It Adds
240
+
241
+ 1. Calls `retrieve_context()` from `pre-draft-context.py`
242
+ 2. Generates disclosure boundaries for the recipient (via `disclosure_boundaries.py`)
243
+ 3. Logs the lookup result to `logs/audit/YYYY-MM-DD-pre-draft-lookups.jsonl`
244
+ 4. Returns context dict or `None` — **never blocks a send**
245
+
246
+ ### 4.2 Python Import
247
+
248
+ ```python
249
+ from pre_draft_lookup import pre_draft_lookup
250
+
251
+ context = pre_draft_lookup("Mehran Granfar", message_type="slack")
252
+ if context:
253
+ # context contains entity info, user profile, recent interactions
254
+ pass
255
+ ```
256
+
257
+ ### 4.3 CLI Usage (for Shell Scripts)
258
+
259
+ ```bash
260
+ python3 scripts/pre_draft_lookup.py --recipient "Mehran Granfar" --type slack --channel C099ABC
261
+ ```
262
+
263
+ ### 4.4 Integration Points
264
+
265
+ Called automatically by these send scripts:
266
+ - `send-email-threaded.py` — imported as Python module
267
+ - `send-email-with-attachment.py` — imported as Python module
268
+ - `slack-send.sh` — called as subprocess before sending
269
+ - `send-whatsapp.sh` — called via `validate-outbound.py` pipeline
270
+
271
+ The lookup is **advisory only** — it enriches the agent's context but never prevents a send.
272
+
273
+ ---
274
+
275
+ ## 5. Post-Interaction Indexer (`post-interaction-indexer.py`)
276
+
277
+ Automatically extracts entities, relationships, and facts from interactions and updates the entity index.
278
+
279
+ ### 5.1 What It Extracts
280
+
281
+ From each interaction (email, Slack message, call transcript):
282
+ - **People** mentioned (names, titles, roles)
283
+ - **Organisations** referenced
284
+ - **Topics** discussed
285
+ - **Facts** stated (dates, decisions, commitments)
286
+ - **Relationships** between entities
287
+
288
+ ### 5.2 When It Runs
289
+
290
+ | Trigger | How |
291
+ |---|---|
292
+ | Session end | `session-end-log.sh` hook spawns it in background with `--scan-today` |
293
+ | After inbox processing | Inbox processor calls it per-item |
294
+ | After backlog execution | Backlog executor calls it for interaction-heavy tasks |
295
+ | Manual backfill | Run with `--input` for specific files |
296
+
297
+ ### 5.3 Usage
298
+
299
+ ```bash
300
+ # Process a specific inbox item
301
+ python3 scripts/post-interaction-indexer.py --input state/inbox/slack/1775230363.978999-dm.json
302
+
303
+ # Process all unprocessed interactions from today
304
+ python3 scripts/post-interaction-indexer.py --scan-today
305
+
306
+ # Dry run (show extractions without writing)
307
+ python3 scripts/post-interaction-indexer.py --input <file> --dry-run
308
+
309
+ # Process from stdin
310
+ echo '{"event_type":"email","email":{"from":"Jane <jane@co.com>"}}' | \
311
+ python3 scripts/post-interaction-indexer.py --stdin
312
+ ```
313
+
314
+ ### 5.4 Output
315
+
316
+ Updates `memory/indexes/entity-relationships.yaml` with new entities and relationships. Uses a JSON cache (`.entity-relationships.json`) for fast reads.
317
+
318
+ ### 5.5 Performance Target
319
+
320
+ < 2 seconds per interaction (regex-based extraction, no API calls).
321
+
322
+ ---
323
+
324
+ ## 6. Directory Structure
325
+
326
+ ```
327
+ state/rag/
328
+ search.db # SQLite FTS5 database
329
+ index-state.json # File modification tracking for incremental indexing
330
+
331
+ memory/
332
+ interactions/ # Raw interaction logs (JSONL)
333
+ slack/
334
+ {channel}/{date}.jsonl
335
+ email/
336
+ {user}/{date}.jsonl
337
+ indexes/
338
+ entity-relationships.yaml # Extracted entities and relationships
339
+ .entity-relationships.json # JSON cache for fast reads
340
+ profiles/
341
+ users/{slug}.yaml # Per-user preferences, standing instructions
342
+ channels/{name}.yaml # Per-channel tone, rules
343
+
344
+ knowledge/
345
+ sources/ # Public company knowledge (yaml)
346
+ decisions/ # Strategic decisions (leadership+)
347
+ syntheses/ # Analysis summaries
348
+
349
+ config/
350
+ caller-id-map.yaml # User → access level mapping
351
+ ```
352
+
353
+ ---
354
+
355
+ ## 7. Testing
356
+
357
+ ### 7.1 RAG Search Tests
358
+
359
+ ```bash
360
+ # Run the full RAG search test suite
361
+ ./scripts/test-rag-search.sh
362
+ ```
363
+
364
+ Tests:
365
+ 1. CEO (mehran) gets broad results
366
+ 2. Leadership (hootan) gets scoped results (no private CEO data)
367
+ 3. Unknown user gets minimal access (knowledge/sources only)
368
+ 4. Slack ID resolution works
369
+ 5. Email resolution works
370
+
371
+ ### 7.2 Pre-Draft Integration Test
372
+
373
+ ```bash
374
+ python3 scripts/test-pre-draft-integration.py
375
+ ```
376
+
377
+ ### 7.3 Manual Verification
378
+
379
+ | # | Test | How to Verify |
380
+ |---|---|---|
381
+ | 1 | Index builds | `python3 scripts/rag-indexer.py --stats` — should show document counts |
382
+ | 2 | Incremental works | Run indexer twice; second run should skip unchanged files |
383
+ | 3 | Full rebuild | `python3 scripts/rag-indexer.py --full` — should recreate DB |
384
+ | 4 | CEO search | `python3 scripts/user-context-search.py --user mehran --query "test"` |
385
+ | 5 | Scoped search | Leadership user shouldn't see CEO-only docs |
386
+ | 6 | Pre-draft context | `python3 scripts/pre-draft-context.py --recipient "Mehran"` |
387
+ | 7 | Lookup wrapper | `python3 scripts/pre_draft_lookup.py --recipient "Mehran" --type slack` |
388
+ | 8 | Post-interaction | `python3 scripts/post-interaction-indexer.py --scan-today` |
389
+ | 9 | Audit logging | Check `logs/audit/YYYY-MM-DD-pre-draft-lookups.jsonl` |
390
+
391
+ ---
392
+
393
+ ## 8. Troubleshooting
394
+
395
+ ### "No results found" for a query you know exists
396
+
397
+ 1. Check the index is built: `python3 scripts/rag-indexer.py --stats`
398
+ 2. If stats show 0 documents, run a full index: `python3 scripts/rag-indexer.py --full`
399
+ 3. Check the content is in an indexed directory (see section 1.1)
400
+ 4. Try the grep backend: `--backend grep` may find files not yet indexed
401
+
402
+ ### Recipient not found in pre-draft context
403
+
404
+ 1. Check entity index exists: `ls memory/indexes/entity-relationships.yaml`
405
+ 2. Run the post-interaction indexer: `python3 scripts/post-interaction-indexer.py --scan-today`
406
+ 3. Check if the recipient has a user profile: `ls memory/profiles/users/`
407
+ 4. Names must match — try variations (full name, email, slug)
408
+
409
+ ### Search too slow
410
+
411
+ 1. Use the sqlite backend instead of grep: `--backend sqlite`
412
+ 2. Rebuild the FTS index: `python3 scripts/rag-indexer.py --full`
413
+ 3. Check DB size: `ls -lh state/rag/search.db`
414
+
415
+ ### PyYAML not available
416
+
417
+ The search script works without PyYAML using a built-in minimal parser. However, the post-interaction indexer **requires** PyYAML:
418
+
419
+ ```bash
420
+ pip3 install pyyaml
421
+ ```
422
+
423
+ ### Index state corrupted
424
+
425
+ Delete and rebuild:
426
+
427
+ ```bash
428
+ rm state/rag/index-state.json
429
+ python3 scripts/rag-indexer.py --full
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Key Files
435
+
436
+ | File | Purpose |
437
+ |---|---|
438
+ | `scripts/rag-indexer.py` | SQLite FTS5 indexer (incremental + full rebuild) |
439
+ | `scripts/user-context-search.py` | Per-user ring-fenced search (grep or sqlite) |
440
+ | `scripts/pre-draft-context.py` | Pre-send recipient context retrieval |
441
+ | `scripts/pre_draft_lookup.py` | Wrapper for send script integration + audit logging |
442
+ | `scripts/post-interaction-indexer.py` | Entity/relationship extraction from interactions |
443
+ | `scripts/test-rag-search.sh` | RAG search test suite |
444
+ | `scripts/test-rag-phase2.sh` | Phase 2 (FTS5) test suite |
445
+ | `scripts/test-pre-draft-integration.py` | Pre-draft integration tests |
446
+ | `config/caller-id-map.yaml` | User → access level mapping |
447
+ | `state/rag/search.db` | SQLite FTS5 database |
448
+ | `state/rag/index-state.json` | Incremental indexing state |
449
+ | `memory/indexes/entity-relationships.yaml` | Extracted entity index |
450
+
451
+ ---
452
+
453
+ ## Related Documents
454
+
455
+ - [Outbound Governance Setup](outbound-governance-setup.md) — How pre-draft context feeds into disclosure assessment
456
+ - [Email Setup](email-setup.md) — Email send scripts that consume pre-draft context
457
+ - [Slack Setup](slack-setup.md) — Slack send script pre-draft integration
458
+ - [Voice & SMS Setup](voice-sms-setup.md) — Caller ID mapping shared with RAG access control
459
+ - [Poller & Daemon Setup](poller-daemon-setup.md) — Session-end hook triggers post-interaction indexer