remdb 0.3.146__py3-none-any.whl → 0.3.163__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 +310 -0
- rem/agentic/context.py +81 -3
- rem/agentic/context_builder.py +18 -3
- rem/api/deps.py +3 -5
- rem/api/main.py +22 -3
- rem/api/mcp_router/server.py +2 -0
- rem/api/mcp_router/tools.py +90 -0
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/auth.py +346 -5
- rem/api/routers/chat/completions.py +4 -2
- rem/api/routers/chat/streaming.py +77 -22
- rem/api/routers/messages.py +24 -15
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +42 -5
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +134 -0
- 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 +511 -0
- rem/services/email/templates.py +360 -0
- rem/services/postgres/README.md +38 -0
- rem/services/postgres/diff_service.py +19 -3
- rem/services/postgres/pydantic_to_sqlalchemy.py +37 -2
- rem/services/postgres/repository.py +5 -4
- rem/services/session/compression.py +113 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +29 -0
- rem/settings.py +175 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/METADATA +1 -1
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/RECORD +40 -31
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/WHEEL +0 -0
- {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/entry_points.txt +0 -0
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
"""Session message compression and rehydration for efficient context loading.
|
|
2
2
|
|
|
3
|
-
This module implements message compression to keep conversation history
|
|
4
|
-
context windows while preserving full content via REM LOOKUP.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
This module implements message storage and compression to keep conversation history
|
|
4
|
+
within context windows while preserving full content via REM LOOKUP.
|
|
5
|
+
|
|
6
|
+
Message Types and Storage Strategy
|
|
7
|
+
===================================
|
|
8
|
+
|
|
9
|
+
All messages are stored UNCOMPRESSED in the database for full audit/analysis.
|
|
10
|
+
Compression happens only on RELOAD when reconstructing context for the LLM.
|
|
11
|
+
|
|
12
|
+
Message Types:
|
|
13
|
+
- `user`: User messages - stored and reloaded as-is
|
|
14
|
+
- `tool`: Tool call messages (e.g., register_metadata) - stored and reloaded as-is
|
|
15
|
+
NEVER compressed - contains important structured metadata
|
|
16
|
+
- `assistant`: Assistant text responses - stored uncompressed, but MAY BE
|
|
17
|
+
compressed on reload if long (>400 chars) with REM LOOKUP hints
|
|
18
|
+
|
|
19
|
+
Example Session Flow:
|
|
20
|
+
```
|
|
21
|
+
Turn 1 (stored uncompressed):
|
|
22
|
+
- user: "I have a headache"
|
|
23
|
+
- tool: register_metadata({confidence: 0.3, collected_fields: {...}})
|
|
24
|
+
- assistant: "I'm sorry to hear that. How long has this been going on?"
|
|
25
|
+
|
|
26
|
+
Turn 2 (stored uncompressed):
|
|
27
|
+
- user: "About 3 days, really bad"
|
|
28
|
+
- tool: register_metadata({confidence: 0.6, collected_fields: {...}})
|
|
29
|
+
- assistant: "Got it - 3 days. On a scale of 1-10..."
|
|
30
|
+
|
|
31
|
+
On reload (for LLM context):
|
|
32
|
+
- user messages: returned as-is
|
|
33
|
+
- tool messages: returned as-is (never compressed)
|
|
34
|
+
- assistant messages: compressed if long, with REM LOOKUP hint for full retrieval
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
REM LOOKUP Pattern:
|
|
38
|
+
- Long assistant messages get truncated with hint: "... [REM LOOKUP session-{id}-msg-{idx}] ..."
|
|
39
|
+
- Agent can retrieve full content on-demand using the LOOKUP key
|
|
40
|
+
- Keeps context window efficient while preserving data integrity
|
|
41
|
+
|
|
42
|
+
Key Design Decisions:
|
|
43
|
+
1. Store everything uncompressed - full audit trail in database
|
|
44
|
+
2. Compress only on reload - optimize for LLM context window
|
|
45
|
+
3. Never compress tool messages - structured metadata must stay intact
|
|
46
|
+
4. REM LOOKUP enables on-demand retrieval of full assistant responses
|
|
11
47
|
"""
|
|
12
48
|
|
|
13
49
|
from typing import Any
|
|
@@ -285,8 +321,23 @@ class SessionMessageStore:
|
|
|
285
321
|
msg_copy["_entity_key"] = entity_key
|
|
286
322
|
compressed_messages.append(msg_copy)
|
|
287
323
|
else:
|
|
288
|
-
# Short assistant messages, user messages, and system messages stored as-is
|
|
324
|
+
# Short assistant messages, user messages, tool messages, and system messages stored as-is
|
|
289
325
|
# Store ALL messages in database for full audit trail
|
|
326
|
+
# Build metadata dict with standard fields
|
|
327
|
+
msg_metadata = {
|
|
328
|
+
"message_index": idx,
|
|
329
|
+
"timestamp": message.get("timestamp"),
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# For tool messages, include tool call details in metadata
|
|
333
|
+
if message.get("role") == "tool":
|
|
334
|
+
if message.get("tool_call_id"):
|
|
335
|
+
msg_metadata["tool_call_id"] = message.get("tool_call_id")
|
|
336
|
+
if message.get("tool_name"):
|
|
337
|
+
msg_metadata["tool_name"] = message.get("tool_name")
|
|
338
|
+
if message.get("tool_arguments"):
|
|
339
|
+
msg_metadata["tool_arguments"] = message.get("tool_arguments")
|
|
340
|
+
|
|
290
341
|
msg = Message(
|
|
291
342
|
id=message.get("id"), # Use pre-generated ID if provided
|
|
292
343
|
content=content,
|
|
@@ -296,10 +347,7 @@ class SessionMessageStore:
|
|
|
296
347
|
user_id=user_id or self.user_id,
|
|
297
348
|
trace_id=message.get("trace_id"),
|
|
298
349
|
span_id=message.get("span_id"),
|
|
299
|
-
metadata=
|
|
300
|
-
"message_index": idx,
|
|
301
|
-
"timestamp": message.get("timestamp"),
|
|
302
|
-
},
|
|
350
|
+
metadata=msg_metadata,
|
|
303
351
|
)
|
|
304
352
|
await self.repo.upsert(msg)
|
|
305
353
|
compressed_messages.append(message.copy())
|
|
@@ -307,18 +355,24 @@ class SessionMessageStore:
|
|
|
307
355
|
return compressed_messages
|
|
308
356
|
|
|
309
357
|
async def load_session_messages(
|
|
310
|
-
self, session_id: str, user_id: str | None = None,
|
|
358
|
+
self, session_id: str, user_id: str | None = None, compress_on_load: bool = True
|
|
311
359
|
) -> list[dict[str, Any]]:
|
|
312
360
|
"""
|
|
313
|
-
Load session messages from database.
|
|
361
|
+
Load session messages from database, optionally compressing long assistant messages.
|
|
362
|
+
|
|
363
|
+
Compression on Load:
|
|
364
|
+
- Tool messages (role: "tool") are NEVER compressed - they contain structured metadata
|
|
365
|
+
- User messages are returned as-is
|
|
366
|
+
- Assistant messages MAY be compressed if long (>400 chars) with REM LOOKUP hints
|
|
314
367
|
|
|
315
368
|
Args:
|
|
316
369
|
session_id: Session identifier
|
|
317
370
|
user_id: Optional user identifier for filtering
|
|
318
|
-
|
|
371
|
+
compress_on_load: Whether to compress long assistant messages (default: True)
|
|
319
372
|
|
|
320
373
|
Returns:
|
|
321
|
-
List of session messages in chronological order
|
|
374
|
+
List of session messages in chronological order, with long assistant
|
|
375
|
+
messages optionally compressed with REM LOOKUP hints
|
|
322
376
|
"""
|
|
323
377
|
if not settings.postgres.enabled:
|
|
324
378
|
logger.debug("Postgres disabled, returning empty message list")
|
|
@@ -335,49 +389,58 @@ class SessionMessageStore:
|
|
|
335
389
|
|
|
336
390
|
# Convert Message entities to dict format
|
|
337
391
|
message_dicts = []
|
|
338
|
-
for msg in messages:
|
|
392
|
+
for idx, msg in enumerate(messages):
|
|
393
|
+
role = msg.message_type or "assistant"
|
|
339
394
|
msg_dict = {
|
|
340
|
-
"role":
|
|
395
|
+
"role": role,
|
|
341
396
|
"content": msg.content,
|
|
342
397
|
"timestamp": msg.created_at.isoformat() if msg.created_at else None,
|
|
343
398
|
}
|
|
344
399
|
|
|
345
|
-
#
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
400
|
+
# For tool messages, reconstruct tool call metadata
|
|
401
|
+
if role == "tool" and msg.metadata:
|
|
402
|
+
if msg.metadata.get("tool_call_id"):
|
|
403
|
+
msg_dict["tool_call_id"] = msg.metadata["tool_call_id"]
|
|
404
|
+
if msg.metadata.get("tool_name"):
|
|
405
|
+
msg_dict["tool_name"] = msg.metadata["tool_name"]
|
|
406
|
+
if msg.metadata.get("tool_arguments"):
|
|
407
|
+
msg_dict["tool_arguments"] = msg.metadata["tool_arguments"]
|
|
408
|
+
|
|
409
|
+
# Compress long ASSISTANT messages on load (never tool messages)
|
|
410
|
+
if (
|
|
411
|
+
compress_on_load
|
|
412
|
+
and role == "assistant"
|
|
413
|
+
and len(msg.content) > self.compressor.min_length_for_compression
|
|
414
|
+
):
|
|
415
|
+
# Generate entity key for REM LOOKUP
|
|
416
|
+
entity_key = truncate_key(f"session-{session_id}-msg-{idx}")
|
|
417
|
+
msg_dict = self.compressor.compress_message(msg_dict, entity_key)
|
|
352
418
|
|
|
353
419
|
message_dicts.append(msg_dict)
|
|
354
420
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if self.compressor.is_compressed(message):
|
|
360
|
-
entity_key = self.compressor.get_entity_key(message)
|
|
361
|
-
if entity_key:
|
|
362
|
-
full_content = await self.retrieve_message(entity_key)
|
|
363
|
-
if full_content:
|
|
364
|
-
decompressed_messages.append(
|
|
365
|
-
self.compressor.decompress_message(
|
|
366
|
-
message, full_content
|
|
367
|
-
)
|
|
368
|
-
)
|
|
369
|
-
else:
|
|
370
|
-
# Fallback to compressed version if retrieval fails
|
|
371
|
-
decompressed_messages.append(message)
|
|
372
|
-
else:
|
|
373
|
-
decompressed_messages.append(message)
|
|
374
|
-
else:
|
|
375
|
-
decompressed_messages.append(message)
|
|
376
|
-
|
|
377
|
-
return decompressed_messages
|
|
378
|
-
|
|
421
|
+
logger.debug(
|
|
422
|
+
f"Loaded {len(message_dicts)} messages for session {session_id} "
|
|
423
|
+
f"(compress_on_load={compress_on_load})"
|
|
424
|
+
)
|
|
379
425
|
return message_dicts
|
|
380
426
|
|
|
381
427
|
except Exception as e:
|
|
382
428
|
logger.error(f"Failed to load session messages: {e}")
|
|
383
429
|
return []
|
|
430
|
+
|
|
431
|
+
async def retrieve_full_message(self, session_id: str, message_index: int) -> str | None:
|
|
432
|
+
"""
|
|
433
|
+
Retrieve full message content by session and message index (for REM LOOKUP).
|
|
434
|
+
|
|
435
|
+
This is used when an agent needs to recover full content from a compressed
|
|
436
|
+
message that has a REM LOOKUP hint.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
session_id: Session identifier
|
|
440
|
+
message_index: Index of message in session (from REM LOOKUP key)
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Full message content or None if not found
|
|
444
|
+
"""
|
|
445
|
+
entity_key = truncate_key(f"session-{session_id}-msg-{message_index}")
|
|
446
|
+
return await self.retrieve_message(entity_key)
|
rem/services/session/reload.py
CHANGED
|
@@ -6,8 +6,14 @@ allowing conversations to be resumed across multiple API calls.
|
|
|
6
6
|
Design Pattern:
|
|
7
7
|
- Session identified by session_id from X-Session-Id header
|
|
8
8
|
- All messages for session loaded in chronological order
|
|
9
|
-
-
|
|
9
|
+
- Long assistant messages compressed on load with REM LOOKUP hints
|
|
10
|
+
- Tool messages (register_metadata, etc.) are NEVER compressed
|
|
10
11
|
- Gracefully handles missing database (returns empty history)
|
|
12
|
+
|
|
13
|
+
Message Types on Reload:
|
|
14
|
+
- user: Returned as-is
|
|
15
|
+
- tool: Returned as-is with metadata (tool_call_id, tool_name, tool_arguments)
|
|
16
|
+
- assistant: Compressed on load if long (>400 chars), with REM LOOKUP for recovery
|
|
11
17
|
"""
|
|
12
18
|
|
|
13
19
|
from loguru import logger
|
|
@@ -19,7 +25,7 @@ from rem.settings import settings
|
|
|
19
25
|
async def reload_session(
|
|
20
26
|
session_id: str,
|
|
21
27
|
user_id: str,
|
|
22
|
-
|
|
28
|
+
compress_on_load: bool = True,
|
|
23
29
|
) -> list[dict]:
|
|
24
30
|
"""
|
|
25
31
|
Reload all messages for a session from the database.
|
|
@@ -27,7 +33,8 @@ async def reload_session(
|
|
|
27
33
|
Args:
|
|
28
34
|
session_id: Session/conversation identifier
|
|
29
35
|
user_id: User identifier for data isolation
|
|
30
|
-
|
|
36
|
+
compress_on_load: Whether to compress long assistant messages (default: True)
|
|
37
|
+
Tool messages are NEVER compressed.
|
|
31
38
|
|
|
32
39
|
Returns:
|
|
33
40
|
List of message dicts in chronological order (oldest first)
|
|
@@ -41,7 +48,7 @@ async def reload_session(
|
|
|
41
48
|
history = await reload_session(
|
|
42
49
|
session_id=context.session_id,
|
|
43
50
|
user_id=context.user_id,
|
|
44
|
-
|
|
51
|
+
compress_on_load=True, # Compress long assistant messages
|
|
45
52
|
)
|
|
46
53
|
|
|
47
54
|
# Combine with new user message
|
|
@@ -60,14 +67,14 @@ async def reload_session(
|
|
|
60
67
|
# Create message store for this session
|
|
61
68
|
store = SessionMessageStore(user_id=user_id)
|
|
62
69
|
|
|
63
|
-
# Load messages (
|
|
70
|
+
# Load messages (assistant messages compressed on load, tool messages never compressed)
|
|
64
71
|
messages = await store.load_session_messages(
|
|
65
|
-
session_id=session_id, user_id=user_id,
|
|
72
|
+
session_id=session_id, user_id=user_id, compress_on_load=compress_on_load
|
|
66
73
|
)
|
|
67
74
|
|
|
68
75
|
logger.debug(
|
|
69
76
|
f"Reloaded {len(messages)} messages for session {session_id} "
|
|
70
|
-
f"(
|
|
77
|
+
f"(compress_on_load={compress_on_load})"
|
|
71
78
|
)
|
|
72
79
|
|
|
73
80
|
return messages
|
rem/services/user_service.py
CHANGED
|
@@ -73,6 +73,35 @@ class UserService:
|
|
|
73
73
|
logger.info(f"Created new user: {email}")
|
|
74
74
|
return user
|
|
75
75
|
|
|
76
|
+
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
77
|
+
"""
|
|
78
|
+
Get a user by their UUID.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
user_id: The user's UUID
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
User if found, None otherwise
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
return await self.repo.get_by_id(user_id)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning(f"Could not find user by id {user_id}: {e}")
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
async def get_user_by_email(self, email: str) -> Optional[User]:
|
|
93
|
+
"""
|
|
94
|
+
Get a user by their email address.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
email: The user's email
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
User if found, None otherwise
|
|
101
|
+
"""
|
|
102
|
+
users = await self.repo.find(filters={"email": email}, limit=1)
|
|
103
|
+
return users[0] if users else None
|
|
104
|
+
|
|
76
105
|
async def link_anonymous_session(self, user: User, anon_id: str) -> None:
|
|
77
106
|
"""
|
|
78
107
|
Link an anonymous session ID to a user account.
|
rem/settings.py
CHANGED
|
@@ -1114,6 +1114,14 @@ class APISettings(BaseSettings):
|
|
|
1114
1114
|
),
|
|
1115
1115
|
)
|
|
1116
1116
|
|
|
1117
|
+
rate_limit_enabled: bool = Field(
|
|
1118
|
+
default=True,
|
|
1119
|
+
description=(
|
|
1120
|
+
"Enable rate limiting for API endpoints. "
|
|
1121
|
+
"Set to false to disable rate limiting entirely (useful for development)."
|
|
1122
|
+
),
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1117
1125
|
|
|
1118
1126
|
class ModelsSettings(BaseSettings):
|
|
1119
1127
|
"""
|
|
@@ -1471,6 +1479,172 @@ class DBListenerSettings(BaseSettings):
|
|
|
1471
1479
|
return [c.strip() for c in self.channels.split(",") if c.strip()]
|
|
1472
1480
|
|
|
1473
1481
|
|
|
1482
|
+
class EmailSettings(BaseSettings):
|
|
1483
|
+
"""
|
|
1484
|
+
Email service settings for SMTP.
|
|
1485
|
+
|
|
1486
|
+
Supports passwordless login via email codes and transactional emails.
|
|
1487
|
+
Uses Gmail SMTP with App Passwords by default.
|
|
1488
|
+
|
|
1489
|
+
Generate app password at: https://myaccount.google.com/apppasswords
|
|
1490
|
+
|
|
1491
|
+
Environment variables:
|
|
1492
|
+
EMAIL__ENABLED - Enable email service (default: false)
|
|
1493
|
+
EMAIL__SMTP_HOST - SMTP server host (default: smtp.gmail.com)
|
|
1494
|
+
EMAIL__SMTP_PORT - SMTP server port (default: 587 for TLS)
|
|
1495
|
+
EMAIL__SENDER_EMAIL - Sender email address
|
|
1496
|
+
EMAIL__SENDER_NAME - Sender display name
|
|
1497
|
+
EMAIL__APP_PASSWORD - Gmail app password (from secrets)
|
|
1498
|
+
EMAIL__USE_TLS - Use TLS encryption (default: true)
|
|
1499
|
+
EMAIL__LOGIN_CODE_EXPIRY_MINUTES - Login code expiry (default: 10)
|
|
1500
|
+
|
|
1501
|
+
Branding environment variables (for email templates):
|
|
1502
|
+
EMAIL__APP_NAME - Application name in emails (default: REM)
|
|
1503
|
+
EMAIL__LOGO_URL - Logo URL for email templates (40x40 recommended)
|
|
1504
|
+
EMAIL__TAGLINE - Tagline shown in email footer
|
|
1505
|
+
EMAIL__WEBSITE_URL - Main website URL for email links
|
|
1506
|
+
EMAIL__PRIVACY_URL - Privacy policy URL for email footer
|
|
1507
|
+
EMAIL__TERMS_URL - Terms of service URL for email footer
|
|
1508
|
+
"""
|
|
1509
|
+
|
|
1510
|
+
model_config = SettingsConfigDict(
|
|
1511
|
+
env_prefix="EMAIL__",
|
|
1512
|
+
env_file=".env",
|
|
1513
|
+
env_file_encoding="utf-8",
|
|
1514
|
+
extra="ignore",
|
|
1515
|
+
)
|
|
1516
|
+
|
|
1517
|
+
enabled: bool = Field(
|
|
1518
|
+
default=False,
|
|
1519
|
+
description="Enable email service (requires app_password to be set)",
|
|
1520
|
+
)
|
|
1521
|
+
|
|
1522
|
+
smtp_host: str = Field(
|
|
1523
|
+
default="smtp.gmail.com",
|
|
1524
|
+
description="SMTP server host",
|
|
1525
|
+
)
|
|
1526
|
+
|
|
1527
|
+
smtp_port: int = Field(
|
|
1528
|
+
default=587,
|
|
1529
|
+
description="SMTP server port (587 for TLS, 465 for SSL)",
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
sender_email: str = Field(
|
|
1533
|
+
default="",
|
|
1534
|
+
description="Sender email address",
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1537
|
+
sender_name: str = Field(
|
|
1538
|
+
default="REM",
|
|
1539
|
+
description="Sender display name",
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
# Branding settings for email templates
|
|
1543
|
+
app_name: str = Field(
|
|
1544
|
+
default="REM",
|
|
1545
|
+
description="Application name shown in email templates",
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
logo_url: str | None = Field(
|
|
1549
|
+
default=None,
|
|
1550
|
+
description="Logo URL for email templates (40x40 recommended)",
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
tagline: str = Field(
|
|
1554
|
+
default="Your AI-powered platform",
|
|
1555
|
+
description="Tagline shown in email footer",
|
|
1556
|
+
)
|
|
1557
|
+
|
|
1558
|
+
website_url: str = Field(
|
|
1559
|
+
default="https://rem.ai",
|
|
1560
|
+
description="Main website URL for email links",
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
privacy_url: str = Field(
|
|
1564
|
+
default="https://rem.ai/privacy",
|
|
1565
|
+
description="Privacy policy URL for email footer",
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
terms_url: str = Field(
|
|
1569
|
+
default="https://rem.ai/terms",
|
|
1570
|
+
description="Terms of service URL for email footer",
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
app_password: str | None = Field(
|
|
1574
|
+
default=None,
|
|
1575
|
+
description="Gmail app password for SMTP authentication",
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
use_tls: bool = Field(
|
|
1579
|
+
default=True,
|
|
1580
|
+
description="Use TLS encryption for SMTP",
|
|
1581
|
+
)
|
|
1582
|
+
|
|
1583
|
+
login_code_expiry_minutes: int = Field(
|
|
1584
|
+
default=10,
|
|
1585
|
+
description="Login code expiry in minutes",
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
trusted_email_domains: str = Field(
|
|
1589
|
+
default="",
|
|
1590
|
+
description=(
|
|
1591
|
+
"Comma-separated list of trusted email domains for new user registration. "
|
|
1592
|
+
"Existing users can always login regardless of domain. "
|
|
1593
|
+
"New users must have an email from a trusted domain. "
|
|
1594
|
+
"Empty string means all domains are allowed. "
|
|
1595
|
+
"Example: 'siggymd.ai,example.com'"
|
|
1596
|
+
),
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1599
|
+
@property
|
|
1600
|
+
def trusted_domain_list(self) -> list[str]:
|
|
1601
|
+
"""Get trusted domains as a list, filtering empty strings."""
|
|
1602
|
+
if not self.trusted_email_domains:
|
|
1603
|
+
return []
|
|
1604
|
+
return [d.strip().lower() for d in self.trusted_email_domains.split(",") if d.strip()]
|
|
1605
|
+
|
|
1606
|
+
def is_domain_trusted(self, email: str) -> bool:
|
|
1607
|
+
"""Check if an email's domain is in the trusted list.
|
|
1608
|
+
|
|
1609
|
+
Args:
|
|
1610
|
+
email: Email address to check
|
|
1611
|
+
|
|
1612
|
+
Returns:
|
|
1613
|
+
True if domain is trusted (or if no trusted domains configured)
|
|
1614
|
+
"""
|
|
1615
|
+
domains = self.trusted_domain_list
|
|
1616
|
+
if not domains:
|
|
1617
|
+
# No restrictions configured
|
|
1618
|
+
return True
|
|
1619
|
+
|
|
1620
|
+
email_domain = email.lower().split("@")[-1].strip()
|
|
1621
|
+
return email_domain in domains
|
|
1622
|
+
|
|
1623
|
+
@property
|
|
1624
|
+
def is_configured(self) -> bool:
|
|
1625
|
+
"""Check if email service is properly configured."""
|
|
1626
|
+
return bool(self.sender_email and self.app_password)
|
|
1627
|
+
|
|
1628
|
+
@property
|
|
1629
|
+
def template_kwargs(self) -> dict:
|
|
1630
|
+
"""
|
|
1631
|
+
Get branding kwargs for email templates.
|
|
1632
|
+
|
|
1633
|
+
Returns a dict that can be passed to template functions:
|
|
1634
|
+
login_code_template(..., **settings.email.template_kwargs)
|
|
1635
|
+
"""
|
|
1636
|
+
kwargs = {
|
|
1637
|
+
"app_name": self.app_name,
|
|
1638
|
+
"tagline": self.tagline,
|
|
1639
|
+
"website_url": self.website_url,
|
|
1640
|
+
"privacy_url": self.privacy_url,
|
|
1641
|
+
"terms_url": self.terms_url,
|
|
1642
|
+
}
|
|
1643
|
+
if self.logo_url:
|
|
1644
|
+
kwargs["logo_url"] = self.logo_url
|
|
1645
|
+
return kwargs
|
|
1646
|
+
|
|
1647
|
+
|
|
1474
1648
|
class TestSettings(BaseSettings):
|
|
1475
1649
|
"""
|
|
1476
1650
|
Test environment settings.
|
|
@@ -1585,6 +1759,7 @@ class Settings(BaseSettings):
|
|
|
1585
1759
|
chunking: ChunkingSettings = Field(default_factory=ChunkingSettings)
|
|
1586
1760
|
content: ContentSettings = Field(default_factory=ContentSettings)
|
|
1587
1761
|
schema_search: SchemaSettings = Field(default_factory=SchemaSettings)
|
|
1762
|
+
email: EmailSettings = Field(default_factory=EmailSettings)
|
|
1588
1763
|
test: TestSettings = Field(default_factory=TestSettings)
|
|
1589
1764
|
|
|
1590
1765
|
|
|
@@ -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);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remdb
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.163
|
|
4
4
|
Summary: Resources Entities Moments - Bio-inspired memory system for agentic AI workloads
|
|
5
5
|
Project-URL: Homepage, https://github.com/Percolation-Labs/reminiscent
|
|
6
6
|
Project-URL: Documentation, https://github.com/Percolation-Labs/reminiscent/blob/main/README.md
|