remdb 0.3.114__py3-none-any.whl → 0.3.172__py3-none-any.whl

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.

Potentially problematic release.


This version of remdb might be problematic. Click here for more details.

Files changed (83) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +311 -0
  3. rem/agentic/agents/sse_simulator.py +2 -0
  4. rem/agentic/context.py +103 -5
  5. rem/agentic/context_builder.py +36 -9
  6. rem/agentic/mcp/tool_wrapper.py +161 -18
  7. rem/agentic/otel/setup.py +1 -0
  8. rem/agentic/providers/phoenix.py +371 -108
  9. rem/agentic/providers/pydantic_ai.py +172 -30
  10. rem/agentic/schema.py +8 -4
  11. rem/api/deps.py +3 -5
  12. rem/api/main.py +26 -4
  13. rem/api/mcp_router/resources.py +15 -10
  14. rem/api/mcp_router/server.py +11 -3
  15. rem/api/mcp_router/tools.py +418 -4
  16. rem/api/middleware/tracking.py +5 -5
  17. rem/api/routers/admin.py +218 -1
  18. rem/api/routers/auth.py +349 -6
  19. rem/api/routers/chat/completions.py +255 -7
  20. rem/api/routers/chat/models.py +81 -7
  21. rem/api/routers/chat/otel_utils.py +33 -0
  22. rem/api/routers/chat/sse_events.py +17 -1
  23. rem/api/routers/chat/streaming.py +126 -19
  24. rem/api/routers/feedback.py +134 -14
  25. rem/api/routers/messages.py +24 -15
  26. rem/api/routers/query.py +6 -3
  27. rem/auth/__init__.py +13 -3
  28. rem/auth/jwt.py +352 -0
  29. rem/auth/middleware.py +115 -10
  30. rem/auth/providers/__init__.py +4 -1
  31. rem/auth/providers/email.py +215 -0
  32. rem/cli/commands/README.md +42 -0
  33. rem/cli/commands/cluster.py +617 -168
  34. rem/cli/commands/configure.py +4 -7
  35. rem/cli/commands/db.py +66 -22
  36. rem/cli/commands/experiments.py +468 -76
  37. rem/cli/commands/schema.py +6 -5
  38. rem/cli/commands/session.py +336 -0
  39. rem/cli/dreaming.py +2 -2
  40. rem/cli/main.py +2 -0
  41. rem/config.py +8 -1
  42. rem/models/core/experiment.py +58 -14
  43. rem/models/entities/__init__.py +4 -0
  44. rem/models/entities/ontology.py +1 -1
  45. rem/models/entities/ontology_config.py +1 -1
  46. rem/models/entities/subscriber.py +175 -0
  47. rem/models/entities/user.py +1 -0
  48. rem/schemas/agents/core/agent-builder.yaml +235 -0
  49. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  50. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  51. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  52. rem/services/__init__.py +3 -1
  53. rem/services/content/service.py +4 -3
  54. rem/services/email/__init__.py +10 -0
  55. rem/services/email/service.py +513 -0
  56. rem/services/email/templates.py +360 -0
  57. rem/services/phoenix/client.py +59 -18
  58. rem/services/postgres/README.md +38 -0
  59. rem/services/postgres/diff_service.py +127 -6
  60. rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
  61. rem/services/postgres/repository.py +5 -4
  62. rem/services/postgres/schema_generator.py +205 -4
  63. rem/services/session/compression.py +120 -50
  64. rem/services/session/reload.py +14 -7
  65. rem/services/user_service.py +41 -9
  66. rem/settings.py +442 -23
  67. rem/sql/migrations/001_install.sql +156 -0
  68. rem/sql/migrations/002_install_models.sql +1951 -88
  69. rem/sql/migrations/004_cache_system.sql +548 -0
  70. rem/sql/migrations/005_schema_update.sql +145 -0
  71. rem/utils/README.md +45 -0
  72. rem/utils/__init__.py +18 -0
  73. rem/utils/files.py +157 -1
  74. rem/utils/schema_loader.py +139 -10
  75. rem/utils/sql_paths.py +146 -0
  76. rem/utils/vision.py +1 -1
  77. rem/workers/__init__.py +3 -1
  78. rem/workers/db_listener.py +579 -0
  79. rem/workers/unlogged_maintainer.py +463 -0
  80. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/METADATA +218 -180
  81. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/RECORD +83 -68
  82. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
  83. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,548 @@
