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.
- rem/agentic/agents/__init__.py +16 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +2 -0
- rem/agentic/context.py +103 -5
- rem/agentic/context_builder.py +36 -9
- rem/agentic/mcp/tool_wrapper.py +161 -18
- rem/agentic/otel/setup.py +1 -0
- rem/agentic/providers/phoenix.py +371 -108
- rem/agentic/providers/pydantic_ai.py +172 -30
- rem/agentic/schema.py +8 -4
- rem/api/deps.py +3 -5
- rem/api/main.py +26 -4
- rem/api/mcp_router/resources.py +15 -10
- rem/api/mcp_router/server.py +11 -3
- rem/api/mcp_router/tools.py +418 -4
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/admin.py +218 -1
- rem/api/routers/auth.py +349 -6
- rem/api/routers/chat/completions.py +255 -7
- rem/api/routers/chat/models.py +81 -7
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +17 -1
- rem/api/routers/chat/streaming.py +126 -19
- rem/api/routers/feedback.py +134 -14
- rem/api/routers/messages.py +24 -15
- rem/api/routers/query.py +6 -3
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +115 -10
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/README.md +42 -0
- rem/cli/commands/cluster.py +617 -168
- rem/cli/commands/configure.py +4 -7
- rem/cli/commands/db.py +66 -22
- rem/cli/commands/experiments.py +468 -76
- rem/cli/commands/schema.py +6 -5
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +2 -0
- rem/config.py +8 -1
- rem/models/core/experiment.py +58 -14
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/ontology.py +1 -1
- rem/models/entities/ontology_config.py +1 -1
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
- rem/schemas/agents/examples/contract-extractor.yaml +1 -1
- rem/schemas/agents/examples/cv-parser.yaml +1 -1
- rem/services/__init__.py +3 -1
- rem/services/content/service.py +4 -3
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +513 -0
- rem/services/email/templates.py +360 -0
- rem/services/phoenix/client.py +59 -18
- rem/services/postgres/README.md +38 -0
- rem/services/postgres/diff_service.py +127 -6
- rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
- rem/services/postgres/repository.py +5 -4
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/session/compression.py +120 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +41 -9
- rem/settings.py +442 -23
- rem/sql/migrations/001_install.sql +156 -0
- rem/sql/migrations/002_install_models.sql +1951 -88
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/__init__.py +18 -0
- rem/utils/files.py +157 -1
- rem/utils/schema_loader.py +139 -10
- rem/utils/sql_paths.py +146 -0
- rem/utils/vision.py +1 -1
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/METADATA +218 -180
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/RECORD +83 -68
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/entry_points.txt +0 -0
rem/settings.py
CHANGED
|
@@ -21,8 +21,8 @@ Example .env file:
|
|
|
21
21
|
LLM__OPENAI_API_KEY=sk-...
|
|
22
22
|
LLM__ANTHROPIC_API_KEY=sk-ant-...
|
|
23
23
|
|
|
24
|
-
# Database (port
|
|
25
|
-
POSTGRES__CONNECTION_STRING=postgresql://rem:rem@localhost:
|
|
24
|
+
# Database (port 5051 for Docker Compose prebuilt, 5050 for local dev)
|
|
25
|
+
POSTGRES__CONNECTION_STRING=postgresql://rem:rem@localhost:5051/rem
|
|
26
26
|
POSTGRES__POOL_MIN_SIZE=5
|
|
27
27
|
POSTGRES__POOL_MAX_SIZE=20
|
|
28
28
|
POSTGRES__STATEMENT_TIMEOUT=30000
|
|
@@ -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:
|
|
40
|
-
OTEL__PROTOCOL=
|
|
40
|
+
OTEL__COLLECTOR_ENDPOINT=http://localhost:4317
|
|
41
|
+
OTEL__PROTOCOL=grpc
|
|
41
42
|
|
|
42
|
-
# Arize Phoenix (
|
|
43
|
-
PHOENIX__ENABLED=
|
|
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
|
|
|
@@ -76,6 +77,7 @@ class LLMSettings(BaseSettings):
|
|
|
76
77
|
LLM__ANTHROPIC_API_KEY or ANTHROPIC_API_KEY - Anthropic API key
|
|
77
78
|
LLM__EMBEDDING_PROVIDER or EMBEDDING_PROVIDER - Default embedding provider (openai)
|
|
78
79
|
LLM__EMBEDDING_MODEL or EMBEDDING_MODEL - Default embedding model name
|
|
80
|
+
LLM__DEFAULT_STRUCTURED_OUTPUT - Default structured output mode (False = streaming text)
|
|
79
81
|
"""
|
|
80
82
|
|
|
81
83
|
model_config = SettingsConfigDict(
|
|
@@ -137,6 +139,11 @@ class LLMSettings(BaseSettings):
|
|
|
137
139
|
description="Default embedding model (provider-specific model name)",
|
|
138
140
|
)
|
|
139
141
|
|
|
142
|
+
default_structured_output: bool = Field(
|
|
143
|
+
default=False,
|
|
144
|
+
description="Default structured output mode for agents. False = streaming text (easier), True = JSON schema validation",
|
|
145
|
+
)
|
|
146
|
+
|
|
140
147
|
@field_validator("openai_api_key", mode="before")
|
|
141
148
|
@classmethod
|
|
142
149
|
def validate_openai_api_key(cls, v):
|
|
@@ -241,6 +248,11 @@ class OTELSettings(BaseSettings):
|
|
|
241
248
|
description="Export timeout in milliseconds",
|
|
242
249
|
)
|
|
243
250
|
|
|
251
|
+
insecure: bool = Field(
|
|
252
|
+
default=True,
|
|
253
|
+
description="Use insecure (non-TLS) gRPC connection (default: True for local dev)",
|
|
254
|
+
)
|
|
255
|
+
|
|
244
256
|
|
|
245
257
|
class PhoenixSettings(BaseSettings):
|
|
246
258
|
"""
|
|
@@ -267,8 +279,8 @@ class PhoenixSettings(BaseSettings):
|
|
|
267
279
|
)
|
|
268
280
|
|
|
269
281
|
enabled: bool = Field(
|
|
270
|
-
default=
|
|
271
|
-
description="Enable Phoenix integration (
|
|
282
|
+
default=True,
|
|
283
|
+
description="Enable Phoenix integration (enabled by default)",
|
|
272
284
|
)
|
|
273
285
|
|
|
274
286
|
base_url: str = Field(
|
|
@@ -458,10 +470,11 @@ class PostgresSettings(BaseSettings):
|
|
|
458
470
|
)
|
|
459
471
|
|
|
460
472
|
connection_string: str = Field(
|
|
461
|
-
default="postgresql://rem:rem@localhost:
|
|
462
|
-
description="PostgreSQL connection string (default uses Docker Compose port
|
|
473
|
+
default="postgresql://rem:rem@localhost:5051/rem",
|
|
474
|
+
description="PostgreSQL connection string (default uses Docker Compose prebuilt port 5051)",
|
|
463
475
|
)
|
|
464
476
|
|
|
477
|
+
|
|
465
478
|
pool_size: int = Field(
|
|
466
479
|
default=10,
|
|
467
480
|
description="Connection pool size (deprecated, use pool_min_size/pool_max_size)",
|
|
@@ -686,6 +699,91 @@ class S3Settings(BaseSettings):
|
|
|
686
699
|
)
|
|
687
700
|
|
|
688
701
|
|
|
702
|
+
class DataLakeSettings(BaseSettings):
|
|
703
|
+
"""
|
|
704
|
+
Data lake settings for experiment and dataset storage.
|
|
705
|
+
|
|
706
|
+
Data Lake Convention:
|
|
707
|
+
The data lake provides a standardized structure for storing datasets,
|
|
708
|
+
experiments, and calibration data in S3. Users bring their own bucket
|
|
709
|
+
and the version is pinned by default to v0 in the path.
|
|
710
|
+
|
|
711
|
+
S3 Path Structure:
|
|
712
|
+
s3://{bucket}/{version}/datasets/
|
|
713
|
+
├── raw/ # Raw source data + transformers
|
|
714
|
+
│ └── {dataset_name}/ # e.g., cns_drugs, codes, care
|
|
715
|
+
├── tables/ # Database table data (JSONL)
|
|
716
|
+
│ ├── resources/ # → resources table
|
|
717
|
+
│ │ ├── drugs/{category}/ # Psychotropic drugs
|
|
718
|
+
│ │ ├── care/stages/ # Treatment stages
|
|
719
|
+
│ │ └── crisis/ # Crisis resources
|
|
720
|
+
│ └── codes/ # → codes table
|
|
721
|
+
│ ├── icd10/{category}/ # ICD-10 codes
|
|
722
|
+
│ └── cpt/ # CPT codes
|
|
723
|
+
└── calibration/ # Agent calibration
|
|
724
|
+
├── experiments/ # Experiment configs + results
|
|
725
|
+
│ └── {agent}/{task}/ # e.g., siggy/risk-assessment
|
|
726
|
+
└── datasets/ # Shared evaluation datasets
|
|
727
|
+
|
|
728
|
+
Experiment Storage:
|
|
729
|
+
- Local: experiments/{agent}/{task}/experiment.yaml
|
|
730
|
+
- S3: s3://{bucket}/{version}/datasets/calibration/experiments/{agent}/{task}/
|
|
731
|
+
|
|
732
|
+
Environment variables:
|
|
733
|
+
DATA_LAKE__BUCKET_NAME - S3 bucket for data lake (required)
|
|
734
|
+
DATA_LAKE__VERSION - Path version prefix (default: v0)
|
|
735
|
+
DATA_LAKE__DATASETS_PREFIX - Datasets directory (default: datasets)
|
|
736
|
+
DATA_LAKE__EXPERIMENTS_PREFIX - Experiments subdirectory (default: experiments)
|
|
737
|
+
"""
|
|
738
|
+
|
|
739
|
+
model_config = SettingsConfigDict(
|
|
740
|
+
env_prefix="DATA_LAKE__",
|
|
741
|
+
env_file=".env",
|
|
742
|
+
env_file_encoding="utf-8",
|
|
743
|
+
extra="ignore",
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
bucket_name: str | None = Field(
|
|
747
|
+
default=None,
|
|
748
|
+
description="S3 bucket for data lake storage (user-provided)",
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
version: str = Field(
|
|
752
|
+
default="v0",
|
|
753
|
+
description="API version for data lake paths",
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
datasets_prefix: str = Field(
|
|
757
|
+
default="datasets",
|
|
758
|
+
description="Root directory for datasets in the bucket",
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
experiments_prefix: str = Field(
|
|
762
|
+
default="experiments",
|
|
763
|
+
description="Subdirectory within calibration for experiments",
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
def get_base_uri(self) -> str | None:
|
|
767
|
+
"""Get the base S3 URI for the data lake."""
|
|
768
|
+
if not self.bucket_name:
|
|
769
|
+
return None
|
|
770
|
+
return f"s3://{self.bucket_name}/{self.version}/{self.datasets_prefix}"
|
|
771
|
+
|
|
772
|
+
def get_experiment_uri(self, agent: str, task: str = "general") -> str | None:
|
|
773
|
+
"""Get the S3 URI for an experiment."""
|
|
774
|
+
base = self.get_base_uri()
|
|
775
|
+
if not base:
|
|
776
|
+
return None
|
|
777
|
+
return f"{base}/calibration/{self.experiments_prefix}/{agent}/{task}"
|
|
778
|
+
|
|
779
|
+
def get_tables_uri(self, table: str = "resources") -> str | None:
|
|
780
|
+
"""Get the S3 URI for a table directory."""
|
|
781
|
+
base = self.get_base_uri()
|
|
782
|
+
if not base:
|
|
783
|
+
return None
|
|
784
|
+
return f"{base}/tables/{table}"
|
|
785
|
+
|
|
786
|
+
|
|
689
787
|
class ChunkingSettings(BaseSettings):
|
|
690
788
|
"""
|
|
691
789
|
Document chunking settings for semantic text splitting.
|
|
@@ -936,7 +1034,7 @@ class ChatSettings(BaseSettings):
|
|
|
936
1034
|
- Prevents context window bloat while maintaining conversation continuity
|
|
937
1035
|
|
|
938
1036
|
User Context (on-demand by default):
|
|
939
|
-
- Agent system prompt includes: "User
|
|
1037
|
+
- Agent system prompt includes: "User: {email}. To load user profile: Use REM LOOKUP \"{email}\""
|
|
940
1038
|
- Agent decides whether to load profile based on query
|
|
941
1039
|
- More efficient for queries that don't need personalization
|
|
942
1040
|
|
|
@@ -969,6 +1067,8 @@ class APISettings(BaseSettings):
|
|
|
969
1067
|
API__RELOAD - Enable auto-reload for development
|
|
970
1068
|
API__WORKERS - Number of worker processes (production)
|
|
971
1069
|
API__LOG_LEVEL - Logging level (debug, info, warning, error)
|
|
1070
|
+
API__API_KEY_ENABLED - Enable X-API-Key header authentication
|
|
1071
|
+
API__API_KEY - API key for X-API-Key authentication
|
|
972
1072
|
"""
|
|
973
1073
|
|
|
974
1074
|
model_config = SettingsConfigDict(
|
|
@@ -1003,6 +1103,31 @@ class APISettings(BaseSettings):
|
|
|
1003
1103
|
description="Logging level (debug, info, warning, error, critical)",
|
|
1004
1104
|
)
|
|
1005
1105
|
|
|
1106
|
+
api_key_enabled: bool = Field(
|
|
1107
|
+
default=False,
|
|
1108
|
+
description=(
|
|
1109
|
+
"Enable X-API-Key header authentication for API endpoints. "
|
|
1110
|
+
"When enabled, requests must include X-API-Key header with valid key. "
|
|
1111
|
+
"This provides simple API key auth independent of OAuth."
|
|
1112
|
+
),
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
api_key: str | None = Field(
|
|
1116
|
+
default=None,
|
|
1117
|
+
description=(
|
|
1118
|
+
"API key for X-API-Key authentication. Required when api_key_enabled=true. "
|
|
1119
|
+
"Generate with: python -c \"import secrets; print(secrets.token_urlsafe(32))\""
|
|
1120
|
+
),
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
rate_limit_enabled: bool = Field(
|
|
1124
|
+
default=True,
|
|
1125
|
+
description=(
|
|
1126
|
+
"Enable rate limiting for API endpoints. "
|
|
1127
|
+
"Set to false to disable rate limiting entirely (useful for development)."
|
|
1128
|
+
),
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1006
1131
|
|
|
1007
1132
|
class ModelsSettings(BaseSettings):
|
|
1008
1133
|
"""
|
|
@@ -1051,10 +1176,26 @@ class ModelsSettings(BaseSettings):
|
|
|
1051
1176
|
|
|
1052
1177
|
@property
|
|
1053
1178
|
def module_list(self) -> list[str]:
|
|
1054
|
-
"""
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1179
|
+
"""
|
|
1180
|
+
Get modules as a list, filtering empty strings.
|
|
1181
|
+
|
|
1182
|
+
Auto-detects ./models folder if it exists and is importable.
|
|
1183
|
+
"""
|
|
1184
|
+
modules = []
|
|
1185
|
+
if self.import_modules:
|
|
1186
|
+
modules = [m.strip() for m in self.import_modules.split(";") if m.strip()]
|
|
1187
|
+
|
|
1188
|
+
# Auto-detect ./models if it exists and is a Python package (convention over configuration)
|
|
1189
|
+
from pathlib import Path
|
|
1190
|
+
|
|
1191
|
+
models_path = Path("./models")
|
|
1192
|
+
if models_path.exists() and models_path.is_dir():
|
|
1193
|
+
# Check if it's a Python package (has __init__.py)
|
|
1194
|
+
if (models_path / "__init__.py").exists():
|
|
1195
|
+
if "models" not in modules:
|
|
1196
|
+
modules.insert(0, "models")
|
|
1197
|
+
|
|
1198
|
+
return modules
|
|
1058
1199
|
|
|
1059
1200
|
|
|
1060
1201
|
class SchemaSettings(BaseSettings):
|
|
@@ -1240,6 +1381,276 @@ class GitSettings(BaseSettings):
|
|
|
1240
1381
|
)
|
|
1241
1382
|
|
|
1242
1383
|
|
|
1384
|
+
class DBListenerSettings(BaseSettings):
|
|
1385
|
+
"""
|
|
1386
|
+
PostgreSQL LISTEN/NOTIFY database listener settings.
|
|
1387
|
+
|
|
1388
|
+
The DB Listener is a lightweight worker that subscribes to PostgreSQL
|
|
1389
|
+
NOTIFY events and dispatches them to external systems (SQS, REST, custom).
|
|
1390
|
+
|
|
1391
|
+
Architecture:
|
|
1392
|
+
- Single-replica deployment (to avoid duplicate processing)
|
|
1393
|
+
- Dedicated connection for LISTEN (not from connection pool)
|
|
1394
|
+
- Automatic reconnection with exponential backoff
|
|
1395
|
+
- Graceful shutdown on SIGTERM
|
|
1396
|
+
|
|
1397
|
+
Use Cases:
|
|
1398
|
+
- Sync data changes to external systems (Phoenix, webhooks)
|
|
1399
|
+
- Trigger async jobs without polling
|
|
1400
|
+
- Event-driven architectures with PostgreSQL as event source
|
|
1401
|
+
|
|
1402
|
+
Example PostgreSQL trigger:
|
|
1403
|
+
CREATE OR REPLACE FUNCTION notify_feedback_insert()
|
|
1404
|
+
RETURNS TRIGGER AS $$
|
|
1405
|
+
BEGIN
|
|
1406
|
+
PERFORM pg_notify('feedback_sync', json_build_object(
|
|
1407
|
+
'id', NEW.id,
|
|
1408
|
+
'table', 'feedbacks',
|
|
1409
|
+
'action', 'insert'
|
|
1410
|
+
)::text);
|
|
1411
|
+
RETURN NEW;
|
|
1412
|
+
END;
|
|
1413
|
+
$$ LANGUAGE plpgsql;
|
|
1414
|
+
|
|
1415
|
+
Environment variables:
|
|
1416
|
+
DB_LISTENER__ENABLED - Enable the listener worker (default: false)
|
|
1417
|
+
DB_LISTENER__CHANNELS - Comma-separated PostgreSQL channels to listen on
|
|
1418
|
+
DB_LISTENER__HANDLER_TYPE - Handler type: 'sqs', 'rest', or 'custom'
|
|
1419
|
+
DB_LISTENER__SQS_QUEUE_URL - SQS queue URL (for handler_type=sqs)
|
|
1420
|
+
DB_LISTENER__REST_ENDPOINT - REST endpoint URL (for handler_type=rest)
|
|
1421
|
+
DB_LISTENER__RECONNECT_DELAY - Initial reconnect delay in seconds
|
|
1422
|
+
DB_LISTENER__MAX_RECONNECT_DELAY - Maximum reconnect delay in seconds
|
|
1423
|
+
|
|
1424
|
+
References:
|
|
1425
|
+
- PostgreSQL NOTIFY: https://www.postgresql.org/docs/current/sql-notify.html
|
|
1426
|
+
- Brandur's Notifier: https://brandur.org/notifier
|
|
1427
|
+
"""
|
|
1428
|
+
|
|
1429
|
+
model_config = SettingsConfigDict(
|
|
1430
|
+
env_prefix="DB_LISTENER__",
|
|
1431
|
+
env_file=".env",
|
|
1432
|
+
env_file_encoding="utf-8",
|
|
1433
|
+
extra="ignore",
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
enabled: bool = Field(
|
|
1437
|
+
default=False,
|
|
1438
|
+
description="Enable the DB Listener worker (disabled by default)",
|
|
1439
|
+
)
|
|
1440
|
+
|
|
1441
|
+
channels: str = Field(
|
|
1442
|
+
default="",
|
|
1443
|
+
description=(
|
|
1444
|
+
"Comma-separated list of PostgreSQL channels to LISTEN on. "
|
|
1445
|
+
"Example: 'feedback_sync,entity_update,user_events'"
|
|
1446
|
+
),
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
handler_type: str = Field(
|
|
1450
|
+
default="rest",
|
|
1451
|
+
description=(
|
|
1452
|
+
"Handler type for dispatching notifications. Options: "
|
|
1453
|
+
"'sqs' (publish to SQS), 'rest' (POST to endpoint), 'custom' (Python handlers)"
|
|
1454
|
+
),
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
sqs_queue_url: str = Field(
|
|
1458
|
+
default="",
|
|
1459
|
+
description="SQS queue URL for handler_type='sqs'",
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
rest_endpoint: str = Field(
|
|
1463
|
+
default="http://localhost:8000/api/v1/internal/events",
|
|
1464
|
+
description=(
|
|
1465
|
+
"REST endpoint URL for handler_type='rest'. "
|
|
1466
|
+
"Receives POST with {channel, payload, source} JSON body."
|
|
1467
|
+
),
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
reconnect_delay: float = Field(
|
|
1471
|
+
default=1.0,
|
|
1472
|
+
description="Initial delay (seconds) between reconnection attempts",
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
max_reconnect_delay: float = Field(
|
|
1476
|
+
default=60.0,
|
|
1477
|
+
description="Maximum delay (seconds) between reconnection attempts (exponential backoff cap)",
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
@property
|
|
1481
|
+
def channel_list(self) -> list[str]:
|
|
1482
|
+
"""Get channels as a list, filtering empty strings."""
|
|
1483
|
+
if not self.channels:
|
|
1484
|
+
return []
|
|
1485
|
+
return [c.strip() for c in self.channels.split(",") if c.strip()]
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
class EmailSettings(BaseSettings):
|
|
1489
|
+
"""
|
|
1490
|
+
Email service settings for SMTP.
|
|
1491
|
+
|
|
1492
|
+
Supports passwordless login via email codes and transactional emails.
|
|
1493
|
+
Uses Gmail SMTP with App Passwords by default.
|
|
1494
|
+
|
|
1495
|
+
Generate app password at: https://myaccount.google.com/apppasswords
|
|
1496
|
+
|
|
1497
|
+
Environment variables:
|
|
1498
|
+
EMAIL__ENABLED - Enable email service (default: false)
|
|
1499
|
+
EMAIL__SMTP_HOST - SMTP server host (default: smtp.gmail.com)
|
|
1500
|
+
EMAIL__SMTP_PORT - SMTP server port (default: 587 for TLS)
|
|
1501
|
+
EMAIL__SENDER_EMAIL - Sender email address
|
|
1502
|
+
EMAIL__SENDER_NAME - Sender display name
|
|
1503
|
+
EMAIL__APP_PASSWORD - Gmail app password (from secrets)
|
|
1504
|
+
EMAIL__USE_TLS - Use TLS encryption (default: true)
|
|
1505
|
+
EMAIL__LOGIN_CODE_EXPIRY_MINUTES - Login code expiry (default: 10)
|
|
1506
|
+
|
|
1507
|
+
Branding environment variables (for email templates):
|
|
1508
|
+
EMAIL__APP_NAME - Application name in emails (default: REM)
|
|
1509
|
+
EMAIL__LOGO_URL - Logo URL for email templates (40x40 recommended)
|
|
1510
|
+
EMAIL__TAGLINE - Tagline shown in email footer
|
|
1511
|
+
EMAIL__WEBSITE_URL - Main website URL for email links
|
|
1512
|
+
EMAIL__PRIVACY_URL - Privacy policy URL for email footer
|
|
1513
|
+
EMAIL__TERMS_URL - Terms of service URL for email footer
|
|
1514
|
+
"""
|
|
1515
|
+
|
|
1516
|
+
model_config = SettingsConfigDict(
|
|
1517
|
+
env_prefix="EMAIL__",
|
|
1518
|
+
env_file=".env",
|
|
1519
|
+
env_file_encoding="utf-8",
|
|
1520
|
+
extra="ignore",
|
|
1521
|
+
)
|
|
1522
|
+
|
|
1523
|
+
enabled: bool = Field(
|
|
1524
|
+
default=False,
|
|
1525
|
+
description="Enable email service (requires app_password to be set)",
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
smtp_host: str = Field(
|
|
1529
|
+
default="smtp.gmail.com",
|
|
1530
|
+
description="SMTP server host",
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
smtp_port: int = Field(
|
|
1534
|
+
default=587,
|
|
1535
|
+
description="SMTP server port (587 for TLS, 465 for SSL)",
|
|
1536
|
+
)
|
|
1537
|
+
|
|
1538
|
+
sender_email: str = Field(
|
|
1539
|
+
default="",
|
|
1540
|
+
description="Sender email address",
|
|
1541
|
+
)
|
|
1542
|
+
|
|
1543
|
+
sender_name: str = Field(
|
|
1544
|
+
default="REM",
|
|
1545
|
+
description="Sender display name",
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
# Branding settings for email templates
|
|
1549
|
+
app_name: str = Field(
|
|
1550
|
+
default="REM",
|
|
1551
|
+
description="Application name shown in email templates",
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
logo_url: str | None = Field(
|
|
1555
|
+
default=None,
|
|
1556
|
+
description="Logo URL for email templates (40x40 recommended)",
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1559
|
+
tagline: str = Field(
|
|
1560
|
+
default="Your AI-powered platform",
|
|
1561
|
+
description="Tagline shown in email footer",
|
|
1562
|
+
)
|
|
1563
|
+
|
|
1564
|
+
website_url: str = Field(
|
|
1565
|
+
default="https://rem.ai",
|
|
1566
|
+
description="Main website URL for email links",
|
|
1567
|
+
)
|
|
1568
|
+
|
|
1569
|
+
privacy_url: str = Field(
|
|
1570
|
+
default="https://rem.ai/privacy",
|
|
1571
|
+
description="Privacy policy URL for email footer",
|
|
1572
|
+
)
|
|
1573
|
+
|
|
1574
|
+
terms_url: str = Field(
|
|
1575
|
+
default="https://rem.ai/terms",
|
|
1576
|
+
description="Terms of service URL for email footer",
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
app_password: str | None = Field(
|
|
1580
|
+
default=None,
|
|
1581
|
+
description="Gmail app password for SMTP authentication",
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
use_tls: bool = Field(
|
|
1585
|
+
default=True,
|
|
1586
|
+
description="Use TLS encryption for SMTP",
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
login_code_expiry_minutes: int = Field(
|
|
1590
|
+
default=10,
|
|
1591
|
+
description="Login code expiry in minutes",
|
|
1592
|
+
)
|
|
1593
|
+
|
|
1594
|
+
trusted_email_domains: str = Field(
|
|
1595
|
+
default="",
|
|
1596
|
+
description=(
|
|
1597
|
+
"Comma-separated list of trusted email domains for new user registration. "
|
|
1598
|
+
"Existing users can always login regardless of domain. "
|
|
1599
|
+
"New users must have an email from a trusted domain. "
|
|
1600
|
+
"Empty string means all domains are allowed. "
|
|
1601
|
+
"Example: 'siggymd.ai,example.com'"
|
|
1602
|
+
),
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
@property
|
|
1606
|
+
def trusted_domain_list(self) -> list[str]:
|
|
1607
|
+
"""Get trusted domains as a list, filtering empty strings."""
|
|
1608
|
+
if not self.trusted_email_domains:
|
|
1609
|
+
return []
|
|
1610
|
+
return [d.strip().lower() for d in self.trusted_email_domains.split(",") if d.strip()]
|
|
1611
|
+
|
|
1612
|
+
def is_domain_trusted(self, email: str) -> bool:
|
|
1613
|
+
"""Check if an email's domain is in the trusted list.
|
|
1614
|
+
|
|
1615
|
+
Args:
|
|
1616
|
+
email: Email address to check
|
|
1617
|
+
|
|
1618
|
+
Returns:
|
|
1619
|
+
True if domain is trusted (or if no trusted domains configured)
|
|
1620
|
+
"""
|
|
1621
|
+
domains = self.trusted_domain_list
|
|
1622
|
+
if not domains:
|
|
1623
|
+
# No restrictions configured
|
|
1624
|
+
return True
|
|
1625
|
+
|
|
1626
|
+
email_domain = email.lower().split("@")[-1].strip()
|
|
1627
|
+
return email_domain in domains
|
|
1628
|
+
|
|
1629
|
+
@property
|
|
1630
|
+
def is_configured(self) -> bool:
|
|
1631
|
+
"""Check if email service is properly configured."""
|
|
1632
|
+
return bool(self.sender_email and self.app_password)
|
|
1633
|
+
|
|
1634
|
+
@property
|
|
1635
|
+
def template_kwargs(self) -> dict:
|
|
1636
|
+
"""
|
|
1637
|
+
Get branding kwargs for email templates.
|
|
1638
|
+
|
|
1639
|
+
Returns a dict that can be passed to template functions:
|
|
1640
|
+
login_code_template(..., **settings.email.template_kwargs)
|
|
1641
|
+
"""
|
|
1642
|
+
kwargs = {
|
|
1643
|
+
"app_name": self.app_name,
|
|
1644
|
+
"tagline": self.tagline,
|
|
1645
|
+
"website_url": self.website_url,
|
|
1646
|
+
"privacy_url": self.privacy_url,
|
|
1647
|
+
"terms_url": self.terms_url,
|
|
1648
|
+
}
|
|
1649
|
+
if self.logo_url:
|
|
1650
|
+
kwargs["logo_url"] = self.logo_url
|
|
1651
|
+
return kwargs
|
|
1652
|
+
|
|
1653
|
+
|
|
1243
1654
|
class TestSettings(BaseSettings):
|
|
1244
1655
|
"""
|
|
1245
1656
|
Test environment settings.
|
|
@@ -1334,11 +1745,6 @@ class Settings(BaseSettings):
|
|
|
1334
1745
|
description="Root path for reverse proxy (e.g., /rem for ALB routing)",
|
|
1335
1746
|
)
|
|
1336
1747
|
|
|
1337
|
-
sql_dir: str = Field(
|
|
1338
|
-
default="src/rem/sql",
|
|
1339
|
-
description="Directory for SQL files and migrations",
|
|
1340
|
-
)
|
|
1341
|
-
|
|
1342
1748
|
# Nested settings groups
|
|
1343
1749
|
api: APISettings = Field(default_factory=APISettings)
|
|
1344
1750
|
chat: ChatSettings = Field(default_factory=ChatSettings)
|
|
@@ -1352,18 +1758,31 @@ class Settings(BaseSettings):
|
|
|
1352
1758
|
migration: MigrationSettings = Field(default_factory=MigrationSettings)
|
|
1353
1759
|
storage: StorageSettings = Field(default_factory=StorageSettings)
|
|
1354
1760
|
s3: S3Settings = Field(default_factory=S3Settings)
|
|
1761
|
+
data_lake: DataLakeSettings = Field(default_factory=DataLakeSettings)
|
|
1355
1762
|
git: GitSettings = Field(default_factory=GitSettings)
|
|
1356
1763
|
sqs: SQSSettings = Field(default_factory=SQSSettings)
|
|
1764
|
+
db_listener: DBListenerSettings = Field(default_factory=DBListenerSettings)
|
|
1357
1765
|
chunking: ChunkingSettings = Field(default_factory=ChunkingSettings)
|
|
1358
1766
|
content: ContentSettings = Field(default_factory=ContentSettings)
|
|
1359
1767
|
schema_search: SchemaSettings = Field(default_factory=SchemaSettings)
|
|
1768
|
+
email: EmailSettings = Field(default_factory=EmailSettings)
|
|
1360
1769
|
test: TestSettings = Field(default_factory=TestSettings)
|
|
1361
1770
|
|
|
1362
1771
|
|
|
1772
|
+
# Auto-load .env file from current directory if it exists
|
|
1773
|
+
# This happens BEFORE config file loading, so .env takes precedence
|
|
1774
|
+
from pathlib import Path
|
|
1775
|
+
from dotenv import load_dotenv
|
|
1776
|
+
|
|
1777
|
+
_dotenv_path = Path(".env")
|
|
1778
|
+
if _dotenv_path.exists():
|
|
1779
|
+
load_dotenv(_dotenv_path, override=False) # Don't override existing env vars
|
|
1780
|
+
logger.debug(f"Loaded environment from {_dotenv_path.resolve()}")
|
|
1781
|
+
|
|
1363
1782
|
# Load configuration from ~/.rem/config.yaml before initializing settings
|
|
1364
1783
|
# This allows user configuration to be merged with environment variables
|
|
1365
|
-
# Set
|
|
1366
|
-
if not os.getenv("
|
|
1784
|
+
# Set REM_SKIP_CONFIG=1 to disable (useful for development with .env)
|
|
1785
|
+
if not os.getenv("REM_SKIP_CONFIG", "").lower() in ("true", "1", "yes"):
|
|
1367
1786
|
try:
|
|
1368
1787
|
from rem.config import load_config, merge_config_to_env
|
|
1369
1788
|
|