remdb 0.3.172__py3-none-any.whl → 0.3.223__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 (57) hide show
  1. rem/agentic/README.md +262 -2
  2. rem/agentic/context.py +173 -0
  3. rem/agentic/context_builder.py +12 -2
  4. rem/agentic/mcp/tool_wrapper.py +39 -16
  5. rem/agentic/providers/pydantic_ai.py +46 -43
  6. rem/agentic/schema.py +2 -2
  7. rem/agentic/tools/rem_tools.py +11 -0
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/resources.py +64 -8
  10. rem/api/mcp_router/server.py +31 -24
  11. rem/api/mcp_router/tools.py +621 -166
  12. rem/api/routers/admin.py +30 -4
  13. rem/api/routers/auth.py +114 -15
  14. rem/api/routers/chat/completions.py +66 -18
  15. rem/api/routers/chat/sse_events.py +7 -3
  16. rem/api/routers/chat/streaming.py +254 -22
  17. rem/api/routers/common.py +18 -0
  18. rem/api/routers/dev.py +7 -1
  19. rem/api/routers/feedback.py +9 -1
  20. rem/api/routers/messages.py +176 -38
  21. rem/api/routers/models.py +9 -1
  22. rem/api/routers/query.py +12 -1
  23. rem/api/routers/shared_sessions.py +16 -0
  24. rem/auth/jwt.py +19 -4
  25. rem/auth/middleware.py +42 -28
  26. rem/cli/README.md +62 -0
  27. rem/cli/commands/ask.py +1 -1
  28. rem/cli/commands/db.py +148 -70
  29. rem/cli/commands/process.py +171 -43
  30. rem/models/entities/ontology.py +91 -101
  31. rem/schemas/agents/rem.yaml +1 -1
  32. rem/services/content/service.py +18 -5
  33. rem/services/email/service.py +11 -2
  34. rem/services/embeddings/worker.py +26 -12
  35. rem/services/postgres/__init__.py +28 -3
  36. rem/services/postgres/diff_service.py +57 -5
  37. rem/services/postgres/programmable_diff_service.py +635 -0
  38. rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
  39. rem/services/postgres/register_type.py +12 -11
  40. rem/services/postgres/repository.py +46 -25
  41. rem/services/postgres/schema_generator.py +5 -5
  42. rem/services/postgres/sql_builder.py +6 -5
  43. rem/services/session/__init__.py +8 -1
  44. rem/services/session/compression.py +40 -2
  45. rem/services/session/pydantic_messages.py +276 -0
  46. rem/settings.py +28 -0
  47. rem/sql/background_indexes.sql +5 -0
  48. rem/sql/migrations/001_install.sql +157 -10
  49. rem/sql/migrations/002_install_models.sql +160 -132
  50. rem/sql/migrations/004_cache_system.sql +7 -275
  51. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  52. rem/utils/model_helpers.py +101 -0
  53. rem/utils/schema_loader.py +6 -6
  54. {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/METADATA +1 -1
  55. {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/RECORD +57 -53
  56. {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/WHEEL +0 -0
  57. {remdb-0.3.172.dist-info → remdb-0.3.223.dist-info}/entry_points.txt +0 -0
@@ -44,6 +44,33 @@ BEGIN
44
44
  RAISE NOTICE '✓ All required extensions installed successfully';
45
45
  END $$;
46
46
 
47
+ -- ============================================================================
48
+ -- NORMALIZATION HELPER
49
+ -- ============================================================================
50
+
51
+ -- Normalize entity keys to lower-kebab-case for consistent lookups
52
+ -- "Mood Disorder" -> "mood-disorder"
53
+ -- "mood_disorder" -> "mood-disorder"
54
+ -- "MoodDisorder" -> "mood-disorder"
55
+ CREATE OR REPLACE FUNCTION normalize_key(input TEXT)
56
+ RETURNS TEXT AS $$
57
+ BEGIN
58
+ RETURN lower(
59
+ regexp_replace(
60
+ regexp_replace(
61
+ regexp_replace(input, '([a-z])([A-Z])', '\1-\2', 'g'), -- camelCase -> kebab
62
+ '[_\s]+', '-', 'g' -- underscores/spaces -> hyphens
63
+ ),
64
+ '-+', '-', 'g' -- collapse multiple hyphens
65
+ )
66
+ );
67
+ END;
68
+ $$ LANGUAGE plpgsql IMMUTABLE;
69
+
70
+ COMMENT ON FUNCTION normalize_key IS
71
+ 'Normalizes entity keys to lower-kebab-case for consistent lookups.
72
+ Examples: "Mood Disorder" -> "mood-disorder", "mood_disorder" -> "mood-disorder"';
73
+
47
74
  -- ============================================================================
48
75
  -- MIGRATION TRACKING
49
76
  -- ============================================================================
@@ -94,18 +121,18 @@ CREATE UNLOGGED TABLE IF NOT EXISTS kv_store (
94
121
  entity_key VARCHAR(255) NOT NULL,
95
122
  entity_type VARCHAR(100) NOT NULL,
96
123
  entity_id UUID NOT NULL,
97
- tenant_id VARCHAR(100) NOT NULL,
124
+ tenant_id VARCHAR(100), -- NULL = public/shared data
98
125
  user_id VARCHAR(100),
99
126
  content_summary TEXT,
100
127
  metadata JSONB DEFAULT '{}',
101
128
  graph_edges JSONB DEFAULT '[]'::jsonb, -- Cached edges for fast graph traversal
102
129
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
103
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
104
-
105
- -- Composite primary key: entity_key unique per tenant
106
- PRIMARY KEY (tenant_id, entity_key)
130
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
107
131
  );
108
132
 
133
+ -- Unique constraint on (tenant_id, entity_key) using COALESCE to handle NULL tenant_id
134
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_kv_store_tenant_key ON kv_store (COALESCE(tenant_id, ''), entity_key);
135
+
109
136
  -- Index for user-scoped lookups (when user_id IS NOT NULL)
110
137
  CREATE INDEX IF NOT EXISTS idx_kv_store_user ON kv_store (tenant_id, user_id)
111
138
  WHERE user_id IS NOT NULL;
@@ -146,7 +173,7 @@ COMMENT ON COLUMN kv_store.entity_id IS
146
173
  'UUID from primary table for reverse lookup';
147
174
 
148
175
  COMMENT ON COLUMN kv_store.tenant_id IS
149
- 'Tenant identifier for multi-tenancy isolation';
176
+ 'Tenant identifier for multi-tenancy isolation. NULL = public/shared data visible to all.';
150
177
 
151
178
  COMMENT ON COLUMN kv_store.user_id IS
152
179
  'Optional user scoping. NULL = system-level entity, visible to all users in tenant';
@@ -237,14 +264,19 @@ BEGIN
237
264
 
238
265
  -- First lookup in KV store to get entity_type (table name)
239
266
  -- Include user-owned AND public (NULL user_id) entries
267
+ -- Normalize input key for consistent matching
240
268
  SELECT kv.entity_type INTO entity_table
241
269
  FROM kv_store kv
242
270
  WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
243
- AND kv.entity_key = p_entity_key
271
+ AND kv.entity_key = normalize_key(p_entity_key)
244
272
  LIMIT 1;
245
273
 
246
- -- If not found, return empty
274
+ -- If not found, check if cache is empty and maybe trigger rebuild
247
275
  IF entity_table IS NULL THEN
276
+ -- SELF-HEALING: Check if this is because cache is empty
277
+ IF rem_kv_store_empty(effective_user_id) THEN
278
+ PERFORM maybe_trigger_kv_rebuild(effective_user_id, 'rem_lookup');
279
+ END IF;
248
280
  RETURN;
249
281
  END IF;
250
282
 
@@ -329,6 +361,7 @@ DECLARE
329
361
  entities_by_table JSONB := '{}'::jsonb;
330
362
  table_keys JSONB;
331
363
  effective_user_id VARCHAR(100);
364
+ v_found_any BOOLEAN := FALSE;
332
365
  BEGIN
333
366
  effective_user_id := COALESCE(p_user_id, p_tenant_id);
334
367
 
@@ -345,6 +378,7 @@ BEGIN
345
378
  ORDER BY sim_score DESC
346
379
  LIMIT p_limit
347
380
  LOOP
381
+ v_found_any := TRUE;
348
382
  -- Build JSONB mapping {table: [keys]}
349
383
  IF entities_by_table ? kv_matches.entity_type THEN
350
384
  table_keys := entities_by_table->kv_matches.entity_type;
@@ -362,6 +396,11 @@ BEGIN
362
396
  END IF;
363
397
  END LOOP;
364
398
 
399
+ -- SELF-HEALING: If no matches and cache is empty, trigger rebuild
400
+ IF NOT v_found_any AND rem_kv_store_empty(effective_user_id) THEN
401
+ PERFORM maybe_trigger_kv_rebuild(effective_user_id, 'rem_fuzzy');
402
+ END IF;
403
+
365
404
  -- Fetch full records using rem_fetch (which now supports NULL user_id)
366
405
  RETURN QUERY
367
406
  SELECT
@@ -408,12 +447,29 @@ DECLARE
408
447
  entities_by_table JSONB := '{}'::jsonb;
409
448
  table_keys JSONB;
410
449
  effective_user_id VARCHAR(100);
450
+ v_found_start BOOLEAN := FALSE;
411
451
  BEGIN
412
452
  effective_user_id := COALESCE(p_user_id, p_tenant_id);
413
453
 
454
+ -- Check if start entity exists in kv_store
455
+ SELECT TRUE INTO v_found_start
456
+ FROM kv_store kv
457
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
458
+ AND kv.entity_key = normalize_key(p_entity_key)
459
+ LIMIT 1;
460
+
461
+ -- SELF-HEALING: If start not found and cache is empty, trigger rebuild
462
+ IF NOT COALESCE(v_found_start, FALSE) THEN
463
+ IF rem_kv_store_empty(effective_user_id) THEN
464
+ PERFORM maybe_trigger_kv_rebuild(effective_user_id, 'rem_traverse');
465
+ END IF;
466
+ RETURN;
467
+ END IF;
468
+
414
469
  FOR graph_keys IN
415
470
  WITH RECURSIVE graph_traversal AS (
416
471
  -- Base case: Find starting entity (user-owned OR public)
472
+ -- Normalize input key for consistent matching
417
473
  SELECT
418
474
  0 AS depth,
419
475
  kv.entity_key,
@@ -424,7 +480,7 @@ BEGIN
424
480
  ARRAY[kv.entity_key]::TEXT[] AS path
425
481
  FROM kv_store kv
426
482
  WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
427
- AND kv.entity_key = p_entity_key
483
+ AND kv.entity_key = normalize_key(p_entity_key)
428
484
 
429
485
  UNION ALL
430
486
 
@@ -441,7 +497,7 @@ BEGIN
441
497
  JOIN kv_store source_kv ON source_kv.entity_key = gt.entity_key
442
498
  AND (source_kv.user_id = effective_user_id OR source_kv.user_id IS NULL)
443
499
  CROSS JOIN LATERAL jsonb_array_elements(COALESCE(source_kv.graph_edges, '[]'::jsonb)) AS edge
444
- JOIN kv_store target_kv ON target_kv.entity_key = (edge->>'dst')::VARCHAR(255)
500
+ JOIN kv_store target_kv ON target_kv.entity_key = normalize_key((edge->>'dst')::VARCHAR(255))
445
501
  AND (target_kv.user_id = effective_user_id OR target_kv.user_id IS NULL)
446
502
  WHERE gt.depth < p_max_depth
447
503
  AND (p_rel_type IS NULL OR (edge->>'rel_type')::VARCHAR(100) = p_rel_type)
@@ -760,6 +816,97 @@ $$ LANGUAGE plpgsql STABLE;
760
816
  COMMENT ON FUNCTION fn_get_shared_messages IS
761
817
  'Get messages from sessions shared by a specific user with the recipient.';
762
818
 
819
+ -- ============================================================================
820
+ -- SESSIONS WITH USER INFO
821
+ -- ============================================================================
822
+ -- Function to list sessions with user details (name, email) for admin views
823
+
824
+ -- List sessions with user info, CTE pagination
825
+ -- Note: messages.session_id stores the session UUID (sessions.id)
826
+ CREATE OR REPLACE FUNCTION fn_list_sessions_with_user(
827
+ p_user_id VARCHAR(256) DEFAULT NULL, -- Filter by user_id (NULL = all users, admin only)
828
+ p_user_name VARCHAR(256) DEFAULT NULL, -- Filter by user name (partial match, admin only)
829
+ p_user_email VARCHAR(256) DEFAULT NULL, -- Filter by user email (partial match, admin only)
830
+ p_mode VARCHAR(50) DEFAULT NULL, -- Filter by session mode
831
+ p_page INTEGER DEFAULT 1,
832
+ p_page_size INTEGER DEFAULT 50
833
+ )
834
+ RETURNS TABLE(
835
+ id UUID,
836
+ name VARCHAR(256),
837
+ mode TEXT,
838
+ description TEXT,
839
+ user_id VARCHAR(256),
840
+ user_name VARCHAR(256),
841
+ user_email VARCHAR(256),
842
+ message_count INTEGER,
843
+ total_tokens INTEGER,
844
+ created_at TIMESTAMP,
845
+ updated_at TIMESTAMP,
846
+ metadata JSONB,
847
+ total_count BIGINT
848
+ ) AS $$
849
+ BEGIN
850
+ RETURN QUERY
851
+ WITH session_msg_counts AS (
852
+ -- Count messages per session (joining on session UUID)
853
+ SELECT
854
+ m.session_id,
855
+ COUNT(*)::INTEGER as actual_message_count
856
+ FROM messages m
857
+ GROUP BY m.session_id
858
+ ),
859
+ filtered_sessions AS (
860
+ SELECT
861
+ s.id,
862
+ s.name,
863
+ s.mode,
864
+ s.description,
865
+ s.user_id,
866
+ COALESCE(u.name, s.user_id)::VARCHAR(256) AS user_name,
867
+ u.email::VARCHAR(256) AS user_email,
868
+ COALESCE(mc.actual_message_count, 0) AS message_count,
869
+ s.total_tokens,
870
+ s.created_at,
871
+ s.updated_at,
872
+ s.metadata
873
+ FROM sessions s
874
+ LEFT JOIN users u ON u.id::text = s.user_id
875
+ LEFT JOIN session_msg_counts mc ON mc.session_id = s.id::text
876
+ WHERE s.deleted_at IS NULL
877
+ AND (p_user_id IS NULL OR s.user_id = p_user_id)
878
+ AND (p_user_name IS NULL OR u.name ILIKE '%' || p_user_name || '%')
879
+ AND (p_user_email IS NULL OR u.email ILIKE '%' || p_user_email || '%')
880
+ AND (p_mode IS NULL OR s.mode = p_mode)
881
+ ),
882
+ counted AS (
883
+ SELECT *, COUNT(*) OVER () AS total_count
884
+ FROM filtered_sessions
885
+ )
886
+ SELECT
887
+ c.id,
888
+ c.name,
889
+ c.mode,
890
+ c.description,
891
+ c.user_id,
892
+ c.user_name,
893
+ c.user_email,
894
+ c.message_count,
895
+ c.total_tokens,
896
+ c.created_at,
897
+ c.updated_at,
898
+ c.metadata,
899
+ c.total_count
900
+ FROM counted c
901
+ ORDER BY c.created_at DESC
902
+ LIMIT p_page_size
903
+ OFFSET (p_page - 1) * p_page_size;
904
+ END;
905
+ $$ LANGUAGE plpgsql STABLE;
906
+
907
+ COMMENT ON FUNCTION fn_list_sessions_with_user IS
908
+ 'List sessions with user details and computed message counts. Joins messages on session name.';
909
+
763
910
  -- ============================================================================
764
911
  -- RECORD INSTALLATION
765
912
  -- ============================================================================