1
+ -- REM Cache System
2
+ -- Description: Self-healing cache for UNLOGGED tables (kv_store)
3
+ -- Version: 1.0.0
4
+ -- Date: 2025-11-29
5
+ --
6
+ -- This migration adds:
7
+ -- 1. cache_system_state table for debouncing and API secret storage
8
+ -- 2. maybe_trigger_kv_rebuild() function for async rebuild triggering
9
+ -- 3. Updated rem_lookup/fuzzy/traverse with self-healing on empty cache
10
+ --
11
+ -- Self-Healing Flow:
12
+ -- Query returns 0 results → Check if kv_store empty → Trigger async rebuild
13
+ -- Priority: pg_net (if available) → dblink (always available)
14
+
15
+ -- ============================================================================
16
+ -- REQUIRED EXTENSION
17
+ -- ============================================================================
18
+ -- pgcrypto is needed for gen_random_bytes() to generate API secrets
19
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
20
+
21
+ -- ============================================================================
22
+ -- CACHE SYSTEM STATE TABLE
23
+ -- ============================================================================
24
+ -- Stores:
25
+ -- - Last rebuild trigger timestamp (for debouncing)
26
+ -- - API secret for internal endpoint authentication
27
+ -- - Rebuild statistics
28
+
29
+ CREATE TABLE IF NOT EXISTS cache_system_state (
30
+ id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), -- Single row table
31
+ api_secret TEXT NOT NULL, -- Secret for internal API auth
32
+ last_triggered_at TIMESTAMPTZ, -- Debounce: last trigger time
33
+ last_rebuild_at TIMESTAMPTZ, -- Last successful rebuild
34
+ triggered_by TEXT, -- What triggered last rebuild
35
+ trigger_count INTEGER DEFAULT 0, -- Total trigger count
36
+ rebuild_count INTEGER DEFAULT 0, -- Total successful rebuilds
37
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
38
+ updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
39
+ );
40
+
41
+ -- Generate initial secret if table is empty
42
+ INSERT INTO cache_system_state (id, api_secret)
43
+ SELECT 1, encode(gen_random_bytes(32), 'hex')
44
+ WHERE NOT EXISTS (SELECT 1 FROM cache_system_state WHERE id = 1);
45
+
46
+ COMMENT ON TABLE cache_system_state IS
47
+ 'Single-row table storing cache system state: API secret for internal auth and debounce tracking';
48
+
49
+ -- ============================================================================
50
+ -- HELPER: Check if extension exists
51
+ -- ============================================================================
52
+
53
+ CREATE OR REPLACE FUNCTION rem_extension_exists(p_extension TEXT)
54
+ RETURNS BOOLEAN AS $$
55
+ BEGIN
56
+ RETURN EXISTS (SELECT 1 FROM pg_extension WHERE extname = p_extension);
57
+ END;
58
+ $$ LANGUAGE plpgsql STABLE;
59
+
60
+ -- ============================================================================
61
+ -- HELPER: Check if kv_store is empty for user
62
+ -- ============================================================================
63
+
64
+ CREATE OR REPLACE FUNCTION rem_kv_store_empty(p_user_id TEXT)
65
+ RETURNS BOOLEAN AS $$
66
+ BEGIN
67
+ -- Quick existence check - very fast with index
68
+ RETURN NOT EXISTS (
69
+ SELECT 1 FROM kv_store
70
+ WHERE user_id = p_user_id
71
+ LIMIT 1
72
+ );
73
+ END;
74
+ $$ LANGUAGE plpgsql STABLE;
75
+
76
+ -- ============================================================================
77
+ -- MAIN: Maybe trigger KV rebuild (async, non-blocking)
78
+ -- ============================================================================
79
+ -- Called when a query returns 0 results and kv_store appears empty.
80
+ -- Uses pg_net (if available) to call API, falls back to dblink.
81
+ -- Includes debouncing to prevent request storms.
82
+
83
+ CREATE OR REPLACE FUNCTION maybe_trigger_kv_rebuild(
84
+ p_user_id TEXT,
85
+ p_triggered_by TEXT DEFAULT 'query'
86
+ )
87
+ RETURNS VOID AS $$
88
+ DECLARE
89
+ v_has_pgnet BOOLEAN;
90
+ v_has_dblink BOOLEAN;
91
+ v_last_trigger TIMESTAMPTZ;
92
+ v_api_secret TEXT;
93
+ v_debounce_seconds CONSTANT INTEGER := 30;
94
+ v_api_url TEXT := 'http://rem-api.rem.svc.cluster.local:8000/api/admin/internal/rebuild-kv';
95
+ v_request_id BIGINT;
96
+ BEGIN
97
+ -- Quick check: is kv_store actually empty for this user?
98
+ IF NOT rem_kv_store_empty(p_user_id) THEN
99
+ RETURN; -- Cache has data, nothing to do
100
+ END IF;
101
+
102
+ -- Try to acquire advisory lock (non-blocking, transaction-scoped)
103
+ -- This prevents multiple concurrent triggers
104
+ IF NOT pg_try_advisory_xact_lock(2147483646) THEN
105
+ RETURN; -- Another session is handling it
106
+ END IF;
107
+
108
+ -- Check debounce: was rebuild triggered recently?
109
+ SELECT last_triggered_at, api_secret
110
+ INTO v_last_trigger, v_api_secret
111
+ FROM cache_system_state
112
+ WHERE id = 1;
113
+
114
+ IF v_last_trigger IS NOT NULL
115
+ AND v_last_trigger > (CURRENT_TIMESTAMP - (v_debounce_seconds || ' seconds')::INTERVAL) THEN
116
+ RETURN; -- Triggered recently, skip
117
+ END IF;
118
+
119
+ -- Update state (so concurrent callers see it)
120
+ UPDATE cache_system_state
121
+ SET last_triggered_at = CURRENT_TIMESTAMP,
122
+ triggered_by = p_triggered_by,
123
+ trigger_count = trigger_count + 1,
124
+ updated_at = CURRENT_TIMESTAMP
125
+ WHERE id = 1;
126
+
127
+ -- Check available extensions
128
+ v_has_pgnet := rem_extension_exists('pg_net');
129
+ v_has_dblink := rem_extension_exists('dblink');
130
+
131
+ -- Priority 1: pg_net (async HTTP to API - supports S3 restore)
132
+ IF v_has_pgnet THEN
133
+ BEGIN
134
+ SELECT net.http_post(
135
+ url := v_api_url,
136
+ headers := jsonb_build_object(
137
+ 'Content-Type', 'application/json',
138
+ 'X-Internal-Secret', v_api_secret
139
+ ),
140
+ body := jsonb_build_object(
141
+ 'user_id', p_user_id,
142
+ 'triggered_by', 'pg_net_' || p_triggered_by,
143
+ 'timestamp', CURRENT_TIMESTAMP
144
+ )
145
+ ) INTO v_request_id;
146
+
147
+ RAISE DEBUG 'kv_rebuild triggered via pg_net (request_id: %)', v_request_id;
148
+ RETURN;
149
+ EXCEPTION WHEN OTHERS THEN
150
+ RAISE WARNING 'pg_net trigger failed: %, falling back to dblink', SQLERRM;
151
+ END;
152
+ END IF;
153
+
154
+ -- Priority 2: dblink (async SQL - direct rebuild)
155
+ IF v_has_dblink THEN
156
+ BEGIN
157
+ -- Connect to self (same database)
158
+ PERFORM dblink_connect(
159
+ 'kv_rebuild_conn',
160
+ format('dbname=%s', current_database())
161
+ );
162
+
163
+ -- Send async query (returns immediately)
164
+ PERFORM dblink_send_query(
165
+ 'kv_rebuild_conn',
166
+ 'SELECT rebuild_kv_store()'
167
+ );
168
+
169
+ -- Don't disconnect - query continues in background
170
+ -- Connection auto-closes when session ends
171
+
172
+ RAISE DEBUG 'kv_rebuild triggered via dblink';
173
+ RETURN;
174
+ EXCEPTION WHEN OTHERS THEN
175
+ -- Clean up failed connection
176
+ BEGIN
177
+ PERFORM dblink_disconnect('kv_rebuild_conn');
178
+ EXCEPTION WHEN OTHERS THEN
179
+ NULL;
180
+ END;
181
+ RAISE WARNING 'dblink trigger failed: %', SQLERRM;
182
+ END;
183
+ END IF;
184
+
185
+ -- No async method available - log warning but don't block query
186
+ RAISE WARNING 'No async rebuild method available (pg_net or dblink). Cache rebuild skipped.';
187
+
188
+ EXCEPTION WHEN OTHERS THEN
189
+ -- Never fail the calling query
190
+ RAISE WARNING 'maybe_trigger_kv_rebuild failed: %', SQLERRM;
191
+ END;
192
+ $$ LANGUAGE plpgsql;
193
+
194
+ COMMENT ON FUNCTION maybe_trigger_kv_rebuild IS
195
+ 'Async trigger for kv_store rebuild. Uses pg_net (API) or dblink (SQL). Includes debouncing.';
196
+
197
+ -- ============================================================================
198
+ -- UPDATED: rem_lookup with self-healing
199
+ -- ============================================================================
200
+
201
+ CREATE OR REPLACE FUNCTION rem_lookup(
202
+ p_entity_key VARCHAR(255),
203
+ p_tenant_id VARCHAR(100),
204
+ p_user_id VARCHAR(100)
205
+ )
206
+ RETURNS TABLE(
207
+ entity_type VARCHAR(100),
208
+ data JSONB
209
+ ) AS $$
210
+ DECLARE
211
+ entity_table VARCHAR(100);
212
+ query_sql TEXT;
213
+ effective_user_id VARCHAR(100);
214
+ v_result_count INTEGER := 0;
215
+ BEGIN
216
+ effective_user_id := COALESCE(p_user_id, p_tenant_id);
217
+
218
+ -- First lookup in KV store to get entity_type (table name)
219
+ SELECT kv.entity_type INTO entity_table
220
+ FROM kv_store kv
221
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
222
+ AND kv.entity_key = p_entity_key
223
+ LIMIT 1;
224
+
225
+ -- If not found, check if cache is empty and maybe trigger rebuild
226
+ IF entity_table IS NULL THEN
227
+ -- SELF-HEALING: Check if this is because cache is empty
228
+ IF rem_kv_store_empty(effective_user_id) THEN
229
+ PERFORM maybe_trigger_kv_rebuild(effective_user_id, 'rem_lookup');
230
+ END IF;
231
+ RETURN;
232
+ END IF;
233
+
234
+ -- Fetch raw record from underlying table as JSONB
235
+ query_sql := format('
236
+ SELECT
237
+ %L::VARCHAR(100) AS entity_type,
238
+ row_to_json(t)::jsonb AS data
239
+ FROM %I t
240
+ WHERE (t.user_id = $1 OR t.user_id IS NULL)
241
+ AND t.name = $2
242
+ AND t.deleted_at IS NULL
243
+ ', entity_table, entity_table);
244
+
245
+ RETURN QUERY EXECUTE query_sql USING effective_user_id, p_entity_key;
246
+ END;
247
+ $$ LANGUAGE plpgsql STABLE;
248
+
249
+ -- ============================================================================
250
+ -- UPDATED: rem_fuzzy with self-healing
251
+ -- ============================================================================
252
+
253
+ CREATE OR REPLACE FUNCTION rem_fuzzy(
254
+ p_query TEXT,
255
+ p_tenant_id VARCHAR(100),
256
+ p_threshold REAL DEFAULT 0.3,
257
+ p_limit INTEGER DEFAULT 10,
258
+ p_user_id VARCHAR(100) DEFAULT NULL
259
+ )
260
+ RETURNS TABLE(
261
+ entity_type VARCHAR(100),
262
+ similarity_score REAL,
263
+ data JSONB
264
+ ) AS $$
265
+ DECLARE
266
+ kv_matches RECORD;
267
+ entities_by_table JSONB := '{}'::jsonb;
268
+ table_keys JSONB;
269
+ effective_user_id VARCHAR(100);
270
+ v_found_any BOOLEAN := FALSE;
271
+ BEGIN
272
+ effective_user_id := COALESCE(p_user_id, p_tenant_id);
273
+
274
+ -- Find matching keys in KV store
275
+ FOR kv_matches IN
276
+ SELECT
277
+ kv.entity_key,
278
+ kv.entity_type,
279
+ similarity(kv.entity_key, p_query) AS sim_score
280
+ FROM kv_store kv
281
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
282
+ AND kv.entity_key % p_query
283
+ AND similarity(kv.entity_key, p_query) >= p_threshold
284
+ ORDER BY sim_score DESC
285
+ LIMIT p_limit
286
+ LOOP
287
+ v_found_any := TRUE;
288
+ -- Build JSONB mapping {table: [keys]}
289
+ IF entities_by_table ? kv_matches.entity_type THEN
290
+ table_keys := entities_by_table->kv_matches.entity_type;
291
+ entities_by_table := jsonb_set(
292
+ entities_by_table,
293
+ ARRAY[kv_matches.entity_type],
294
+ table_keys || jsonb_build_array(kv_matches.entity_key)
295
+ );
296
+ ELSE
297
+ entities_by_table := jsonb_set(
298
+ entities_by_table,
299
+ ARRAY[kv_matches.entity_type],
300
+ jsonb_build_array(kv_matches.entity_key)
301
+ );
302
+ END IF;
303
+ END LOOP;
304
+
305
+ -- SELF-HEALING: If no matches and cache is empty, trigger rebuild
306
+ IF NOT v_found_any AND rem_kv_store_empty(effective_user_id) THEN
307
+ PERFORM maybe_trigger_kv_rebuild(effective_user_id, 'rem_fuzzy');
308
+ END IF;
309
+
310
+ -- Fetch full records
311
+ RETURN QUERY
312
+ SELECT
313
+ f.entity_type::VARCHAR(100),
314
+ similarity(f.entity_key, p_query) AS similarity_score,
315
+ f.entity_record AS data
316
+ FROM rem_fetch(entities_by_table, effective_user_id) f
317
+ ORDER BY similarity_score DESC;
318
+ END;
319
+ $$ LANGUAGE plpgsql STABLE;
320
+
321
+ -- ============================================================================
322
+ -- UPDATED: rem_traverse with self-healing
323
+ -- ============================================================================
324
+
325
+ CREATE OR REPLACE FUNCTION rem_traverse(
326
+ p_entity_key VARCHAR(255),
327
+ p_tenant_id VARCHAR(100),
328
+ p_user_id VARCHAR(100),
329
+ p_max_depth INTEGER DEFAULT 1,
330
+ p_rel_type VARCHAR(100) DEFAULT NULL,
331
+ p_keys_only BOOLEAN DEFAULT FALSE
332
+ )
333
+ RETURNS TABLE(
334
+ depth INTEGER,
335
+ entity_key VARCHAR(255),
336
+ entity_type VARCHAR(100),
337
+ entity_id UUID,
338
+ rel_type VARCHAR(100),
339
+ rel_weight REAL,
340
+ path TEXT[],
341
+ entity_record JSONB
342
+ ) AS $$
343
+ DECLARE
344
+ graph_keys RECORD;
345
+ entities_by_table JSONB := '{}'::jsonb;
346
+ table_keys JSONB;
347
+ effective_user_id VARCHAR(100);
348
+ v_found_start BOOLEAN := FALSE;
349
+ BEGIN
350
+ effective_user_id := COALESCE(p_user_id, p_tenant_id);
351
+
352
+ -- Check if start entity exists in kv_store
353
+ SELECT TRUE INTO v_found_start
354
+ FROM kv_store kv
355
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
356
+ AND kv.entity_key = p_entity_key
357
+ LIMIT 1;
358
+
359
+ -- SELF-HEALING: If start not found and cache is empty, trigger rebuild
360
+ IF NOT COALESCE(v_found_start, FALSE) THEN
361
+ IF rem_kv_store_empty(effective_user_id) THEN
362
+ PERFORM maybe_trigger_kv_rebuild(effective_user_id, 'rem_traverse');
363
+ END IF;
364
+ RETURN;
365
+ END IF;
366
+
367
+ -- Original traverse logic
368
+ FOR graph_keys IN
369
+ WITH RECURSIVE graph_traversal AS (
370
+ SELECT
371
+ 0 AS depth,
372
+ kv.entity_key,
373
+ kv.entity_type,
374
+ kv.entity_id,
375
+ NULL::VARCHAR(100) AS rel_type,
376
+ NULL::REAL AS rel_weight,
377
+ ARRAY[kv.entity_key]::TEXT[] AS path
378
+ FROM kv_store kv
379
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
380
+ AND kv.entity_key = p_entity_key
381
+
382
+ UNION ALL
383
+
384
+ SELECT
385
+ gt.depth + 1,
386
+ target_kv.entity_key,
387
+ target_kv.entity_type,
388
+ target_kv.entity_id,
389
+ (edge->>'rel_type')::VARCHAR(100) AS rel_type,
390
+ COALESCE((edge->>'weight')::REAL, 1.0) AS rel_weight,
391
+ gt.path || target_kv.entity_key AS path
392
+ FROM graph_traversal gt
393
+ JOIN kv_store source_kv ON source_kv.entity_key = gt.entity_key
394
+ AND (source_kv.user_id = effective_user_id OR source_kv.user_id IS NULL)
395
+ CROSS JOIN LATERAL jsonb_array_elements(COALESCE(source_kv.graph_edges, '[]'::jsonb)) AS edge
396
+ JOIN kv_store target_kv ON target_kv.entity_key = (edge->>'dst')::VARCHAR(255)
397
+ AND (target_kv.user_id = effective_user_id OR target_kv.user_id IS NULL)
398
+ WHERE gt.depth < p_max_depth
399
+ AND (p_rel_type IS NULL OR (edge->>'rel_type')::VARCHAR(100) = p_rel_type)
400
+ AND NOT (target_kv.entity_key = ANY(gt.path))
401
+ )
402
+ SELECT DISTINCT ON (gt.entity_key)
403
+ gt.depth,
404
+ gt.entity_key,
405
+ gt.entity_type,
406
+ gt.entity_id,
407
+ gt.rel_type,
408
+ gt.rel_weight,
409
+ gt.path
410
+ FROM graph_traversal gt
411
+ WHERE gt.depth > 0
412
+ ORDER BY gt.entity_key, gt.depth
413
+ LOOP
414
+ IF p_keys_only THEN
415
+ depth := graph_keys.depth;
416
+ entity_key := graph_keys.entity_key;
417
+ entity_type := graph_keys.entity_type;
418
+ entity_id := graph_keys.entity_id;
419
+ rel_type := graph_keys.rel_type;
420
+ rel_weight := graph_keys.rel_weight;
421
+ path := graph_keys.path;
422
+ entity_record := NULL;
423
+ RETURN NEXT;
424
+ ELSE
425
+ IF entities_by_table ? graph_keys.entity_type THEN
426
+ table_keys := entities_by_table->graph_keys.entity_type;
427
+ entities_by_table := jsonb_set(
428
+ entities_by_table,
429
+ ARRAY[graph_keys.entity_type],
430
+ table_keys || jsonb_build_array(graph_keys.entity_key)
431
+ );
432
+ ELSE
433
+ entities_by_table := jsonb_set(
434
+ entities_by_table,
435
+ ARRAY[graph_keys.entity_type],
436
+ jsonb_build_array(graph_keys.entity_key)
437
+ );
438
+ END IF;
439
+ END IF;
440
+ END LOOP;
441
+
442
+ IF NOT p_keys_only THEN
443
+ RETURN QUERY
444
+ SELECT
445
+ g.depth,
446
+ g.entity_key,
447
+ g.entity_type,
448
+ g.entity_id,
449
+ g.rel_type,
450
+ g.rel_weight,
451
+ g.path,
452
+ f.entity_record
453
+ FROM (
454
+ SELECT * FROM rem_traverse(p_entity_key, p_tenant_id, effective_user_id, p_max_depth, p_rel_type, TRUE)
455
+ ) g
456
+ LEFT JOIN rem_fetch(entities_by_table, effective_user_id) f
457
+ ON g.entity_key = f.entity_key;
458
+ END IF;
459
+ END;
460
+ $$ LANGUAGE plpgsql STABLE;
461
+
462
+ -- ============================================================================
463
+ -- HELPER: Get API secret for validation
464
+ -- ============================================================================
465
+
466
+ CREATE OR REPLACE FUNCTION rem_get_cache_api_secret()
467
+ RETURNS TEXT AS $$
468
+ DECLARE
469
+ v_secret TEXT;
470
+ BEGIN
471
+ SELECT api_secret INTO v_secret FROM cache_system_state WHERE id = 1;
472
+ RETURN v_secret;
473
+ END;
474
+ $$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
475
+
476
+ -- Only allow rem user to execute
477
+ REVOKE ALL ON FUNCTION rem_get_cache_api_secret() FROM PUBLIC;
478
+
479
+ -- ============================================================================
480
+ -- HELPER: Record successful rebuild
481
+ -- ============================================================================
482
+
483
+ CREATE OR REPLACE FUNCTION rem_record_cache_rebuild(p_triggered_by TEXT DEFAULT 'api')
484
+ RETURNS VOID AS $$
485
+ BEGIN
486
+ UPDATE cache_system_state
487
+ SET last_rebuild_at = CURRENT_TIMESTAMP,
488
+ rebuild_count = rebuild_count + 1,
489
+ updated_at = CURRENT_TIMESTAMP
490
+ WHERE id = 1;
491
+ END;
492
+ $$ LANGUAGE plpgsql;
493
+
494
+ -- ============================================================================
495
+ -- RECORD INSTALLATION
496
+ -- ============================================================================
497
+
498
+ DO $$
499
+ BEGIN
500
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'rem_migrations') THEN
501
+ INSERT INTO rem_migrations (name, type, version)
502
+ VALUES ('004_cache_system.sql', 'install', '1.0.0')
503
+ ON CONFLICT (name) DO UPDATE
504
+ SET applied_at = CURRENT_TIMESTAMP,
505
+ applied_by = CURRENT_USER;
506
+ END IF;
507
+ END $$;
508
+
509
+ -- ============================================================================
510
+ -- COMPLETION
511
+ -- ============================================================================
512
+
513
+ DO $$
514
+ DECLARE
515
+ v_has_pgnet BOOLEAN;
516
+ v_has_dblink BOOLEAN;
517
+ BEGIN
518
+ v_has_pgnet := rem_extension_exists('pg_net');
519
+ v_has_dblink := rem_extension_exists('dblink');
520
+
521
+ RAISE NOTICE '============================================================';
522
+ RAISE NOTICE 'Cache System Installation Complete';
523
+ RAISE NOTICE '============================================================';
524
+ RAISE NOTICE '';
525
+ RAISE NOTICE 'Tables:';
526
+ RAISE NOTICE ' cache_system_state - Debounce tracking and API secret';
527
+ RAISE NOTICE '';
528
+ RAISE NOTICE 'Functions:';
529
+ RAISE NOTICE ' maybe_trigger_kv_rebuild() - Async rebuild trigger';
530
+ RAISE NOTICE ' rem_lookup() - Updated with self-healing';
531
+ RAISE NOTICE ' rem_fuzzy() - Updated with self-healing';
532
+ RAISE NOTICE ' rem_traverse() - Updated with self-healing';
533
+ RAISE NOTICE '';
534
+ RAISE NOTICE 'Async Methods Available:';
535
+ IF v_has_pgnet THEN
536
+ RAISE NOTICE ' [x] pg_net - HTTP POST to API (preferred)';
537
+ ELSE
538
+ RAISE NOTICE ' [ ] pg_net - Not installed';
539
+ END IF;
540
+ IF v_has_dblink THEN
541
+ RAISE NOTICE ' [x] dblink - Async SQL (fallback)';
542
+ ELSE
543
+ RAISE NOTICE ' [ ] dblink - Not installed';
544
+ END IF;
545
+ RAISE NOTICE '';
546
+ RAISE NOTICE 'Self-Healing: Queries will auto-trigger rebuild on empty cache';
547
+ RAISE NOTICE '============================================================';
548
+ END $$;
@@ -0,0 +1,145 @@
1
+ -- Migration: schema_update
2
+ -- Generated by: rem db diff --generate
3
+ -- Changes detected: 139
4
+ --
5
+ -- Review this file before applying!
6
+ -- Apply with: rem db migrate
7
+ --
8
+
9
+ CREATE TABLE IF NOT EXISTS patient_profiles (
10
+ id UUID NOT NULL,
11
+ tenant_id VARCHAR(100) NOT NULL,
12
+ user_id VARCHAR(256),
13
+ patient_ref VARCHAR(256) NOT NULL,
14
+ clinician_ref VARCHAR(256),
15
+ evaluation_type TEXT,
16
+ evaluation_date DATE,
17
+ session_ref VARCHAR(256),
18
+ risk_level TEXT,
19
+ suicidality TEXT,
20
+ suicidality_details VARCHAR(256),
21
+ homicidal_ideation BOOLEAN,
22
+ homicidal_details VARCHAR(256),
23
+ self_harm_current BOOLEAN,
24
+ self_harm_history BOOLEAN,
25
+ suicide_attempts_lifetime INTEGER,
26
+ suicide_attempt_most_recent VARCHAR(256),
27
+ safety_plan_in_place BOOLEAN,
28
+ safety_plan_date DATE,
29
+ appearance VARCHAR(256),
30
+ behavior VARCHAR(256),
31
+ speech VARCHAR(256),
32
+ mood_states JSONB,
33
+ mood_description TEXT,
34
+ affect_quality TEXT,
35
+ affect_congruent BOOLEAN,
36
+ thought_process TEXT,
37
+ delusions_present BOOLEAN,
38
+ delusion_types TEXT[],
39
+ hallucinations_present BOOLEAN,
40
+ hallucination_types TEXT[],
41
+ command_hallucinations BOOLEAN,
42
+ oriented BOOLEAN,
43
+ orientation_deficits TEXT[],
44
+ memory_intact BOOLEAN,
45
+ attention_intact BOOLEAN,
46
+ insight TEXT,
47
+ judgment TEXT,
48
+ symptoms JSONB,
49
+ chief_complaint VARCHAR(256),
50
+ symptom_duration VARCHAR(256),
51
+ symptom_trajectory VARCHAR(256),
52
+ precipitating_factors TEXT[],
53
+ substance_use_profiles JSONB,
54
+ substance_use_summary TEXT,
55
+ in_recovery BOOLEAN,
56
+ recovery_duration VARCHAR(256),
57
+ mat_current BOOLEAN,
58
+ diagnoses_current TEXT[],
59
+ diagnoses_historical TEXT[],
60
+ first_psychiatric_contact_age INTEGER,
61
+ hospitalizations_psychiatric INTEGER,
62
+ last_hospitalization VARCHAR(256),
63
+ ect_history BOOLEAN,
64
+ therapy_history VARCHAR(256),
65
+ family_psychiatric_history VARCHAR(256),
66
+ current_medications JSONB,
67
+ other_medications TEXT[],
68
+ allergies TEXT[],
69
+ current_therapist BOOLEAN,
70
+ therapy_type VARCHAR(256),
71
+ therapy_frequency VARCHAR(256),
72
+ other_providers TEXT[],
73
+ functioning JSONB,
74
+ social_determinants JSONB,
75
+ cgi_severity TEXT,
76
+ cgi_improvement TEXT,
77
+ gaf_equivalent INTEGER,
78
+ clinical_impression VARCHAR(256),
79
+ differential_diagnoses TEXT[],
80
+ treatment_plan VARCHAR(256),
81
+ treatment_goals TEXT[],
82
+ barriers_to_treatment TEXT[],
83
+ strengths TEXT[],
84
+ follow_up_recommended BOOLEAN,
85
+ follow_up_urgency VARCHAR(256),
86
+ follow_up_interval VARCHAR(256),
87
+ referrals TEXT[],
88
+ labs_ordered TEXT[],
89
+ data_source VARCHAR(256),
90
+ confidence_score FLOAT,
91
+ reviewed_by_clinician BOOLEAN,
92
+ notes VARCHAR(256),
93
+ created_at TIMESTAMP WITHOUT TIME ZONE,
94
+ updated_at TIMESTAMP WITHOUT TIME ZONE,
95
+ deleted_at TIMESTAMP WITHOUT TIME ZONE,
96
+ graph_edges JSONB,
97
+ metadata JSONB,
98
+ tags TEXT[]
99
+ );
100
+ -- Changes to table: patient_profiles
101
+ CREATE INDEX IF NOT EXISTS idx_patient_profiles_graph_edges ON patient_profiles (graph_edges);
102
+ CREATE INDEX IF NOT EXISTS idx_patient_profiles_metadata ON patient_profiles (metadata);
103
+ CREATE INDEX IF NOT EXISTS idx_patient_profiles_tags ON patient_profiles (tags);
104
+ CREATE INDEX IF NOT EXISTS idx_patient_profiles_tenant ON patient_profiles (tenant_id);
105
+ CREATE INDEX IF NOT EXISTS idx_patient_profiles_user ON patient_profiles (user_id);
106
+ CREATE TABLE IF NOT EXISTS subscribers (
107
+ id UUID NOT NULL,
108
+ tenant_id VARCHAR(100) NOT NULL,
109
+ user_id VARCHAR(256),
110
+ email TEXT NOT NULL,
111
+ name VARCHAR(256),
112
+ comment TEXT,
113
+ status TEXT,
114
+ origin TEXT,
115
+ origin_detail VARCHAR(256),
116
+ subscribed_at TIMESTAMP WITHOUT TIME ZONE,
117
+ unsubscribed_at TIMESTAMP WITHOUT TIME ZONE,
118
+ ip_address VARCHAR(256),
119
+ user_agent VARCHAR(256),
120
+ created_at TIMESTAMP WITHOUT TIME ZONE,
121
+ updated_at TIMESTAMP WITHOUT TIME ZONE,
122
+ deleted_at TIMESTAMP WITHOUT TIME ZONE,
123
+ graph_edges JSONB,
124
+ metadata JSONB,
125
+ tags TEXT[]
126
+ );
127
+ -- Changes to table: subscribers
128
+ CREATE INDEX IF NOT EXISTS idx_subscribers_graph_edges ON subscribers (graph_edges);
129
+ CREATE INDEX IF NOT EXISTS idx_subscribers_metadata ON subscribers (metadata);
130
+ CREATE INDEX IF NOT EXISTS idx_subscribers_tags ON subscribers (tags);
131
+ CREATE INDEX IF NOT EXISTS idx_subscribers_tenant ON subscribers (tenant_id);
132
+ CREATE INDEX IF NOT EXISTS idx_subscribers_user ON subscribers (user_id);
133
+ CREATE TABLE IF NOT EXISTS embeddings_patient_profiles (
134
+ id UUID NOT NULL,
135
+ entity_id UUID NOT NULL,
136
+ field_name VARCHAR(100) NOT NULL,
137
+ provider VARCHAR(50) NOT NULL,
138
+ model VARCHAR(100) NOT NULL,
139
+ embedding FLOAT[] NOT NULL,
140
+ created_at TIMESTAMP WITHOUT TIME ZONE,
141
+ updated_at TIMESTAMP WITHOUT TIME ZONE
142
+ );
143
+ -- Changes to table: embeddings_patient_profiles
144
+ CREATE INDEX IF NOT EXISTS idx_embeddings_patient_profiles_entity ON embeddings_patient_profiles (entity_id);
145
+ CREATE INDEX IF NOT EXISTS idx_embeddings_patient_profiles_field_provider ON embeddings_patient_profiles (field_name, provider);