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.

Files changed (57) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +311 -0
  3. rem/agentic/context.py +81 -3
  4. rem/agentic/context_builder.py +36 -9
  5. rem/agentic/mcp/tool_wrapper.py +43 -14
  6. rem/agentic/providers/pydantic_ai.py +76 -34
  7. rem/agentic/schema.py +4 -3
  8. rem/agentic/tools/rem_tools.py +11 -0
  9. rem/api/deps.py +3 -5
  10. rem/api/main.py +22 -3
  11. rem/api/mcp_router/resources.py +75 -14
  12. rem/api/mcp_router/server.py +28 -23
  13. rem/api/mcp_router/tools.py +177 -2
  14. rem/api/middleware/tracking.py +5 -5
  15. rem/api/routers/auth.py +352 -6
  16. rem/api/routers/chat/completions.py +5 -3
  17. rem/api/routers/chat/streaming.py +95 -22
  18. rem/api/routers/messages.py +24 -15
  19. rem/auth/__init__.py +13 -3
  20. rem/auth/jwt.py +352 -0
  21. rem/auth/middleware.py +70 -30
  22. rem/auth/providers/__init__.py +4 -1
  23. rem/auth/providers/email.py +215 -0
  24. rem/cli/commands/ask.py +1 -1
  25. rem/cli/commands/db.py +118 -54
  26. rem/models/entities/__init__.py +4 -0
  27. rem/models/entities/ontology.py +93 -101
  28. rem/models/entities/subscriber.py +175 -0
  29. rem/models/entities/user.py +1 -0
  30. rem/schemas/agents/core/agent-builder.yaml +235 -0
  31. rem/services/__init__.py +3 -1
  32. rem/services/content/service.py +4 -3
  33. rem/services/email/__init__.py +10 -0
  34. rem/services/email/service.py +522 -0
  35. rem/services/email/templates.py +360 -0
  36. rem/services/embeddings/worker.py +26 -12
  37. rem/services/postgres/README.md +38 -0
  38. rem/services/postgres/diff_service.py +19 -3
  39. rem/services/postgres/pydantic_to_sqlalchemy.py +37 -2
  40. rem/services/postgres/register_type.py +1 -1
  41. rem/services/postgres/repository.py +37 -25
  42. rem/services/postgres/schema_generator.py +5 -5
  43. rem/services/postgres/sql_builder.py +6 -5
  44. rem/services/session/compression.py +113 -50
  45. rem/services/session/reload.py +14 -7
  46. rem/services/user_service.py +41 -9
  47. rem/settings.py +182 -1
  48. rem/sql/background_indexes.sql +5 -0
  49. rem/sql/migrations/001_install.sql +33 -4
  50. rem/sql/migrations/002_install_models.sql +204 -186
  51. rem/sql/migrations/005_schema_update.sql +145 -0
  52. rem/utils/model_helpers.py +101 -0
  53. rem/utils/schema_loader.py +45 -7
  54. {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/METADATA +1 -1
  55. {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/RECORD +57 -48
  56. {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/WHEEL +0 -0
  57. {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 ID: {user_id}. To load user profile: Use REM LOOKUP users/{user_id}"
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
 
@@ -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.user_id = ss.owner_user_id AND u.tenant_id = ss.tenant_id
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,