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.
- 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 +54 -6
- rem/agentic/providers/phoenix.py +91 -21
- rem/agentic/providers/pydantic_ai.py +88 -45
- 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 +94 -2
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/auth.py +349 -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 +115 -10
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/configure.py +3 -4
- rem/cli/commands/experiments.py +50 -49
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +2 -0
- rem/models/core/experiment.py +4 -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/postgres/README.md +38 -0
- rem/services/postgres/diff_service.py +19 -3
- rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
- 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 +41 -9
- rem/settings.py +200 -5
- rem/sql/migrations/001_install.sql +1 -1
- rem/sql/migrations/002_install_models.sql +91 -91
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/files.py +157 -1
- rem/utils/schema_loader.py +45 -7
- rem/utils/vision.py +1 -1
- {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/METADATA +7 -5
- {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/RECORD +60 -50
- {remdb-0.3.133.dist-info → remdb-0.3.171.dist-info}/WHEEL +0 -0
- {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
|
+
•
|
|
148
|
+
<a href="{privacy}" style="color: {c['primary']}; text-decoration: none;">Privacy</a>
|
|
149
|
+
•
|
|
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
|
+
© 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
|
+
)
|
rem/services/postgres/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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=
|
|
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
|
|
513
|
+
def _import_model_modules() -> list[str]:
|
|
513
514
|
"""
|
|
514
|
-
|
|
515
|
+
Import modules specified in MODELS__IMPORT_MODULES setting.
|
|
515
516
|
|
|
516
|
-
This
|
|
517
|
+
This ensures downstream models decorated with @rem.register_model
|
|
518
|
+
are registered before schema generation.
|
|
517
519
|
|
|
518
520
|
Returns:
|
|
519
|
-
|
|
521
|
+
List of successfully imported module names
|
|
520
522
|
"""
|
|
521
|
-
import
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
537
|
+
def get_target_metadata() -> MetaData:
|
|
538
|
+
"""
|
|
539
|
+
Get SQLAlchemy metadata for Alembic autogenerate.
|
|
529
540
|
|
|
530
|
-
|
|
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:
|
|
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
|
|
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
|
|
174
|
+
row = await conn.fetchrow(query, record_id)
|
|
174
175
|
|
|
175
176
|
if not row:
|
|
176
177
|
return None
|