remdb 0.3.146__py3-none-any.whl → 0.3.181__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/context.py +81 -3
- rem/agentic/context_builder.py +36 -9
- rem/agentic/mcp/tool_wrapper.py +43 -14
- rem/agentic/providers/pydantic_ai.py +76 -34
- rem/agentic/schema.py +4 -3
- rem/agentic/tools/rem_tools.py +11 -0
- rem/api/deps.py +3 -5
- rem/api/main.py +22 -3
- rem/api/mcp_router/resources.py +75 -14
- rem/api/mcp_router/server.py +28 -23
- rem/api/mcp_router/tools.py +177 -2
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/auth.py +352 -6
- rem/api/routers/chat/completions.py +5 -3
- rem/api/routers/chat/streaming.py +95 -22
- rem/api/routers/messages.py +24 -15
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +70 -30
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/ask.py +1 -1
- rem/cli/commands/db.py +118 -54
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/ontology.py +93 -101
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -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 +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/worker.py +26 -12
- 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/register_type.py +1 -1
- rem/services/postgres/repository.py +37 -25
- rem/services/postgres/schema_generator.py +5 -5
- rem/services/postgres/sql_builder.py +6 -5
- rem/services/session/compression.py +113 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +41 -9
- rem/settings.py +182 -1
- rem/sql/background_indexes.sql +5 -0
- rem/sql/migrations/001_install.sql +33 -4
- rem/sql/migrations/002_install_models.sql +204 -186
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/model_helpers.py +101 -0
- rem/utils/schema_loader.py +45 -7
- {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/METADATA +1 -1
- {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/RECORD +57 -48
- {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/WHEEL +0 -0
- {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/entry_points.txt +0 -0
rem/settings.py
CHANGED
|
@@ -77,6 +77,7 @@ class LLMSettings(BaseSettings):
|
|
|
77
77
|
LLM__ANTHROPIC_API_KEY or ANTHROPIC_API_KEY - Anthropic API key
|
|
78
78
|
LLM__EMBEDDING_PROVIDER or EMBEDDING_PROVIDER - Default embedding provider (openai)
|
|
79
79
|
LLM__EMBEDDING_MODEL or EMBEDDING_MODEL - Default embedding model name
|
|
80
|
+
LLM__DEFAULT_STRUCTURED_OUTPUT - Default structured output mode (False = streaming text)
|
|
80
81
|
"""
|
|
81
82
|
|
|
82
83
|
model_config = SettingsConfigDict(
|
|
@@ -138,6 +139,11 @@ class LLMSettings(BaseSettings):
|
|
|
138
139
|
description="Default embedding model (provider-specific model name)",
|
|
139
140
|
)
|
|
140
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
|
+
|
|
141
147
|
@field_validator("openai_api_key", mode="before")
|
|
142
148
|
@classmethod
|
|
143
149
|
def validate_openai_api_key(cls, v):
|
|
@@ -1028,7 +1034,7 @@ class ChatSettings(BaseSettings):
|
|
|
1028
1034
|
- Prevents context window bloat while maintaining conversation continuity
|
|
1029
1035
|
|
|
1030
1036
|
User Context (on-demand by default):
|
|
1031
|
-
- Agent system prompt includes: "User
|
|
1037
|
+
- Agent system prompt includes: "User: {email}. To load user profile: Use REM LOOKUP \"{email}\""
|
|
1032
1038
|
- Agent decides whether to load profile based on query
|
|
1033
1039
|
- More efficient for queries that don't need personalization
|
|
1034
1040
|
|
|
@@ -1114,6 +1120,14 @@ class APISettings(BaseSettings):
|
|
|
1114
1120
|
),
|
|
1115
1121
|
)
|
|
1116
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
|
+
|
|
1117
1131
|
|
|
1118
1132
|
class ModelsSettings(BaseSettings):
|
|
1119
1133
|
"""
|
|
@@ -1471,6 +1485,172 @@ class DBListenerSettings(BaseSettings):
|
|
|
1471
1485
|
return [c.strip() for c in self.channels.split(",") if c.strip()]
|
|
1472
1486
|
|
|
1473
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
|
+
|
|
1474
1654
|
class TestSettings(BaseSettings):
|
|
1475
1655
|
"""
|
|
1476
1656
|
Test environment settings.
|
|
@@ -1585,6 +1765,7 @@ class Settings(BaseSettings):
|
|
|
1585
1765
|
chunking: ChunkingSettings = Field(default_factory=ChunkingSettings)
|
|
1586
1766
|
content: ContentSettings = Field(default_factory=ContentSettings)
|
|
1587
1767
|
schema_search: SchemaSettings = Field(default_factory=SchemaSettings)
|
|
1768
|
+
email: EmailSettings = Field(default_factory=EmailSettings)
|
|
1588
1769
|
test: TestSettings = Field(default_factory=TestSettings)
|
|
1589
1770
|
|
|
1590
1771
|
|
rem/sql/background_indexes.sql
CHANGED
|
@@ -21,6 +21,11 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_moments_vector_hnsw
|
|
|
21
21
|
ON embeddings_moments
|
|
22
22
|
USING hnsw (embedding vector_cosine_ops);
|
|
23
23
|
|
|
24
|
+
-- HNSW vector index for embeddings_ontologies
|
|
25
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_ontologies_vector_hnsw
|
|
26
|
+
ON embeddings_ontologies
|
|
27
|
+
USING hnsw (embedding vector_cosine_ops);
|
|
28
|
+
|
|
24
29
|
-- HNSW vector index for embeddings_ontology_configs
|
|
25
30
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_ontology_configs_vector_hnsw
|
|
26
31
|
ON embeddings_ontology_configs
|
|
@@ -44,6 +44,33 @@ BEGIN
|
|
|
44
44
|
RAISE NOTICE '✓ All required extensions installed successfully';
|
|
45
45
|
END $$;
|
|
46
46
|
|
|
47
|
+
-- ============================================================================
|
|
48
|
+
-- NORMALIZATION HELPER
|
|
49
|
+
-- ============================================================================
|
|
50
|
+
|
|
51
|
+
-- Normalize entity keys to lower-kebab-case for consistent lookups
|
|
52
|
+
-- "Mood Disorder" -> "mood-disorder"
|
|
53
|
+
-- "mood_disorder" -> "mood-disorder"
|
|
54
|
+
-- "MoodDisorder" -> "mood-disorder"
|
|
55
|
+
CREATE OR REPLACE FUNCTION normalize_key(input TEXT)
|
|
56
|
+
RETURNS TEXT AS $$
|
|
57
|
+
BEGIN
|
|
58
|
+
RETURN lower(
|
|
59
|
+
regexp_replace(
|
|
60
|
+
regexp_replace(
|
|
61
|
+
regexp_replace(input, '([a-z])([A-Z])', '\1-\2', 'g'), -- camelCase -> kebab
|
|
62
|
+
'[_\s]+', '-', 'g' -- underscores/spaces -> hyphens
|
|
63
|
+
),
|
|
64
|
+
'-+', '-', 'g' -- collapse multiple hyphens
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
END;
|
|
68
|
+
$$ LANGUAGE plpgsql IMMUTABLE;
|
|
69
|
+
|
|
70
|
+
COMMENT ON FUNCTION normalize_key IS
|
|
71
|
+
'Normalizes entity keys to lower-kebab-case for consistent lookups.
|
|
72
|
+
Examples: "Mood Disorder" -> "mood-disorder", "mood_disorder" -> "mood-disorder"';
|
|
73
|
+
|
|
47
74
|
-- ============================================================================
|
|
48
75
|
-- MIGRATION TRACKING
|
|
49
76
|
-- ============================================================================
|
|
@@ -237,10 +264,11 @@ BEGIN
|
|
|
237
264
|
|
|
238
265
|
-- First lookup in KV store to get entity_type (table name)
|
|
239
266
|
-- Include user-owned AND public (NULL user_id) entries
|
|
267
|
+
-- Normalize input key for consistent matching
|
|
240
268
|
SELECT kv.entity_type INTO entity_table
|
|
241
269
|
FROM kv_store kv
|
|
242
270
|
WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
|
|
243
|
-
AND kv.entity_key = p_entity_key
|
|
271
|
+
AND kv.entity_key = normalize_key(p_entity_key)
|
|
244
272
|
LIMIT 1;
|
|
245
273
|
|
|
246
274
|
-- If not found, return empty
|
|
@@ -414,6 +442,7 @@ BEGIN
|
|
|
414
442
|
FOR graph_keys IN
|
|
415
443
|
WITH RECURSIVE graph_traversal AS (
|
|
416
444
|
-- Base case: Find starting entity (user-owned OR public)
|
|
445
|
+
-- Normalize input key for consistent matching
|
|
417
446
|
SELECT
|
|
418
447
|
0 AS depth,
|
|
419
448
|
kv.entity_key,
|
|
@@ -424,7 +453,7 @@ BEGIN
|
|
|
424
453
|
ARRAY[kv.entity_key]::TEXT[] AS path
|
|
425
454
|
FROM kv_store kv
|
|
426
455
|
WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
|
|
427
|
-
AND kv.entity_key = p_entity_key
|
|
456
|
+
AND kv.entity_key = normalize_key(p_entity_key)
|
|
428
457
|
|
|
429
458
|
UNION ALL
|
|
430
459
|
|
|
@@ -441,7 +470,7 @@ BEGIN
|
|
|
441
470
|
JOIN kv_store source_kv ON source_kv.entity_key = gt.entity_key
|
|
442
471
|
AND (source_kv.user_id = effective_user_id OR source_kv.user_id IS NULL)
|
|
443
472
|
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)
|
|
473
|
+
JOIN kv_store target_kv ON target_kv.entity_key = normalize_key((edge->>'dst')::VARCHAR(255))
|
|
445
474
|
AND (target_kv.user_id = effective_user_id OR target_kv.user_id IS NULL)
|
|
446
475
|
WHERE gt.depth < p_max_depth
|
|
447
476
|
AND (p_rel_type IS NULL OR (edge->>'rel_type')::VARCHAR(100) = p_rel_type)
|
|
@@ -657,7 +686,7 @@ BEGIN
|
|
|
657
686
|
MIN(msg_counts.first_msg)::TIMESTAMP AS first_message_at,
|
|
658
687
|
MAX(msg_counts.last_msg)::TIMESTAMP AS last_message_at
|
|
659
688
|
FROM shared_sessions ss
|
|
660
|
-
LEFT JOIN users u ON u.
|
|
689
|
+
LEFT JOIN users u ON u.id::text = ss.owner_user_id AND u.tenant_id = ss.tenant_id
|
|
661
690
|
LEFT JOIN (
|
|
662
691
|
SELECT
|
|
663
692
|
m.session_id,
|