remdb 0.3.133__py3-none-any.whl → 0.3.171__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.
Files changed (60) 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 +54 -6
  6. rem/agentic/providers/phoenix.py +91 -21
  7. rem/agentic/providers/pydantic_ai.py +88 -45
  8. rem/api/deps.py +3 -5
  9. rem/api/main.py +22 -3
  10. rem/api/mcp_router/server.py +2 -0
  11. rem/api/mcp_router/tools.py +94 -2
  12. rem/api/middleware/tracking.py +5 -5
  13. rem/api/routers/auth.py +349 -6
  14. rem/api/routers/chat/completions.py +5 -3
  15. rem/api/routers/chat/streaming.py +95 -22
  16. rem/api/routers/messages.py +24 -15
  17. rem/auth/__init__.py +13 -3
  18. rem/auth/jwt.py +352 -0
  19. rem/auth/middleware.py +115 -10
  20. rem/auth/providers/__init__.py +4 -1
  21. rem/auth/providers/email.py +215 -0
  22. rem/cli/commands/configure.py +3 -4
  23. rem/cli/commands/experiments.py +50 -49
  24. rem/cli/commands/session.py +336 -0
  25. rem/cli/dreaming.py +2 -2
  26. rem/cli/main.py +2 -0
  27. rem/models/core/experiment.py +4 -14
  28. rem/models/entities/__init__.py +4 -0
  29. rem/models/entities/ontology.py +1 -1
  30. rem/models/entities/ontology_config.py +1 -1
  31. rem/models/entities/subscriber.py +175 -0
  32. rem/models/entities/user.py +1 -0
  33. rem/schemas/agents/core/agent-builder.yaml +235 -0
  34. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  35. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  36. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  37. rem/services/__init__.py +3 -1
  38. rem/services/content/service.py +4 -3
  39. rem/services/email/__init__.py +10 -0
  40. rem/services/email/service.py +513 -0
  41. rem/services/email/templates.py +360 -0
  42. rem/services/postgres/README.md +38 -0
  43. rem/services/postgres/diff_service.py +19 -3
  44. rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
  45. rem/services/postgres/repository.py +5 -4
  46. rem/services/session/compression.py +113 -50
  47. rem/services/session/reload.py +14 -7
  48. rem/services/user_service.py +41 -9
  49. rem/settings.py +200 -5
  50. rem/sql/migrations/001_install.sql +1 -1
  51. rem/sql/migrations/002_install_models.sql +91 -91
  52. rem/sql/migrations/005_schema_update.sql +145 -0
  53. rem/utils/README.md +45 -0
  54. rem/utils/files.py +157 -1
  55. rem/utils/schema_loader.py +45 -7
  56. rem/utils/vision.py +1 -1
  57. {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/METADATA +7 -5
  58. {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/RECORD +60 -50
  59. {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/WHEEL +0 -0
  60. {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,360 @@
1
+ """
2
+ Email Templates.
3
+
4
+ HTML email templates for transactional emails.
5
+ Uses inline CSS for maximum email client compatibility.
6
+
7
+ Downstream apps can customize by:
8
+ 1. Overriding COLORS dict
9
+ 2. Setting custom LOGO_URL and TAGLINE
10
+ 3. Creating custom templates using base_template()
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+ from typing import Optional
15
+
16
+
17
+ # Default colors - override in downstream apps
18
+ COLORS = {
19
+ "background": "#F5F5F5",
20
+ "foreground": "#333333",
21
+ "primary": "#4A90D9",
22
+ "accent": "#5CB85C",
23
+ "card": "#FFFFFF",
24
+ "muted": "#6b7280",
25
+ "border": "#E0E0E0",
26
+ }
27
+
28
+ # Branding - override in downstream apps
29
+ LOGO_URL: str | None = None
30
+ APP_NAME = "REM"
31
+ TAGLINE = "Your AI-powered platform"
32
+ WEBSITE_URL = "https://rem.ai"
33
+ PRIVACY_URL = "https://rem.ai/privacy"
34
+ TERMS_URL = "https://rem.ai/terms"
35
+
36
+
37
+ @dataclass
38
+ class EmailTemplate:
39
+ """Email template with subject and HTML body."""
40
+
41
+ subject: str
42
+ html_body: str
43
+
44
+
45
+ def base_template(
46
+ content: str,
47
+ preheader: Optional[str] = None,
48
+ colors: dict | None = None,
49
+ logo_url: str | None = None,
50
+ app_name: str | None = None,
51
+ tagline: str | None = None,
52
+ website_url: str | None = None,
53
+ privacy_url: str | None = None,
54
+ terms_url: str | None = None,
55
+ ) -> str:
56
+ """
57
+ Base email template.
58
+
59
+ Args:
60
+ content: The main content HTML
61
+ preheader: Optional preview text shown in email clients
62
+ colors: Color overrides (merges with COLORS)
63
+ logo_url: Logo image URL
64
+ app_name: Application name
65
+ tagline: Footer tagline
66
+ website_url: Main website URL
67
+ privacy_url: Privacy policy URL
68
+ terms_url: Terms of service URL
69
+
70
+ Returns:
71
+ Complete HTML email
72
+ """
73
+ # Merge colors
74
+ c = {**COLORS, **(colors or {})}
75
+
76
+ # Use provided values or module defaults
77
+ logo = logo_url or LOGO_URL
78
+ name = app_name or APP_NAME
79
+ tag = tagline or TAGLINE
80
+ web = website_url or WEBSITE_URL
81
+ privacy = privacy_url or PRIVACY_URL
82
+ terms = terms_url or TERMS_URL
83
+
84
+ preheader_html = ""
85
+ if preheader:
86
+ preheader_html = f'''
87
+ <div style="display: none; max-height: 0; overflow: hidden;">
88
+ {preheader}
89
+ </div>
90
+ '''
91
+
92
+ logo_html = ""
93
+ if logo:
94
+ logo_html = f'''
95
+ <img src="{logo}" alt="{name}" width="40" height="40" style="display: block; margin: 0 auto 16px auto; border-radius: 8px;">
96
+ '''
97
+
98
+ return f'''<!DOCTYPE html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="UTF-8">
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
103
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
104
+ <title>{name}</title>
105
+ <!--[if mso]>
106
+ <style type="text/css">
107
+ body, table, td {{font-family: Arial, sans-serif !important;}}
108
+ </style>
109
+ <![endif]-->
110
+ </head>
111
+ <body style="margin: 0; padding: 0; background-color: {c['background']}; font-family: 'Georgia', 'Times New Roman', serif;">
112
+ {preheader_html}
113
+
114
+ <!-- Email Container -->
115
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="background-color: {c['background']};">
116
+ <tr>
117
+ <td style="padding: 40px 20px;">
118
+ <!-- Inner Container -->
119
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="max-width: 560px; margin: 0 auto;">
120
+
121
+ <!-- Main Content Card -->
122
+ <tr>
123
+ <td style="background-color: {c['card']}; border-radius: 16px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);">
124
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
125
+ <tr>
126
+ <td style="padding: 40px;">
127
+ {content}
128
+ </td>
129
+ </tr>
130
+ </table>
131
+ </td>
132
+ </tr>
133
+
134
+ <!-- Footer -->
135
+ <tr>
136
+ <td style="padding: 32px 20px; text-align: center;">
137
+ {logo_html}
138
+
139
+ <!-- Tagline -->
140
+ <p style="margin: 0 0 8px 0; font-family: 'Georgia', serif; font-size: 14px; color: {c['muted']}; font-style: italic;">
141
+ {tag}
142
+ </p>
143
+
144
+ <!-- Footer Links -->
145
+ <p style="margin: 0; font-family: Arial, sans-serif; font-size: 12px; color: {c['muted']};">
146
+ <a href="{web}" style="color: {c['primary']}; text-decoration: none;">{name}</a>
147
+ &nbsp;&bull;&nbsp;
148
+ <a href="{privacy}" style="color: {c['primary']}; text-decoration: none;">Privacy</a>
149
+ &nbsp;&bull;&nbsp;
150
+ <a href="{terms}" style="color: {c['primary']}; text-decoration: none;">Terms</a>
151
+ </p>
152
+
153
+ <!-- Copyright -->
154
+ <p style="margin: 16px 0 0 0; font-family: Arial, sans-serif; font-size: 11px; color: {c['muted']};">
155
+ &copy; 2025 {name}. All rights reserved.
156
+ </p>
157
+ </td>
158
+ </tr>
159
+
160
+ </table>
161
+ </td>
162
+ </tr>
163
+ </table>
164
+ </body>
165
+ </html>'''
166
+
167
+
168
+ def login_code_template(
169
+ code: str,
170
+ email: str,
171
+ colors: dict | None = None,
172
+ app_name: str | None = None,
173
+ **kwargs,
174
+ ) -> EmailTemplate:
175
+ """
176
+ Generate a login code email template.
177
+
178
+ Args:
179
+ code: The 6-digit login code
180
+ email: The recipient's email address
181
+ colors: Color overrides
182
+ app_name: Application name
183
+ **kwargs: Additional arguments passed to base_template
184
+
185
+ Returns:
186
+ EmailTemplate with subject and HTML body
187
+ """
188
+ c = {**COLORS, **(colors or {})}
189
+ name = app_name or APP_NAME
190
+
191
+ # Format code with spaces for readability (e.g., "123 456")
192
+ formatted_code = f"{code[:3]} {code[3:]}" if len(code) == 6 else code
193
+
194
+ content = f'''
195
+ <!-- Greeting -->
196
+ <h1 style="margin: 0 0 8px 0; font-family: 'Arial', sans-serif; font-size: 24px; font-weight: 600; color: {c['foreground']};">
197
+ Hi there!
198
+ </h1>
199
+
200
+ <p style="margin: 0 0 24px 0; font-family: 'Georgia', serif; font-size: 16px; line-height: 1.6; color: {c['foreground']};">
201
+ Here's your login code for {name}. Enter this code to securely access your account.
202
+ </p>
203
+
204
+ <!-- Code Box -->
205
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 0 24px 0;">
206
+ <tr>
207
+ <td align="center">
208
+ <div style="
209
+ display: inline-block;
210
+ padding: 20px 40px;
211
+ background-color: {c['background']};
212
+ border: 2px solid {c['border']};
213
+ border-radius: 12px;
214
+ ">
215
+ <span style="
216
+ font-family: 'Courier New', monospace;
217
+ font-size: 32px;
218
+ font-weight: 700;
219
+ letter-spacing: 6px;
220
+ color: {c['foreground']};
221
+ ">{formatted_code}</span>
222
+ </div>
223
+ </td>
224
+ </tr>
225
+ </table>
226
+
227
+ <!-- Instructions -->
228
+ <p style="margin: 0 0 8px 0; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.6; color: {c['muted']};">
229
+ This code expires in <strong>10 minutes</strong>.
230
+ </p>
231
+
232
+ <p style="margin: 0 0 24px 0; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.6; color: {c['muted']};">
233
+ If you didn't request this code, you can safely ignore this email.
234
+ </p>
235
+
236
+ <!-- Divider -->
237
+ <hr style="border: none; border-top: 1px solid {c['border']}; margin: 24px 0;">
238
+
239
+ <!-- Security Note -->
240
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
241
+ <tr>
242
+ <td style="padding: 16px; background-color: {c['background']}; border-radius: 8px;">
243
+ <p style="margin: 0; font-family: Arial, sans-serif; font-size: 12px; color: {c['muted']};">
244
+ <strong style="color: {c['primary']};">Security tip:</strong>
245
+ Never share your login code with anyone. {name} will never ask for your code via phone or text.
246
+ </p>
247
+ </td>
248
+ </tr>
249
+ </table>
250
+ '''
251
+
252
+ return EmailTemplate(
253
+ subject=f"Your {name} Login Code",
254
+ html_body=base_template(
255
+ content=content,
256
+ preheader=f"Your login code is {formatted_code}",
257
+ colors=colors,
258
+ app_name=app_name,
259
+ **kwargs,
260
+ ),
261
+ )
262
+
263
+
264
+ def welcome_template(
265
+ name: Optional[str] = None,
266
+ colors: dict | None = None,
267
+ app_name: str | None = None,
268
+ features: list[str] | None = None,
269
+ cta_url: str | None = None,
270
+ cta_text: str = "Get Started",
271
+ **kwargs,
272
+ ) -> EmailTemplate:
273
+ """
274
+ Generate a welcome email template for new users.
275
+
276
+ Args:
277
+ name: Optional user's name
278
+ colors: Color overrides
279
+ app_name: Application name
280
+ features: List of feature descriptions
281
+ cta_url: Call-to-action button URL
282
+ cta_text: Call-to-action button text
283
+ **kwargs: Additional arguments passed to base_template
284
+
285
+ Returns:
286
+ EmailTemplate with subject and HTML body
287
+ """
288
+ c = {**COLORS, **(colors or {})}
289
+ app = app_name or APP_NAME
290
+ greeting = f"Hi {name}!" if name else "Welcome!"
291
+ url = cta_url or WEBSITE_URL
292
+
293
+ # Default features if not provided
294
+ default_features = [
295
+ "Access powerful AI capabilities",
296
+ "Store and organize your data",
297
+ "Collaborate with your team",
298
+ ]
299
+ feature_list = features or default_features
300
+
301
+ features_html = "".join(f"<li>{f}</li>" for f in feature_list)
302
+
303
+ content = f'''
304
+ <!-- Greeting -->
305
+ <h1 style="margin: 0 0 8px 0; font-family: 'Arial', sans-serif; font-size: 24px; font-weight: 600; color: {c['foreground']};">
306
+ {greeting}
307
+ </h1>
308
+
309
+ <p style="margin: 0 0 24px 0; font-family: 'Georgia', serif; font-size: 16px; line-height: 1.6; color: {c['foreground']};">
310
+ Welcome to {app}! We're excited to have you on board.
311
+ </p>
312
+
313
+ <!-- Feature List -->
314
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 0 24px 0;">
315
+ <tr>
316
+ <td style="padding: 16px; background-color: {c['background']}; border-radius: 8px;">
317
+ <p style="margin: 0 0 12px 0; font-family: Arial, sans-serif; font-size: 14px; font-weight: 600; color: {c['primary']};">
318
+ What you can do with {app}:
319
+ </p>
320
+ <ul style="margin: 0; padding-left: 20px; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.8; color: {c['foreground']};">
321
+ {features_html}
322
+ </ul>
323
+ </td>
324
+ </tr>
325
+ </table>
326
+
327
+ <!-- CTA Button -->
328
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%" style="margin: 0 0 24px 0;">
329
+ <tr>
330
+ <td align="center">
331
+ <a href="{url}" style="
332
+ display: inline-block;
333
+ padding: 14px 32px;
334
+ background-color: {c['primary']};
335
+ color: #FFFFFF;
336
+ font-family: Arial, sans-serif;
337
+ font-size: 16px;
338
+ font-weight: 600;
339
+ text-decoration: none;
340
+ border-radius: 8px;
341
+ ">{cta_text}</a>
342
+ </td>
343
+ </tr>
344
+ </table>
345
+
346
+ <p style="margin: 0; font-family: 'Georgia', serif; font-size: 14px; line-height: 1.6; color: {c['muted']}; text-align: center;">
347
+ We're glad you're here!
348
+ </p>
349
+ '''
350
+
351
+ return EmailTemplate(
352
+ subject=f"Welcome to {app}",
353
+ html_body=base_template(
354
+ content=content,
355
+ preheader=f"Welcome to {app}! Get started today.",
356
+ colors=colors,
357
+ app_name=app_name,
358
+ **kwargs,
359
+ ),
360
+ )
@@ -688,6 +688,44 @@ from rem.api.main import app # Use REM's FastAPI app
688
688
  # Or build your own app using rem.services
689
689
  ```
690
690
 
691
+ ## Adding Models & Migrations
692
+
693
+ Quick workflow for adding new database models:
694
+
695
+ 1. **Create a model** in `models/__init__.py` (or a submodule):
696
+ ```python
697
+ import rem
698
+ from rem.models.core import CoreModel
699
+
700
+ @rem.register_model
701
+ class MyEntity(CoreModel):
702
+ name: str
703
+ description: str # Auto-embedded (common field name)
704
+ ```
705
+
706
+ 2. **Check for schema drift** - REM auto-detects `./models` directory:
707
+ ```bash
708
+ rem db diff # Show pending changes (additive only)
709
+ rem db diff --strategy full # Include destructive changes
710
+ ```
711
+
712
+ 3. **Generate migration** (optional - for version-controlled SQL):
713
+ ```bash
714
+ rem db diff --generate # Creates numbered .sql file
715
+ ```
716
+
717
+ 4. **Apply changes**:
718
+ ```bash
719
+ rem db migrate # Apply all pending migrations
720
+ ```
721
+
722
+ **Key points:**
723
+ - Models in `./models/` are auto-discovered (must have `__init__.py`)
724
+ - Or set `MODELS__IMPORT_MODULES=myapp.models` for custom paths
725
+ - `CoreModel` provides: `id`, `tenant_id`, `user_id`, `created_at`, `updated_at`, `deleted_at`, `graph_edges`, `metadata`, `tags`
726
+ - Fields named `content`, `description`, `summary`, `text`, `body`, `message`, `notes` get embeddings by default
727
+ - Use `Field(json_schema_extra={"embed": True})` to embed other fields
728
+
691
729
  ## Configuration
692
730
 
693
731
  Environment variables:
@@ -22,6 +22,7 @@ from alembic.runtime.migration import MigrationContext
22
22
  from alembic.script import ScriptDirectory
23
23
  from loguru import logger
24
24
  from sqlalchemy import create_engine, text
25
+ from sqlalchemy.dialects import postgresql
25
26
 
26
27
  from ...settings import settings
27
28
  from .pydantic_to_sqlalchemy import get_target_metadata
@@ -472,6 +473,18 @@ class DiffService:
472
473
 
473
474
  return "\n".join(lines) + "\n"
474
475
 
476
+ def _compile_type(self, col_type) -> str:
477
+ """Compile SQLAlchemy type to PostgreSQL DDL string.
478
+
479
+ SQLAlchemy types like ARRAY(Text) need dialect-specific compilation
480
+ to render correctly (e.g., "TEXT[]" instead of just "ARRAY").
481
+ """
482
+ try:
483
+ return col_type.compile(dialect=postgresql.dialect())
484
+ except Exception:
485
+ # Fallback to string representation if compilation fails
486
+ return str(col_type)
487
+
475
488
  def _op_to_sql(self, op: ops.MigrateOperation) -> list[str]:
476
489
  """Convert operation to SQL statements."""
477
490
  lines = []
@@ -481,7 +494,8 @@ class DiffService:
481
494
  for col in op.columns:
482
495
  if hasattr(col, 'name') and hasattr(col, 'type'):
483
496
  nullable = "" if getattr(col, 'nullable', True) else " NOT NULL"
484
- cols.append(f" {col.name} {col.type}{nullable}")
497
+ type_str = self._compile_type(col.type)
498
+ cols.append(f" {col.name} {type_str}{nullable}")
485
499
  col_str = ",\n".join(cols)
486
500
  lines.append(f"CREATE TABLE IF NOT EXISTS {op.table_name} (\n{col_str}\n);")
487
501
 
@@ -491,14 +505,16 @@ class DiffService:
491
505
  elif isinstance(op, ops.AddColumnOp):
492
506
  col = op.column
493
507
  nullable = "" if getattr(col, 'nullable', True) else " NOT NULL"
494
- lines.append(f"ALTER TABLE {op.table_name} ADD COLUMN IF NOT EXISTS {col.name} {col.type}{nullable};")
508
+ type_str = self._compile_type(col.type)
509
+ lines.append(f"ALTER TABLE {op.table_name} ADD COLUMN IF NOT EXISTS {col.name} {type_str}{nullable};")
495
510
 
496
511
  elif isinstance(op, ops.DropColumnOp):
497
512
  lines.append(f"ALTER TABLE {op.table_name} DROP COLUMN IF EXISTS {op.column_name};")
498
513
 
499
514
  elif isinstance(op, ops.AlterColumnOp):
500
515
  if op.modify_type is not None:
501
- lines.append(f"ALTER TABLE {op.table_name} ALTER COLUMN {op.column_name} TYPE {op.modify_type};")
516
+ type_str = self._compile_type(op.modify_type)
517
+ lines.append(f"ALTER TABLE {op.table_name} ALTER COLUMN {op.column_name} TYPE {type_str};")
502
518
  if op.modify_nullable is not None:
503
519
  if op.modify_nullable:
504
520
  lines.append(f"ALTER TABLE {op.table_name} ALTER COLUMN {op.column_name} DROP NOT NULL;")
@@ -494,12 +494,13 @@ def _build_embeddings_table(base_table_name: str, metadata: MetaData) -> Table:
494
494
  ]
495
495
 
496
496
  # Create table with unique constraint
497
- # Note: constraint name matches PostgreSQL's auto-generated naming convention
497
+ # Truncate constraint name to fit PostgreSQL's 63-char identifier limit
498
+ constraint_name = f"uq_{base_table_name[:30]}_emb_entity_field_prov"
498
499
  table = Table(
499
500
  embeddings_table_name,
500
501
  metadata,
501
502
  *columns,
502
- UniqueConstraint("entity_id", "field_name", "provider", name=f"{embeddings_table_name}_entity_id_field_name_provider_key"),
503
+ UniqueConstraint("entity_id", "field_name", "provider", name=constraint_name),
503
504
  )
504
505
 
505
506
  # Add indexes (matching register_type output)
@@ -509,22 +510,53 @@ def _build_embeddings_table(base_table_name: str, metadata: MetaData) -> Table:
509
510
  return table
510
511
 
511
512
 
512
- def get_target_metadata() -> MetaData:
513
+ def _import_model_modules() -> list[str]:
513
514
  """
514
- Get SQLAlchemy metadata for Alembic autogenerate.
515
+ Import modules specified in MODELS__IMPORT_MODULES setting.
515
516
 
516
- This is the main entry point used by alembic/env.py.
517
+ This ensures downstream models decorated with @rem.register_model
518
+ are registered before schema generation.
517
519
 
518
520
  Returns:
519
- SQLAlchemy MetaData object representing current Pydantic models
521
+ List of successfully imported module names
520
522
  """
521
- import rem
523
+ import importlib
524
+ from ...settings import settings
525
+
526
+ imported = []
527
+ for module_name in settings.models.module_list:
528
+ try:
529
+ importlib.import_module(module_name)
530
+ imported.append(module_name)
531
+ logger.debug(f"Imported model module: {module_name}")
532
+ except ImportError as e:
533
+ logger.warning(f"Failed to import model module '{module_name}': {e}")
534
+ return imported
522
535
 
523
- package_root = Path(rem.__file__).parent.parent.parent
524
- models_dir = package_root / "src" / "rem" / "models" / "entities"
525
536
 
526
- if not models_dir.exists():
527
- logger.error(f"Models directory not found: {models_dir}")
528
- return MetaData()
537
+ def get_target_metadata() -> MetaData:
538
+ """
539
+ Get SQLAlchemy metadata for Alembic autogenerate.
529
540
 
530
- return build_sqlalchemy_metadata_from_pydantic(models_dir)
541
+ This is the main entry point used by alembic/env.py and rem db diff.
542
+
543
+ Uses the model registry as the source of truth, which includes:
544
+ - Core REM models (Resource, Message, User, etc.)
545
+ - User-registered models via @rem.register_model decorator
546
+
547
+ Before building metadata, imports model modules from settings to ensure
548
+ downstream models are registered. This supports:
549
+ - Auto-detection of ./models directory (convention)
550
+ - MODELS__IMPORT_MODULES env var (explicit configuration)
551
+
552
+ Returns:
553
+ SQLAlchemy MetaData object representing all registered Pydantic models
554
+ """
555
+ # Import model modules first (auto-detects ./models or uses MODELS__IMPORT_MODULES)
556
+ imported = _import_model_modules()
557
+ if imported:
558
+ logger.info(f"Imported model modules: {imported}")
559
+
560
+ # build_sqlalchemy_metadata_from_pydantic uses the registry internally,
561
+ # so no directory path is needed (the parameter is kept for backwards compat)
562
+ return build_sqlalchemy_metadata_from_pydantic()
@@ -141,13 +141,13 @@ class Repository(Generic[T]):
141
141
  # Return single item or list to match input type
142
142
  return records_list[0] if is_single else records_list
143
143
 
144
- async def get_by_id(self, record_id: str, tenant_id: str) -> T | None:
144
+ async def get_by_id(self, record_id: str, tenant_id: str | None = None) -> T | None:
145
145
  """
146
146
  Get a single record by ID.
147
147
 
148
148
  Args:
149
149
  record_id: Record identifier
150
- tenant_id: Tenant identifier for multi-tenancy isolation
150
+ tenant_id: Optional tenant identifier (deprecated, not used for filtering)
151
151
 
152
152
  Returns:
153
153
  Model instance or None if not found
@@ -164,13 +164,14 @@ class Repository(Generic[T]):
164
164
  if not self.db.pool:
165
165
  raise RuntimeError("Failed to establish database connection")
166
166
 
167
+ # Note: tenant_id filtering removed - use user_id for access control instead
167
168
  query = f"""
168
169
  SELECT * FROM {self.table_name}
169
- WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
170
+ WHERE id = $1 AND deleted_at IS NULL
170
171
  """
171
172
 
172
173
  async with self.db.pool.acquire() as conn:
173
- row = await conn.fetchrow(query, record_id, tenant_id)
174
+ row = await conn.fetchrow(query, record_id)
174
175
 
175
176
  if not row:
176
177
  return None