remdb 0.3.7__py3-none-any.whl → 0.3.133__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.
- rem/__init__.py +129 -2
- rem/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +16 -2
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +51 -25
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +112 -17
- rem/agentic/otel/setup.py +93 -4
- rem/agentic/providers/phoenix.py +314 -132
- rem/agentic/providers/pydantic_ai.py +215 -26
- rem/agentic/schema.py +361 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +238 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +154 -37
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +26 -5
- rem/api/mcp_router/tools.py +465 -7
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +124 -0
- rem/api/routers/chat/completions.py +402 -20
- rem/api/routers/chat/models.py +88 -10
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +542 -0
- rem/api/routers/chat/streaming.py +642 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +268 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/ask.py +13 -10
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +5 -6
- rem/cli/commands/db.py +396 -139
- rem/cli/commands/experiments.py +469 -74
- rem/cli/commands/process.py +22 -15
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +97 -50
- rem/cli/main.py +29 -6
- rem/config.py +10 -3
- rem/models/core/core_model.py +7 -1
- rem/models/core/experiment.py +54 -0
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/__init__.py +21 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +373 -0
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/providers.py +92 -133
- rem/services/content/service.py +92 -20
- rem/services/dreaming/affinity_service.py +2 -16
- rem/services/dreaming/moment_service.py +2 -15
- rem/services/embeddings/api.py +24 -17
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
- rem/services/phoenix/client.py +302 -28
- rem/services/postgres/README.md +159 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +531 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +291 -9
- rem/services/postgres/service.py +6 -6
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +24 -1
- rem/services/session/reload.py +1 -1
- rem/services/user_service.py +98 -0
- rem/settings.py +399 -29
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2320 -393
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/embeddings.py +17 -4
- rem/utils/files.py +167 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +282 -35
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/METADATA +460 -303
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/RECORD +105 -74
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1038
- {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/entry_points.txt +0 -0
|
@@ -218,6 +218,7 @@ COMMENT ON FUNCTION rebuild_kv_store() IS
|
|
|
218
218
|
-- Returns structured columns extracted from entity records
|
|
219
219
|
-- Parameters: entity_key, tenant_id (for backward compat), user_id (actual filter)
|
|
220
220
|
-- Note: tenant_id parameter exists for backward compatibility but is ignored
|
|
221
|
+
-- Note: Includes user-owned AND public (NULL user_id) resources
|
|
221
222
|
CREATE OR REPLACE FUNCTION rem_lookup(
|
|
222
223
|
p_entity_key VARCHAR(255),
|
|
223
224
|
p_tenant_id VARCHAR(100),
|
|
@@ -230,13 +231,17 @@ RETURNS TABLE(
|
|
|
230
231
|
DECLARE
|
|
231
232
|
entity_table VARCHAR(100);
|
|
232
233
|
query_sql TEXT;
|
|
234
|
+
effective_user_id VARCHAR(100);
|
|
233
235
|
BEGIN
|
|
234
|
-
|
|
236
|
+
effective_user_id := COALESCE(p_user_id, p_tenant_id);
|
|
237
|
+
|
|
235
238
|
-- First lookup in KV store to get entity_type (table name)
|
|
239
|
+
-- Include user-owned AND public (NULL user_id) entries
|
|
236
240
|
SELECT kv.entity_type INTO entity_table
|
|
237
241
|
FROM kv_store kv
|
|
238
|
-
WHERE kv.user_id =
|
|
239
|
-
AND kv.entity_key = p_entity_key
|
|
242
|
+
WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
|
|
243
|
+
AND kv.entity_key = p_entity_key
|
|
244
|
+
LIMIT 1;
|
|
240
245
|
|
|
241
246
|
-- If not found, return empty
|
|
242
247
|
IF entity_table IS NULL THEN
|
|
@@ -250,73 +255,63 @@ BEGIN
|
|
|
250
255
|
%L::VARCHAR(100) AS entity_type,
|
|
251
256
|
row_to_json(t)::jsonb AS data
|
|
252
257
|
FROM %I t
|
|
253
|
-
WHERE t.user_id = $1
|
|
258
|
+
WHERE (t.user_id = $1 OR t.user_id IS NULL)
|
|
254
259
|
AND t.name = $2
|
|
255
260
|
AND t.deleted_at IS NULL
|
|
256
261
|
', entity_table, entity_table);
|
|
257
262
|
|
|
258
|
-
RETURN QUERY EXECUTE query_sql USING
|
|
263
|
+
RETURN QUERY EXECUTE query_sql USING effective_user_id, p_entity_key;
|
|
259
264
|
END;
|
|
260
265
|
$$ LANGUAGE plpgsql STABLE;
|
|
261
266
|
|
|
262
267
|
COMMENT ON FUNCTION rem_lookup IS
|
|
263
|
-
'REM LOOKUP
|
|
268
|
+
'REM LOOKUP: O(1) entity lookup. Returns user-owned AND public (NULL user_id) entities.';
|
|
264
269
|
|
|
265
270
|
-- REM FETCH: Fetch full entity records from multiple tables
|
|
266
271
|
-- Takes JSONB mapping of {table_name: [entity_keys]}, fetches all records
|
|
267
272
|
-- Returns complete entity records as JSONB (not just KV store metadata)
|
|
273
|
+
-- Note: Includes user-owned AND public (NULL user_id) resources
|
|
268
274
|
CREATE OR REPLACE FUNCTION rem_fetch(
|
|
269
275
|
p_entities_by_table JSONB,
|
|
270
276
|
p_user_id VARCHAR(100)
|
|
271
277
|
)
|
|
272
278
|
RETURNS TABLE(
|
|
273
|
-
entity_key
|
|
274
|
-
entity_type
|
|
279
|
+
entity_key VARCHAR(255),
|
|
280
|
+
entity_type VARCHAR(100),
|
|
275
281
|
entity_record JSONB
|
|
276
282
|
) AS $$
|
|
277
283
|
DECLARE
|
|
278
284
|
table_name TEXT;
|
|
279
|
-
entity_keys
|
|
285
|
+
entity_keys JSONB;
|
|
280
286
|
query_sql TEXT;
|
|
281
|
-
result_record RECORD;
|
|
282
287
|
BEGIN
|
|
283
|
-
--
|
|
284
|
-
FOR table_name IN SELECT
|
|
288
|
+
-- For each table in the input JSONB
|
|
289
|
+
FOR table_name, entity_keys IN SELECT * FROM jsonb_each(p_entities_by_table)
|
|
285
290
|
LOOP
|
|
286
|
-
--
|
|
287
|
-
|
|
288
|
-
SELECT jsonb_array_elements_text(p_entities_by_table->table_name)
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
-- Build dynamic query for this table
|
|
291
|
+
-- Dynamic query to fetch records from the table
|
|
292
|
+
-- Include user-owned AND public (NULL user_id)
|
|
292
293
|
query_sql := format('
|
|
293
294
|
SELECT
|
|
294
|
-
t.name AS entity_key,
|
|
295
|
-
%L AS entity_type,
|
|
295
|
+
t.name::VARCHAR(255) AS entity_key,
|
|
296
|
+
%L::VARCHAR(100) AS entity_type,
|
|
296
297
|
row_to_json(t)::jsonb AS entity_record
|
|
297
298
|
FROM %I t
|
|
298
|
-
WHERE t.
|
|
299
|
-
AND t.
|
|
299
|
+
WHERE t.name = ANY(SELECT jsonb_array_elements_text($1))
|
|
300
|
+
AND (t.user_id = $2 OR t.user_id IS NULL)
|
|
300
301
|
AND t.deleted_at IS NULL
|
|
301
302
|
', table_name, table_name);
|
|
302
303
|
|
|
303
|
-
|
|
304
|
-
FOR result_record IN EXECUTE query_sql USING p_user_id, entity_keys
|
|
305
|
-
LOOP
|
|
306
|
-
entity_key := result_record.entity_key;
|
|
307
|
-
entity_type := result_record.entity_type;
|
|
308
|
-
entity_record := result_record.entity_record;
|
|
309
|
-
RETURN NEXT;
|
|
310
|
-
END LOOP;
|
|
304
|
+
RETURN QUERY EXECUTE query_sql USING entity_keys, p_user_id;
|
|
311
305
|
END LOOP;
|
|
312
306
|
END;
|
|
313
307
|
$$ LANGUAGE plpgsql STABLE;
|
|
314
308
|
|
|
315
309
|
COMMENT ON FUNCTION rem_fetch IS
|
|
316
|
-
'REM FETCH:
|
|
310
|
+
'REM FETCH: Batch fetch entities. Returns user-owned AND public (NULL user_id) entities.';
|
|
317
311
|
|
|
318
312
|
-- REM FUZZY: Fuzzy text search using pg_trgm similarity
|
|
319
313
|
-- Returns raw entity data as JSONB for LLM consumption
|
|
314
|
+
-- Note: Includes user-owned AND public (NULL user_id) resources
|
|
320
315
|
CREATE OR REPLACE FUNCTION rem_fuzzy(
|
|
321
316
|
p_query TEXT,
|
|
322
317
|
p_tenant_id VARCHAR(100),
|
|
@@ -333,16 +328,18 @@ DECLARE
|
|
|
333
328
|
kv_matches RECORD;
|
|
334
329
|
entities_by_table JSONB := '{}'::jsonb;
|
|
335
330
|
table_keys JSONB;
|
|
331
|
+
effective_user_id VARCHAR(100);
|
|
336
332
|
BEGIN
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
effective_user_id := COALESCE(p_user_id, p_tenant_id);
|
|
334
|
+
|
|
335
|
+
-- Find matching keys in KV store (user-owned AND public)
|
|
339
336
|
FOR kv_matches IN
|
|
340
337
|
SELECT
|
|
341
338
|
kv.entity_key,
|
|
342
339
|
kv.entity_type,
|
|
343
340
|
similarity(kv.entity_key, p_query) AS sim_score
|
|
344
341
|
FROM kv_store kv
|
|
345
|
-
WHERE kv.user_id =
|
|
342
|
+
WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
|
|
346
343
|
AND kv.entity_key % p_query -- Trigram similarity operator
|
|
347
344
|
AND similarity(kv.entity_key, p_query) >= p_threshold
|
|
348
345
|
ORDER BY sim_score DESC
|
|
@@ -365,29 +362,155 @@ BEGIN
|
|
|
365
362
|
END IF;
|
|
366
363
|
END LOOP;
|
|
367
364
|
|
|
368
|
-
-- Fetch full records using rem_fetch
|
|
369
|
-
-- Return raw entity data as JSONB for LLM consumption
|
|
370
|
-
-- Use p_user_id (not p_tenant_id) for actual filtering
|
|
365
|
+
-- Fetch full records using rem_fetch (which now supports NULL user_id)
|
|
371
366
|
RETURN QUERY
|
|
372
367
|
SELECT
|
|
373
368
|
f.entity_type::VARCHAR(100),
|
|
374
369
|
similarity(f.entity_key, p_query) AS similarity_score,
|
|
375
370
|
f.entity_record AS data
|
|
376
|
-
FROM rem_fetch(entities_by_table,
|
|
371
|
+
FROM rem_fetch(entities_by_table, effective_user_id) f
|
|
377
372
|
ORDER BY similarity_score DESC;
|
|
378
373
|
END;
|
|
379
374
|
$$ LANGUAGE plpgsql STABLE;
|
|
380
375
|
|
|
381
376
|
COMMENT ON FUNCTION rem_fuzzy IS
|
|
382
|
-
'REM FUZZY
|
|
377
|
+
'REM FUZZY: Fuzzy text search. Returns user-owned AND public (NULL user_id) entities.';
|
|
378
|
+
|
|
379
|
+
-- ============================================================================
|
|
380
|
+
-- REM TRAVERSE (Graph Traversal)
|
|
381
|
+
-- ============================================================================
|
|
382
|
+
|
|
383
|
+
-- REM TRAVERSE: Recursive graph traversal following edges
|
|
384
|
+
-- Explores graph_edges starting from entity_key up to max_depth
|
|
385
|
+
-- Uses cached kv_store.graph_edges for fast traversal (no polymorphic view!)
|
|
386
|
+
-- When keys_only=false, automatically fetches full entity records
|
|
387
|
+
-- Note: Includes user-owned AND public (NULL user_id) resources
|
|
388
|
+
CREATE OR REPLACE FUNCTION rem_traverse(
|
|
389
|
+
p_entity_key VARCHAR(255),
|
|
390
|
+
p_tenant_id VARCHAR(100), -- Backward compat parameter (not used for filtering)
|
|
391
|
+
p_user_id VARCHAR(100),
|
|
392
|
+
p_max_depth INTEGER DEFAULT 1,
|
|
393
|
+
p_rel_type VARCHAR(100) DEFAULT NULL,
|
|
394
|
+
p_keys_only BOOLEAN DEFAULT FALSE
|
|
395
|
+
)
|
|
396
|
+
RETURNS TABLE(
|
|
397
|
+
depth INTEGER,
|
|
398
|
+
entity_key VARCHAR(255),
|
|
399
|
+
entity_type VARCHAR(100),
|
|
400
|
+
entity_id UUID,
|
|
401
|
+
rel_type VARCHAR(100),
|
|
402
|
+
rel_weight REAL,
|
|
403
|
+
path TEXT[],
|
|
404
|
+
entity_record JSONB
|
|
405
|
+
) AS $$
|
|
406
|
+
DECLARE
|
|
407
|
+
graph_keys RECORD;
|
|
408
|
+
entities_by_table JSONB := '{}'::jsonb;
|
|
409
|
+
table_keys JSONB;
|
|
410
|
+
effective_user_id VARCHAR(100);
|
|
411
|
+
BEGIN
|
|
412
|
+
effective_user_id := COALESCE(p_user_id, p_tenant_id);
|
|
413
|
+
|
|
414
|
+
FOR graph_keys IN
|
|
415
|
+
WITH RECURSIVE graph_traversal AS (
|
|
416
|
+
-- Base case: Find starting entity (user-owned OR public)
|
|
417
|
+
SELECT
|
|
418
|
+
0 AS depth,
|
|
419
|
+
kv.entity_key,
|
|
420
|
+
kv.entity_type,
|
|
421
|
+
kv.entity_id,
|
|
422
|
+
NULL::VARCHAR(100) AS rel_type,
|
|
423
|
+
NULL::REAL AS rel_weight,
|
|
424
|
+
ARRAY[kv.entity_key]::TEXT[] AS path
|
|
425
|
+
FROM kv_store kv
|
|
426
|
+
WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
|
|
427
|
+
AND kv.entity_key = p_entity_key
|
|
428
|
+
|
|
429
|
+
UNION ALL
|
|
430
|
+
|
|
431
|
+
-- Recursive case: Follow outbound edges
|
|
432
|
+
SELECT
|
|
433
|
+
gt.depth + 1,
|
|
434
|
+
target_kv.entity_key,
|
|
435
|
+
target_kv.entity_type,
|
|
436
|
+
target_kv.entity_id,
|
|
437
|
+
(edge->>'rel_type')::VARCHAR(100) AS rel_type,
|
|
438
|
+
COALESCE((edge->>'weight')::REAL, 1.0) AS rel_weight,
|
|
439
|
+
gt.path || target_kv.entity_key AS path
|
|
440
|
+
FROM graph_traversal gt
|
|
441
|
+
JOIN kv_store source_kv ON source_kv.entity_key = gt.entity_key
|
|
442
|
+
AND (source_kv.user_id = effective_user_id OR source_kv.user_id IS NULL)
|
|
443
|
+
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)
|
|
445
|
+
AND (target_kv.user_id = effective_user_id OR target_kv.user_id IS NULL)
|
|
446
|
+
WHERE gt.depth < p_max_depth
|
|
447
|
+
AND (p_rel_type IS NULL OR (edge->>'rel_type')::VARCHAR(100) = p_rel_type)
|
|
448
|
+
AND NOT (target_kv.entity_key = ANY(gt.path))
|
|
449
|
+
)
|
|
450
|
+
SELECT DISTINCT ON (entity_key)
|
|
451
|
+
gt.depth,
|
|
452
|
+
gt.entity_key,
|
|
453
|
+
gt.entity_type,
|
|
454
|
+
gt.entity_id,
|
|
455
|
+
gt.rel_type,
|
|
456
|
+
gt.rel_weight,
|
|
457
|
+
gt.path
|
|
458
|
+
FROM graph_traversal gt
|
|
459
|
+
WHERE gt.depth > 0
|
|
460
|
+
ORDER BY gt.entity_key, gt.depth
|
|
461
|
+
LOOP
|
|
462
|
+
IF p_keys_only THEN
|
|
463
|
+
depth := graph_keys.depth;
|
|
464
|
+
entity_key := graph_keys.entity_key;
|
|
465
|
+
entity_type := graph_keys.entity_type;
|
|
466
|
+
entity_id := graph_keys.entity_id;
|
|
467
|
+
rel_type := graph_keys.rel_type;
|
|
468
|
+
rel_weight := graph_keys.rel_weight;
|
|
469
|
+
path := graph_keys.path;
|
|
470
|
+
entity_record := NULL;
|
|
471
|
+
RETURN NEXT;
|
|
472
|
+
ELSE
|
|
473
|
+
IF entities_by_table ? graph_keys.entity_type THEN
|
|
474
|
+
table_keys := entities_by_table->graph_keys.entity_type;
|
|
475
|
+
entities_by_table := jsonb_set(
|
|
476
|
+
entities_by_table,
|
|
477
|
+
ARRAY[graph_keys.entity_type],
|
|
478
|
+
table_keys || jsonb_build_array(graph_keys.entity_key)
|
|
479
|
+
);
|
|
480
|
+
ELSE
|
|
481
|
+
entities_by_table := jsonb_set(
|
|
482
|
+
entities_by_table,
|
|
483
|
+
ARRAY[graph_keys.entity_type],
|
|
484
|
+
jsonb_build_array(graph_keys.entity_key)
|
|
485
|
+
);
|
|
486
|
+
END IF;
|
|
487
|
+
END IF;
|
|
488
|
+
END LOOP;
|
|
383
489
|
|
|
384
|
-
|
|
385
|
-
|
|
490
|
+
IF NOT p_keys_only AND entities_by_table != '{}'::jsonb THEN
|
|
491
|
+
RETURN QUERY
|
|
492
|
+
SELECT
|
|
493
|
+
NULL::INTEGER AS depth,
|
|
494
|
+
f.entity_key::VARCHAR(255),
|
|
495
|
+
f.entity_type::VARCHAR(100),
|
|
496
|
+
NULL::UUID AS entity_id,
|
|
497
|
+
NULL::VARCHAR(100) AS rel_type,
|
|
498
|
+
NULL::REAL AS rel_weight,
|
|
499
|
+
NULL::TEXT[] AS path,
|
|
500
|
+
f.entity_record
|
|
501
|
+
FROM rem_fetch(entities_by_table, effective_user_id) f;
|
|
502
|
+
END IF;
|
|
503
|
+
END;
|
|
504
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
505
|
+
|
|
506
|
+
COMMENT ON FUNCTION rem_traverse IS
|
|
507
|
+
'REM TRAVERSE: Graph traversal. Returns user-owned AND public (NULL user_id) entities.';
|
|
386
508
|
|
|
387
509
|
-- REM SEARCH: Vector similarity search using embeddings
|
|
388
510
|
-- Joins to embeddings table for semantic search
|
|
511
|
+
-- Note: Includes user-owned AND public (NULL user_id) resources
|
|
389
512
|
CREATE OR REPLACE FUNCTION rem_search(
|
|
390
|
-
p_query_embedding vector
|
|
513
|
+
p_query_embedding vector,
|
|
391
514
|
p_table_name VARCHAR(100),
|
|
392
515
|
p_field_name VARCHAR(100),
|
|
393
516
|
p_tenant_id VARCHAR(100),
|
|
@@ -405,39 +528,38 @@ DECLARE
|
|
|
405
528
|
embeddings_table VARCHAR(200);
|
|
406
529
|
source_table VARCHAR(100);
|
|
407
530
|
query_sql TEXT;
|
|
531
|
+
effective_user_id VARCHAR(100);
|
|
408
532
|
BEGIN
|
|
409
|
-
-- Construct embeddings table name
|
|
410
533
|
embeddings_table := 'embeddings_' || p_table_name;
|
|
411
534
|
source_table := p_table_name;
|
|
535
|
+
effective_user_id := COALESCE(p_user_id, p_tenant_id);
|
|
412
536
|
|
|
413
|
-
--
|
|
414
|
-
--
|
|
415
|
-
--
|
|
416
|
-
-- Inner product <#> returns negative value, so we negate it to get [0, 1]
|
|
417
|
-
-- where 1 = perfect match, 0 = orthogonal
|
|
537
|
+
-- Uses cosine distance <=> operator (0-2 range, 0=identical)
|
|
538
|
+
-- Similarity = 1 - distance gives 0-1 range where 1 = most similar
|
|
539
|
+
-- Includes user-owned AND public (NULL user_id) resources
|
|
418
540
|
query_sql := format('
|
|
419
541
|
SELECT
|
|
420
542
|
%L::VARCHAR(100) AS entity_type,
|
|
421
|
-
(1.0 - (e.embedding
|
|
543
|
+
(1.0 - (e.embedding <=> $1))::REAL AS similarity_score,
|
|
422
544
|
row_to_json(t)::jsonb AS data
|
|
423
545
|
FROM %I t
|
|
424
546
|
JOIN %I e ON e.entity_id = t.id
|
|
425
|
-
WHERE t.user_id = $2
|
|
547
|
+
WHERE (t.user_id = $2 OR t.user_id IS NULL)
|
|
426
548
|
AND e.field_name = $3
|
|
427
549
|
AND e.provider = $4
|
|
428
|
-
AND (1.0 - (e.embedding
|
|
550
|
+
AND (1.0 - (e.embedding <=> $1)) >= $5
|
|
429
551
|
AND t.deleted_at IS NULL
|
|
430
|
-
ORDER BY e.embedding
|
|
552
|
+
ORDER BY e.embedding <=> $1
|
|
431
553
|
LIMIT $6
|
|
432
554
|
', source_table, source_table, embeddings_table);
|
|
433
555
|
|
|
434
556
|
RETURN QUERY EXECUTE query_sql
|
|
435
|
-
USING p_query_embedding,
|
|
557
|
+
USING p_query_embedding, effective_user_id, p_field_name, p_provider, p_min_similarity, p_limit;
|
|
436
558
|
END;
|
|
437
559
|
$$ LANGUAGE plpgsql STABLE;
|
|
438
560
|
|
|
439
561
|
COMMENT ON FUNCTION rem_search IS
|
|
440
|
-
'REM SEARCH
|
|
562
|
+
'REM SEARCH: Vector similarity search. Returns user-owned AND public (NULL user_id) resources.';
|
|
441
563
|
|
|
442
564
|
-- Function to get migration status
|
|
443
565
|
CREATE OR REPLACE FUNCTION migration_status()
|
|
@@ -464,6 +586,180 @@ $$ LANGUAGE plpgsql;
|
|
|
464
586
|
COMMENT ON FUNCTION migration_status() IS
|
|
465
587
|
'Get summary of applied migrations by type';
|
|
466
588
|
|
|
589
|
+
-- ============================================================================
|
|
590
|
+
-- RATE LIMITS (UNLOGGED for performance)
|
|
591
|
+
-- ============================================================================
|
|
592
|
+
-- High-performance rate limiting table. Uses UNLOGGED for speed - counts may
|
|
593
|
+
-- be lost on database crash/restart, which is acceptable (fail-open on error).
|
|
594
|
+
|
|
595
|
+
CREATE UNLOGGED TABLE IF NOT EXISTS rate_limits (
|
|
596
|
+
key VARCHAR(512) PRIMARY KEY,
|
|
597
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
598
|
+
expires_at TIMESTAMP NOT NULL,
|
|
599
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
CREATE INDEX IF NOT EXISTS idx_rate_limits_expires ON rate_limits (expires_at);
|
|
603
|
+
|
|
604
|
+
COMMENT ON TABLE rate_limits IS
|
|
605
|
+
'UNLOGGED rate limiting table. Counts may be lost on crash (acceptable for rate limiting).';
|
|
606
|
+
|
|
607
|
+
-- ============================================================================
|
|
608
|
+
-- SHARED SESSIONS HELPER FUNCTIONS
|
|
609
|
+
-- ============================================================================
|
|
610
|
+
-- Note: The shared_sessions TABLE is created by 002_install_models.sql (auto-generated)
|
|
611
|
+
-- These functions provide aggregate queries for the session sharing workflow.
|
|
612
|
+
|
|
613
|
+
-- Count distinct users sharing sessions with the current user
|
|
614
|
+
CREATE OR REPLACE FUNCTION fn_count_shared_with_me(
|
|
615
|
+
p_tenant_id VARCHAR(100),
|
|
616
|
+
p_user_id VARCHAR(256)
|
|
617
|
+
)
|
|
618
|
+
RETURNS BIGINT AS $$
|
|
619
|
+
BEGIN
|
|
620
|
+
RETURN (
|
|
621
|
+
SELECT COUNT(DISTINCT owner_user_id)
|
|
622
|
+
FROM shared_sessions
|
|
623
|
+
WHERE tenant_id = p_tenant_id
|
|
624
|
+
AND shared_with_user_id = p_user_id
|
|
625
|
+
AND deleted_at IS NULL
|
|
626
|
+
);
|
|
627
|
+
END;
|
|
628
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
629
|
+
|
|
630
|
+
COMMENT ON FUNCTION fn_count_shared_with_me IS
|
|
631
|
+
'Count distinct users sharing sessions with the specified user.';
|
|
632
|
+
|
|
633
|
+
-- Get aggregated summary of users sharing sessions with current user
|
|
634
|
+
CREATE OR REPLACE FUNCTION fn_get_shared_with_me(
|
|
635
|
+
p_tenant_id VARCHAR(100),
|
|
636
|
+
p_user_id VARCHAR(256),
|
|
637
|
+
p_limit INTEGER DEFAULT 50,
|
|
638
|
+
p_offset INTEGER DEFAULT 0
|
|
639
|
+
)
|
|
640
|
+
RETURNS TABLE(
|
|
641
|
+
user_id VARCHAR(256),
|
|
642
|
+
name VARCHAR(256),
|
|
643
|
+
email VARCHAR(256),
|
|
644
|
+
session_count BIGINT,
|
|
645
|
+
message_count BIGINT,
|
|
646
|
+
first_message_at TIMESTAMP,
|
|
647
|
+
last_message_at TIMESTAMP
|
|
648
|
+
) AS $$
|
|
649
|
+
BEGIN
|
|
650
|
+
RETURN QUERY
|
|
651
|
+
SELECT
|
|
652
|
+
ss.owner_user_id AS user_id,
|
|
653
|
+
COALESCE(u.name, ss.owner_user_id) AS name,
|
|
654
|
+
u.email AS email,
|
|
655
|
+
COUNT(DISTINCT ss.session_id)::BIGINT AS session_count,
|
|
656
|
+
COALESCE(SUM(msg_counts.msg_count), 0)::BIGINT AS message_count,
|
|
657
|
+
MIN(msg_counts.first_msg)::TIMESTAMP AS first_message_at,
|
|
658
|
+
MAX(msg_counts.last_msg)::TIMESTAMP AS last_message_at
|
|
659
|
+
FROM shared_sessions ss
|
|
660
|
+
LEFT JOIN users u ON u.user_id = ss.owner_user_id AND u.tenant_id = ss.tenant_id
|
|
661
|
+
LEFT JOIN (
|
|
662
|
+
SELECT
|
|
663
|
+
m.session_id,
|
|
664
|
+
m.user_id,
|
|
665
|
+
COUNT(*)::BIGINT AS msg_count,
|
|
666
|
+
MIN(m.created_at) AS first_msg,
|
|
667
|
+
MAX(m.created_at) AS last_msg
|
|
668
|
+
FROM messages m
|
|
669
|
+
WHERE m.tenant_id = p_tenant_id
|
|
670
|
+
AND m.deleted_at IS NULL
|
|
671
|
+
GROUP BY m.session_id, m.user_id
|
|
672
|
+
) msg_counts ON msg_counts.session_id = ss.session_id AND msg_counts.user_id = ss.owner_user_id
|
|
673
|
+
WHERE ss.tenant_id = p_tenant_id
|
|
674
|
+
AND ss.shared_with_user_id = p_user_id
|
|
675
|
+
AND ss.deleted_at IS NULL
|
|
676
|
+
GROUP BY ss.owner_user_id, u.name, u.email
|
|
677
|
+
ORDER BY MAX(msg_counts.last_msg) DESC NULLS LAST
|
|
678
|
+
LIMIT p_limit
|
|
679
|
+
OFFSET p_offset;
|
|
680
|
+
END;
|
|
681
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
682
|
+
|
|
683
|
+
COMMENT ON FUNCTION fn_get_shared_with_me IS
|
|
684
|
+
'Get aggregated summary of users sharing sessions with the specified user.';
|
|
685
|
+
|
|
686
|
+
-- Count messages in sessions shared by a specific user
|
|
687
|
+
CREATE OR REPLACE FUNCTION fn_count_shared_messages(
|
|
688
|
+
p_tenant_id VARCHAR(100),
|
|
689
|
+
p_recipient_user_id VARCHAR(256),
|
|
690
|
+
p_owner_user_id VARCHAR(256)
|
|
691
|
+
)
|
|
692
|
+
RETURNS BIGINT AS $$
|
|
693
|
+
BEGIN
|
|
694
|
+
RETURN (
|
|
695
|
+
SELECT COUNT(*)
|
|
696
|
+
FROM messages m
|
|
697
|
+
WHERE m.tenant_id = p_tenant_id
|
|
698
|
+
AND m.deleted_at IS NULL
|
|
699
|
+
AND m.session_id IN (
|
|
700
|
+
SELECT ss.session_id
|
|
701
|
+
FROM shared_sessions ss
|
|
702
|
+
WHERE ss.tenant_id = p_tenant_id
|
|
703
|
+
AND ss.owner_user_id = p_owner_user_id
|
|
704
|
+
AND ss.shared_with_user_id = p_recipient_user_id
|
|
705
|
+
AND ss.deleted_at IS NULL
|
|
706
|
+
)
|
|
707
|
+
);
|
|
708
|
+
END;
|
|
709
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
710
|
+
|
|
711
|
+
COMMENT ON FUNCTION fn_count_shared_messages IS
|
|
712
|
+
'Count messages in sessions shared by a specific user with the recipient.';
|
|
713
|
+
|
|
714
|
+
-- Get messages from sessions shared by a specific user
|
|
715
|
+
CREATE OR REPLACE FUNCTION fn_get_shared_messages(
|
|
716
|
+
p_tenant_id VARCHAR(100),
|
|
717
|
+
p_recipient_user_id VARCHAR(256),
|
|
718
|
+
p_owner_user_id VARCHAR(256),
|
|
719
|
+
p_limit INTEGER DEFAULT 50,
|
|
720
|
+
p_offset INTEGER DEFAULT 0
|
|
721
|
+
)
|
|
722
|
+
RETURNS TABLE(
|
|
723
|
+
id UUID,
|
|
724
|
+
content TEXT,
|
|
725
|
+
message_type VARCHAR(256),
|
|
726
|
+
session_id VARCHAR(256),
|
|
727
|
+
model VARCHAR(256),
|
|
728
|
+
token_count INTEGER,
|
|
729
|
+
created_at TIMESTAMP,
|
|
730
|
+
metadata JSONB
|
|
731
|
+
) AS $$
|
|
732
|
+
BEGIN
|
|
733
|
+
RETURN QUERY
|
|
734
|
+
SELECT
|
|
735
|
+
m.id,
|
|
736
|
+
m.content,
|
|
737
|
+
m.message_type,
|
|
738
|
+
m.session_id,
|
|
739
|
+
m.model,
|
|
740
|
+
m.token_count,
|
|
741
|
+
m.created_at,
|
|
742
|
+
m.metadata
|
|
743
|
+
FROM messages m
|
|
744
|
+
WHERE m.tenant_id = p_tenant_id
|
|
745
|
+
AND m.deleted_at IS NULL
|
|
746
|
+
AND m.session_id IN (
|
|
747
|
+
SELECT ss.session_id
|
|
748
|
+
FROM shared_sessions ss
|
|
749
|
+
WHERE ss.tenant_id = p_tenant_id
|
|
750
|
+
AND ss.owner_user_id = p_owner_user_id
|
|
751
|
+
AND ss.shared_with_user_id = p_recipient_user_id
|
|
752
|
+
AND ss.deleted_at IS NULL
|
|
753
|
+
)
|
|
754
|
+
ORDER BY m.created_at DESC
|
|
755
|
+
LIMIT p_limit
|
|
756
|
+
OFFSET p_offset;
|
|
757
|
+
END;
|
|
758
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
759
|
+
|
|
760
|
+
COMMENT ON FUNCTION fn_get_shared_messages IS
|
|
761
|
+
'Get messages from sessions shared by a specific user with the recipient.';
|
|
762
|
+
|
|
467
763
|
-- ============================================================================
|
|
468
764
|
-- RECORD INSTALLATION
|
|
469
765
|
-- ============================================================================
|
|
@@ -474,6 +770,43 @@ ON CONFLICT (name) DO UPDATE
|
|
|
474
770
|
SET applied_at = CURRENT_TIMESTAMP,
|
|
475
771
|
applied_by = CURRENT_USER;
|
|
476
772
|
|
|
773
|
+
-- ============================================================================
|
|
774
|
+
-- GRANTS FOR APPLICATION USER
|
|
775
|
+
-- ============================================================================
|
|
776
|
+
-- Grant permissions to remuser (the application database user)
|
|
777
|
+
-- This ensures the application can run migrations and manage schema
|
|
778
|
+
-- Note: remuser is created by CNPG as the database owner in bootstrap.initdb.owner
|
|
779
|
+
|
|
780
|
+
DO $$
|
|
781
|
+
DECLARE
|
|
782
|
+
app_user TEXT := 'remuser';
|
|
783
|
+
BEGIN
|
|
784
|
+
-- Only grant if the user exists (handles different deployment scenarios)
|
|
785
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = app_user) THEN
|
|
786
|
+
-- Grant ownership of migration tracking table so app can record migrations
|
|
787
|
+
EXECUTE format('ALTER TABLE rem_migrations OWNER TO %I', app_user);
|
|
788
|
+
EXECUTE format('ALTER TABLE kv_store OWNER TO %I', app_user);
|
|
789
|
+
EXECUTE format('ALTER TABLE rate_limits OWNER TO %I', app_user);
|
|
790
|
+
|
|
791
|
+
-- Grant usage on schema
|
|
792
|
+
EXECUTE format('GRANT ALL ON SCHEMA public TO %I', app_user);
|
|
793
|
+
|
|
794
|
+
-- Grant privileges on all tables in public schema
|
|
795
|
+
EXECUTE format('GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO %I', app_user);
|
|
796
|
+
EXECUTE format('GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO %I', app_user);
|
|
797
|
+
EXECUTE format('GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO %I', app_user);
|
|
798
|
+
|
|
799
|
+
-- Set default privileges for future objects
|
|
800
|
+
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO %I', app_user);
|
|
801
|
+
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO %I', app_user);
|
|
802
|
+
EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO %I', app_user);
|
|
803
|
+
|
|
804
|
+
RAISE NOTICE '✓ Granted permissions to application user: %', app_user;
|
|
805
|
+
ELSE
|
|
806
|
+
RAISE NOTICE 'Application user % does not exist, skipping grants', app_user;
|
|
807
|
+
END IF;
|
|
808
|
+
END $$;
|
|
809
|
+
|
|
477
810
|
-- ============================================================================
|
|
478
811
|
-- COMPLETION
|
|
479
812
|
-- ============================================================================
|