remdb 0.3.114__py3-none-any.whl → 0.3.127__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 (41) hide show
  1. rem/agentic/agents/sse_simulator.py +2 -0
  2. rem/agentic/context.py +23 -3
  3. rem/agentic/mcp/tool_wrapper.py +29 -3
  4. rem/agentic/otel/setup.py +1 -0
  5. rem/agentic/providers/pydantic_ai.py +26 -2
  6. rem/api/main.py +4 -1
  7. rem/api/mcp_router/server.py +9 -3
  8. rem/api/mcp_router/tools.py +324 -2
  9. rem/api/routers/admin.py +218 -1
  10. rem/api/routers/chat/completions.py +250 -4
  11. rem/api/routers/chat/models.py +81 -7
  12. rem/api/routers/chat/otel_utils.py +33 -0
  13. rem/api/routers/chat/sse_events.py +17 -1
  14. rem/api/routers/chat/streaming.py +35 -1
  15. rem/api/routers/feedback.py +134 -14
  16. rem/api/routers/query.py +6 -3
  17. rem/cli/commands/README.md +42 -0
  18. rem/cli/commands/cluster.py +617 -168
  19. rem/cli/commands/configure.py +1 -3
  20. rem/cli/commands/db.py +66 -22
  21. rem/cli/commands/experiments.py +242 -26
  22. rem/cli/commands/schema.py +6 -5
  23. rem/config.py +8 -1
  24. rem/services/phoenix/client.py +59 -18
  25. rem/services/postgres/diff_service.py +108 -3
  26. rem/services/postgres/schema_generator.py +205 -4
  27. rem/services/session/compression.py +7 -0
  28. rem/settings.py +150 -18
  29. rem/sql/migrations/001_install.sql +156 -0
  30. rem/sql/migrations/002_install_models.sql +1864 -1
  31. rem/sql/migrations/004_cache_system.sql +548 -0
  32. rem/utils/__init__.py +18 -0
  33. rem/utils/schema_loader.py +94 -3
  34. rem/utils/sql_paths.py +146 -0
  35. rem/workers/__init__.py +3 -1
  36. rem/workers/db_listener.py +579 -0
  37. rem/workers/unlogged_maintainer.py +463 -0
  38. {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/METADATA +213 -177
  39. {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/RECORD +41 -36
  40. {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/WHEEL +0 -0
  41. {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/entry_points.txt +0 -0
rem/settings.py CHANGED
@@ -33,14 +33,15 @@ Example .env file:
33
33
  AUTH__OIDC_CLIENT_ID=your-client-id
34
34
  AUTH__SESSION_SECRET=your-secret-key
35
35
 
36
- # OpenTelemetry (disabled by default)
36
+ # OpenTelemetry (disabled by default - enable via env var when collector available)
37
+ # Standard OTLP collector ports: 4317 (gRPC), 4318 (HTTP)
37
38
  OTEL__ENABLED=false
38
39
  OTEL__SERVICE_NAME=rem-api
39
- OTEL__COLLECTOR_ENDPOINT=http://localhost:4318
40
- OTEL__PROTOCOL=http
40
+ OTEL__COLLECTOR_ENDPOINT=http://localhost:4317
41
+ OTEL__PROTOCOL=grpc
41
42
 
42
- # Arize Phoenix (disabled by default)
43
- PHOENIX__ENABLED=false
43
+ # Arize Phoenix (enabled by default - can be disabled via env var)
44
+ PHOENIX__ENABLED=true
44
45
  PHOENIX__COLLECTOR_ENDPOINT=http://localhost:6006/v1/traces
45
46
  PHOENIX__PROJECT_NAME=rem
46
47
 
@@ -241,6 +242,11 @@ class OTELSettings(BaseSettings):
241
242
  description="Export timeout in milliseconds",
242
243
  )
243
244
 
245
+ insecure: bool = Field(
246
+ default=True,
247
+ description="Use insecure (non-TLS) gRPC connection (default: True for local dev)",
248
+ )
249
+
244
250
 
245
251
  class PhoenixSettings(BaseSettings):
246
252
  """
@@ -267,8 +273,8 @@ class PhoenixSettings(BaseSettings):
267
273
  )
268
274
 
269
275
  enabled: bool = Field(
270
- default=False,
271
- description="Enable Phoenix integration (disabled by default for local dev)",
276
+ default=True,
277
+ description="Enable Phoenix integration (enabled by default)",
272
278
  )
273
279
 
274
280
  base_url: str = Field(
@@ -1051,10 +1057,26 @@ class ModelsSettings(BaseSettings):
1051
1057
 
1052
1058
  @property
1053
1059
  def module_list(self) -> list[str]:
1054
- """Get modules as a list, filtering empty strings."""
1055
- if not self.import_modules:
1056
- return []
1057
- return [m.strip() for m in self.import_modules.split(";") if m.strip()]
1060
+ """
1061
+ Get modules as a list, filtering empty strings.
1062
+
1063
+ Auto-detects ./models folder if it exists and is importable.
1064
+ """
1065
+ modules = []
1066
+ if self.import_modules:
1067
+ modules = [m.strip() for m in self.import_modules.split(";") if m.strip()]
1068
+
1069
+ # Auto-detect ./models if it exists and is a Python package (convention over configuration)
1070
+ from pathlib import Path
1071
+
1072
+ models_path = Path("./models")
1073
+ if models_path.exists() and models_path.is_dir():
1074
+ # Check if it's a Python package (has __init__.py)
1075
+ if (models_path / "__init__.py").exists():
1076
+ if "models" not in modules:
1077
+ modules.insert(0, "models")
1078
+
1079
+ return modules
1058
1080
 
1059
1081
 
1060
1082
  class SchemaSettings(BaseSettings):
@@ -1240,6 +1262,110 @@ class GitSettings(BaseSettings):
1240
1262
  )
1241
1263
 
1242
1264
 
1265
+ class DBListenerSettings(BaseSettings):
1266
+ """
1267
+ PostgreSQL LISTEN/NOTIFY database listener settings.
1268
+
1269
+ The DB Listener is a lightweight worker that subscribes to PostgreSQL
1270
+ NOTIFY events and dispatches them to external systems (SQS, REST, custom).
1271
+
1272
+ Architecture:
1273
+ - Single-replica deployment (to avoid duplicate processing)
1274
+ - Dedicated connection for LISTEN (not from connection pool)
1275
+ - Automatic reconnection with exponential backoff
1276
+ - Graceful shutdown on SIGTERM
1277
+
1278
+ Use Cases:
1279
+ - Sync data changes to external systems (Phoenix, webhooks)
1280
+ - Trigger async jobs without polling
1281
+ - Event-driven architectures with PostgreSQL as event source
1282
+
1283
+ Example PostgreSQL trigger:
1284
+ CREATE OR REPLACE FUNCTION notify_feedback_insert()
1285
+ RETURNS TRIGGER AS $$
1286
+ BEGIN
1287
+ PERFORM pg_notify('feedback_sync', json_build_object(
1288
+ 'id', NEW.id,
1289
+ 'table', 'feedbacks',
1290
+ 'action', 'insert'
1291
+ )::text);
1292
+ RETURN NEW;
1293
+ END;
1294
+ $$ LANGUAGE plpgsql;
1295
+
1296
+ Environment variables:
1297
+ DB_LISTENER__ENABLED - Enable the listener worker (default: false)
1298
+ DB_LISTENER__CHANNELS - Comma-separated PostgreSQL channels to listen on
1299
+ DB_LISTENER__HANDLER_TYPE - Handler type: 'sqs', 'rest', or 'custom'
1300
+ DB_LISTENER__SQS_QUEUE_URL - SQS queue URL (for handler_type=sqs)
1301
+ DB_LISTENER__REST_ENDPOINT - REST endpoint URL (for handler_type=rest)
1302
+ DB_LISTENER__RECONNECT_DELAY - Initial reconnect delay in seconds
1303
+ DB_LISTENER__MAX_RECONNECT_DELAY - Maximum reconnect delay in seconds
1304
+
1305
+ References:
1306
+ - PostgreSQL NOTIFY: https://www.postgresql.org/docs/current/sql-notify.html
1307
+ - Brandur's Notifier: https://brandur.org/notifier
1308
+ """
1309
+
1310
+ model_config = SettingsConfigDict(
1311
+ env_prefix="DB_LISTENER__",
1312
+ env_file=".env",
1313
+ env_file_encoding="utf-8",
1314
+ extra="ignore",
1315
+ )
1316
+
1317
+ enabled: bool = Field(
1318
+ default=False,
1319
+ description="Enable the DB Listener worker (disabled by default)",
1320
+ )
1321
+
1322
+ channels: str = Field(
1323
+ default="",
1324
+ description=(
1325
+ "Comma-separated list of PostgreSQL channels to LISTEN on. "
1326
+ "Example: 'feedback_sync,entity_update,user_events'"
1327
+ ),
1328
+ )
1329
+
1330
+ handler_type: str = Field(
1331
+ default="rest",
1332
+ description=(
1333
+ "Handler type for dispatching notifications. Options: "
1334
+ "'sqs' (publish to SQS), 'rest' (POST to endpoint), 'custom' (Python handlers)"
1335
+ ),
1336
+ )
1337
+
1338
+ sqs_queue_url: str = Field(
1339
+ default="",
1340
+ description="SQS queue URL for handler_type='sqs'",
1341
+ )
1342
+
1343
+ rest_endpoint: str = Field(
1344
+ default="http://localhost:8000/api/v1/internal/events",
1345
+ description=(
1346
+ "REST endpoint URL for handler_type='rest'. "
1347
+ "Receives POST with {channel, payload, source} JSON body."
1348
+ ),
1349
+ )
1350
+
1351
+ reconnect_delay: float = Field(
1352
+ default=1.0,
1353
+ description="Initial delay (seconds) between reconnection attempts",
1354
+ )
1355
+
1356
+ max_reconnect_delay: float = Field(
1357
+ default=60.0,
1358
+ description="Maximum delay (seconds) between reconnection attempts (exponential backoff cap)",
1359
+ )
1360
+
1361
+ @property
1362
+ def channel_list(self) -> list[str]:
1363
+ """Get channels as a list, filtering empty strings."""
1364
+ if not self.channels:
1365
+ return []
1366
+ return [c.strip() for c in self.channels.split(",") if c.strip()]
1367
+
1368
+
1243
1369
  class TestSettings(BaseSettings):
1244
1370
  """
1245
1371
  Test environment settings.
@@ -1334,11 +1460,6 @@ class Settings(BaseSettings):
1334
1460
  description="Root path for reverse proxy (e.g., /rem for ALB routing)",
1335
1461
  )
1336
1462
 
1337
- sql_dir: str = Field(
1338
- default="src/rem/sql",
1339
- description="Directory for SQL files and migrations",
1340
- )
1341
-
1342
1463
  # Nested settings groups
1343
1464
  api: APISettings = Field(default_factory=APISettings)
1344
1465
  chat: ChatSettings = Field(default_factory=ChatSettings)
@@ -1354,16 +1475,27 @@ class Settings(BaseSettings):
1354
1475
  s3: S3Settings = Field(default_factory=S3Settings)
1355
1476
  git: GitSettings = Field(default_factory=GitSettings)
1356
1477
  sqs: SQSSettings = Field(default_factory=SQSSettings)
1478
+ db_listener: DBListenerSettings = Field(default_factory=DBListenerSettings)
1357
1479
  chunking: ChunkingSettings = Field(default_factory=ChunkingSettings)
1358
1480
  content: ContentSettings = Field(default_factory=ContentSettings)
1359
1481
  schema_search: SchemaSettings = Field(default_factory=SchemaSettings)
1360
1482
  test: TestSettings = Field(default_factory=TestSettings)
1361
1483
 
1362
1484
 
1485
+ # Auto-load .env file from current directory if it exists
1486
+ # This happens BEFORE config file loading, so .env takes precedence
1487
+ from pathlib import Path
1488
+ from dotenv import load_dotenv
1489
+
1490
+ _dotenv_path = Path(".env")
1491
+ if _dotenv_path.exists():
1492
+ load_dotenv(_dotenv_path, override=False) # Don't override existing env vars
1493
+ logger.debug(f"Loaded environment from {_dotenv_path.resolve()}")
1494
+
1363
1495
  # Load configuration from ~/.rem/config.yaml before initializing settings
1364
1496
  # This allows user configuration to be merged with environment variables
1365
- # Set REM_SKIP_CONFIG_FILE=true to disable (useful for development with .env)
1366
- if not os.getenv("REM_SKIP_CONFIG_FILE", "").lower() in ("true", "1", "yes"):
1497
+ # Set REM_SKIP_CONFIG=1 to disable (useful for development with .env)
1498
+ if not os.getenv("REM_SKIP_CONFIG", "").lower() in ("true", "1", "yes"):
1367
1499
  try:
1368
1500
  from rem.config import load_config, merge_config_to_env
1369
1501
 
@@ -604,6 +604,162 @@ CREATE INDEX IF NOT EXISTS idx_rate_limits_expires ON rate_limits (expires_at);
604
604
  COMMENT ON TABLE rate_limits IS
605
605
  'UNLOGGED rate limiting table. Counts may be lost on crash (acceptable for rate limiting).';
606
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
+
607
763
  -- ============================================================================
608
764
  -- RECORD INSTALLATION
609
765
  -- ============================================================================