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
rem/auth/middleware.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Authentication Middleware for FastAPI.
|
|
3
3
|
|
|
4
|
-
Protects API endpoints by requiring valid
|
|
5
|
-
Supports
|
|
4
|
+
Protects API endpoints by requiring valid authentication.
|
|
5
|
+
Supports multiple auth methods: JWT, API Key, Session, Dev Token.
|
|
6
|
+
Anonymous access with rate limiting when allow_anonymous=True.
|
|
6
7
|
MCP endpoints are always protected unless explicitly disabled.
|
|
7
8
|
|
|
8
9
|
Design Pattern:
|
|
9
10
|
- Check X-API-Key header first (if API key auth enabled)
|
|
11
|
+
- Check JWT token in Authorization header (Bearer token)
|
|
12
|
+
- Check dev token (non-production only, starts with "dev_")
|
|
10
13
|
- Check session for user on protected paths
|
|
11
|
-
- Check Bearer token for dev token (non-production only)
|
|
12
14
|
- MCP paths always require authentication (protected service)
|
|
13
15
|
- If allow_anonymous=True: Allow unauthenticated requests (marked as ANONYMOUS tier)
|
|
14
16
|
- If allow_anonymous=False: Return 401 for API calls, redirect browsers to login
|
|
@@ -122,6 +124,34 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
122
124
|
logger.warning("Invalid X-API-Key provided")
|
|
123
125
|
return None
|
|
124
126
|
|
|
127
|
+
def _check_jwt_token(self, request: Request) -> dict | None:
|
|
128
|
+
"""
|
|
129
|
+
Check for valid JWT in Authorization header.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
User dict if valid JWT, None otherwise
|
|
133
|
+
"""
|
|
134
|
+
auth_header = request.headers.get("authorization", "")
|
|
135
|
+
if not auth_header.startswith("Bearer "):
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
token = auth_header[7:] # Strip "Bearer "
|
|
139
|
+
|
|
140
|
+
# Skip dev tokens (handled separately)
|
|
141
|
+
if token.startswith("dev_"):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# Verify JWT token
|
|
145
|
+
from .jwt import get_jwt_service
|
|
146
|
+
jwt_service = get_jwt_service()
|
|
147
|
+
user = jwt_service.verify_token(token)
|
|
148
|
+
|
|
149
|
+
if user:
|
|
150
|
+
logger.debug(f"JWT authenticated: {user.get('email')}")
|
|
151
|
+
return user
|
|
152
|
+
|
|
153
|
+
return None
|
|
154
|
+
|
|
125
155
|
def _check_dev_token(self, request: Request) -> dict | None:
|
|
126
156
|
"""
|
|
127
157
|
Check for valid dev token in Authorization header (non-production only).
|
|
@@ -207,6 +237,13 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
207
237
|
headers={"WWW-Authenticate": 'ApiKey realm="REM API"'},
|
|
208
238
|
)
|
|
209
239
|
|
|
240
|
+
# Check for JWT token in Authorization header
|
|
241
|
+
jwt_user = self._check_jwt_token(request)
|
|
242
|
+
if jwt_user:
|
|
243
|
+
request.state.user = jwt_user
|
|
244
|
+
request.state.is_anonymous = False
|
|
245
|
+
return await call_next(request)
|
|
246
|
+
|
|
210
247
|
# Check for dev token (non-production only)
|
|
211
248
|
dev_user = self._check_dev_token(request)
|
|
212
249
|
if dev_user:
|
|
@@ -214,7 +251,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
214
251
|
request.state.is_anonymous = False
|
|
215
252
|
return await call_next(request)
|
|
216
253
|
|
|
217
|
-
# Check for valid session
|
|
254
|
+
# Check for valid session (backward compatibility)
|
|
218
255
|
user = request.session.get("user")
|
|
219
256
|
|
|
220
257
|
if user:
|
rem/auth/providers/__init__.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Authentication provider implementations."""
|
|
2
2
|
|
|
3
3
|
from .base import OAuthProvider, OAuthTokens, OAuthUserInfo
|
|
4
|
+
from .email import EmailAuthProvider, EmailAuthResult
|
|
4
5
|
from .google import GoogleOAuthProvider
|
|
5
6
|
from .microsoft import MicrosoftOAuthProvider
|
|
6
7
|
|
|
@@ -8,6 +9,8 @@ __all__ = [
|
|
|
8
9
|
"OAuthProvider",
|
|
9
10
|
"OAuthTokens",
|
|
10
11
|
"OAuthUserInfo",
|
|
12
|
+
"EmailAuthProvider",
|
|
13
|
+
"EmailAuthResult",
|
|
11
14
|
"GoogleOAuthProvider",
|
|
12
15
|
"MicrosoftOAuthProvider",
|
|
13
16
|
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Email Authentication Provider.
|
|
3
|
+
|
|
4
|
+
Passwordless authentication using email verification codes.
|
|
5
|
+
Unlike OAuth providers, this handles the full flow internally.
|
|
6
|
+
|
|
7
|
+
Flow:
|
|
8
|
+
1. User requests login with email address
|
|
9
|
+
2. System generates code, upserts user, sends email
|
|
10
|
+
3. User enters code
|
|
11
|
+
4. System verifies code and creates session
|
|
12
|
+
|
|
13
|
+
Design:
|
|
14
|
+
- Uses EmailService for sending codes
|
|
15
|
+
- Creates users with deterministic UUID from email hash
|
|
16
|
+
- Stores challenge in user metadata
|
|
17
|
+
- No external OAuth dependencies
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
from loguru import logger
|
|
23
|
+
|
|
24
|
+
from ...services.email import EmailService
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from ...services.postgres import PostgresService
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EmailAuthResult(BaseModel):
|
|
31
|
+
"""Result of email authentication operations."""
|
|
32
|
+
|
|
33
|
+
success: bool = Field(description="Whether operation succeeded")
|
|
34
|
+
email: str = Field(description="Email address")
|
|
35
|
+
user_id: str | None = Field(default=None, description="User ID if authenticated")
|
|
36
|
+
error: str | None = Field(default=None, description="Error message if failed")
|
|
37
|
+
message: str | None = Field(default=None, description="User-friendly message")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EmailAuthProvider:
|
|
41
|
+
"""
|
|
42
|
+
Email-based passwordless authentication provider.
|
|
43
|
+
|
|
44
|
+
Handles the complete email login flow:
|
|
45
|
+
1. send_code() - Generate and send verification code
|
|
46
|
+
2. verify_code() - Verify code and return user info
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
email_service: EmailService | None = None,
|
|
52
|
+
template_kwargs: dict | None = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize EmailAuthProvider.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
email_service: EmailService instance (creates new one if not provided)
|
|
59
|
+
template_kwargs: Customization for email templates (colors, branding, etc.)
|
|
60
|
+
"""
|
|
61
|
+
self._email_service = email_service or EmailService()
|
|
62
|
+
self._template_kwargs = template_kwargs or {}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_configured(self) -> bool:
|
|
66
|
+
"""Check if email auth is properly configured."""
|
|
67
|
+
return self._email_service.is_configured
|
|
68
|
+
|
|
69
|
+
async def send_code(
|
|
70
|
+
self,
|
|
71
|
+
email: str,
|
|
72
|
+
db: "PostgresService",
|
|
73
|
+
tenant_id: str = "default",
|
|
74
|
+
) -> EmailAuthResult:
|
|
75
|
+
"""
|
|
76
|
+
Send a verification code to an email address.
|
|
77
|
+
|
|
78
|
+
Creates user if not exists (using deterministic UUID from email).
|
|
79
|
+
Stores code in user metadata.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
email: Email address to send code to
|
|
83
|
+
db: PostgresService instance
|
|
84
|
+
tenant_id: Tenant identifier
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
EmailAuthResult with success status
|
|
88
|
+
"""
|
|
89
|
+
if not self.is_configured:
|
|
90
|
+
return EmailAuthResult(
|
|
91
|
+
success=False,
|
|
92
|
+
email=email,
|
|
93
|
+
error="Email service not configured",
|
|
94
|
+
message="Email login is not available. Please try another method.",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
result = await self._email_service.send_login_code(
|
|
99
|
+
email=email,
|
|
100
|
+
db=db,
|
|
101
|
+
tenant_id=tenant_id,
|
|
102
|
+
template_kwargs=self._template_kwargs,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if result["success"]:
|
|
106
|
+
return EmailAuthResult(
|
|
107
|
+
success=True,
|
|
108
|
+
email=email,
|
|
109
|
+
user_id=result["user_id"],
|
|
110
|
+
message=f"Verification code sent to {email}. Check your inbox.",
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
return EmailAuthResult(
|
|
114
|
+
success=False,
|
|
115
|
+
email=email,
|
|
116
|
+
error=result.get("error", "Failed to send code"),
|
|
117
|
+
message="Failed to send verification code. Please try again.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.error(f"Error sending login code: {e}")
|
|
122
|
+
return EmailAuthResult(
|
|
123
|
+
success=False,
|
|
124
|
+
email=email,
|
|
125
|
+
error=str(e),
|
|
126
|
+
message="An error occurred. Please try again.",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def verify_code(
|
|
130
|
+
self,
|
|
131
|
+
email: str,
|
|
132
|
+
code: str,
|
|
133
|
+
db: "PostgresService",
|
|
134
|
+
tenant_id: str = "default",
|
|
135
|
+
) -> EmailAuthResult:
|
|
136
|
+
"""
|
|
137
|
+
Verify a login code and authenticate user.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
email: Email address
|
|
141
|
+
code: 6-digit verification code
|
|
142
|
+
db: PostgresService instance
|
|
143
|
+
tenant_id: Tenant identifier
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
EmailAuthResult with user_id if successful
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
result = await self._email_service.verify_login_code(
|
|
150
|
+
email=email,
|
|
151
|
+
code=code,
|
|
152
|
+
db=db,
|
|
153
|
+
tenant_id=tenant_id,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if result["valid"]:
|
|
157
|
+
return EmailAuthResult(
|
|
158
|
+
success=True,
|
|
159
|
+
email=email,
|
|
160
|
+
user_id=result["user_id"],
|
|
161
|
+
message="Successfully authenticated!",
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
error = result.get("error", "Invalid code")
|
|
165
|
+
# User-friendly error messages
|
|
166
|
+
if error == "Login code expired":
|
|
167
|
+
message = "Your code has expired. Please request a new one."
|
|
168
|
+
elif error == "Invalid login code":
|
|
169
|
+
message = "Invalid code. Please check and try again."
|
|
170
|
+
elif error == "No login code requested":
|
|
171
|
+
message = "No code was requested for this email. Please request a new code."
|
|
172
|
+
elif error == "User not found":
|
|
173
|
+
message = "Email not found. Please request a login code first."
|
|
174
|
+
else:
|
|
175
|
+
message = "Verification failed. Please try again."
|
|
176
|
+
|
|
177
|
+
return EmailAuthResult(
|
|
178
|
+
success=False,
|
|
179
|
+
email=email,
|
|
180
|
+
error=error,
|
|
181
|
+
message=message,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Error verifying login code: {e}")
|
|
186
|
+
return EmailAuthResult(
|
|
187
|
+
success=False,
|
|
188
|
+
email=email,
|
|
189
|
+
error=str(e),
|
|
190
|
+
message="An error occurred. Please try again.",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def get_user_dict(self, email: str, user_id: str) -> dict:
|
|
194
|
+
"""
|
|
195
|
+
Create a user dict for session storage.
|
|
196
|
+
|
|
197
|
+
Compatible with OAuth user format for consistent session handling.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
email: User's email
|
|
201
|
+
user_id: User's UUID
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
User dict for session
|
|
205
|
+
"""
|
|
206
|
+
return {
|
|
207
|
+
"id": user_id,
|
|
208
|
+
"email": email,
|
|
209
|
+
"email_verified": True, # Email is verified through code
|
|
210
|
+
"name": email.split("@")[0], # Use email prefix as name
|
|
211
|
+
"provider": "email",
|
|
212
|
+
"tenant_id": "default",
|
|
213
|
+
"tier": "free", # Email users start at free tier
|
|
214
|
+
"roles": ["user"],
|
|
215
|
+
}
|
rem/models/entities/__init__.py
CHANGED
|
@@ -39,6 +39,7 @@ from .shared_session import (
|
|
|
39
39
|
SharedWithMeResponse,
|
|
40
40
|
SharedWithMeSummary,
|
|
41
41
|
)
|
|
42
|
+
from .subscriber import Subscriber, SubscriberOrigin, SubscriberStatus
|
|
42
43
|
from .user import User, UserTier
|
|
43
44
|
|
|
44
45
|
__all__ = [
|
|
@@ -56,6 +57,9 @@ __all__ = [
|
|
|
56
57
|
"FeedbackCategory",
|
|
57
58
|
"User",
|
|
58
59
|
"UserTier",
|
|
60
|
+
"Subscriber",
|
|
61
|
+
"SubscriberStatus",
|
|
62
|
+
"SubscriberOrigin",
|
|
59
63
|
"File",
|
|
60
64
|
"Moment",
|
|
61
65
|
"Schema",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subscriber - Email subscription management.
|
|
3
|
+
|
|
4
|
+
This model stores subscribers who sign up via websites/apps.
|
|
5
|
+
Subscribers can be collected before user registration for newsletters,
|
|
6
|
+
updates, and approval-based access control.
|
|
7
|
+
|
|
8
|
+
Key features:
|
|
9
|
+
- Deterministic UUID from email (same email = same ID)
|
|
10
|
+
- Approval workflow for access control
|
|
11
|
+
- Tags for segmentation
|
|
12
|
+
- Origin tracking for analytics
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import uuid
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from pydantic import Field, EmailStr, model_validator
|
|
21
|
+
|
|
22
|
+
from ..core import CoreModel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SubscriberStatus(str, Enum):
|
|
26
|
+
"""Subscription status."""
|
|
27
|
+
|
|
28
|
+
ACTIVE = "active" # Actively subscribed
|
|
29
|
+
UNSUBSCRIBED = "unsubscribed" # User unsubscribed
|
|
30
|
+
BOUNCED = "bounced" # Email bounced
|
|
31
|
+
PENDING = "pending" # Pending confirmation (if double opt-in)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SubscriberOrigin(str, Enum):
|
|
35
|
+
"""Where the subscription originated from."""
|
|
36
|
+
|
|
37
|
+
WEBSITE = "website" # Main website subscribe form
|
|
38
|
+
LANDING_PAGE = "landing_page" # Campaign landing page
|
|
39
|
+
APP = "app" # In-app subscription
|
|
40
|
+
IMPORT = "import" # Bulk import
|
|
41
|
+
REFERRAL = "referral" # Referred by another user
|
|
42
|
+
OTHER = "other"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Subscriber(CoreModel):
|
|
46
|
+
"""
|
|
47
|
+
Email subscriber for newsletters and access control.
|
|
48
|
+
|
|
49
|
+
This model captures subscribers who sign up via the website, landing pages,
|
|
50
|
+
or in-app prompts. Uses deterministic UUID from email for natural upserts.
|
|
51
|
+
|
|
52
|
+
Access control via `approved` field:
|
|
53
|
+
- When email auth checks subscriber status, only approved subscribers
|
|
54
|
+
can complete login (if approval is enabled in settings).
|
|
55
|
+
- Subscribers can be pre-approved, or approved manually/automatically.
|
|
56
|
+
|
|
57
|
+
Usage:
|
|
58
|
+
from rem.services.postgres import Repository
|
|
59
|
+
from rem.models.entities import Subscriber, SubscriberStatus
|
|
60
|
+
|
|
61
|
+
repo = Repository(Subscriber, db=db)
|
|
62
|
+
|
|
63
|
+
# Create subscriber (ID auto-generated from email)
|
|
64
|
+
subscriber = Subscriber(
|
|
65
|
+
email="user@example.com",
|
|
66
|
+
name="John Doe",
|
|
67
|
+
origin=SubscriberOrigin.WEBSITE,
|
|
68
|
+
)
|
|
69
|
+
await repo.upsert(subscriber)
|
|
70
|
+
|
|
71
|
+
# Check if approved for login
|
|
72
|
+
subscriber = await repo.get_by_id(subscriber.id, tenant_id="default")
|
|
73
|
+
if subscriber and subscriber.approved:
|
|
74
|
+
# Allow login
|
|
75
|
+
pass
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# Required field
|
|
79
|
+
email: EmailStr = Field(
|
|
80
|
+
description="Subscriber's email address (unique identifier)"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Optional fields
|
|
84
|
+
name: Optional[str] = Field(
|
|
85
|
+
default=None,
|
|
86
|
+
description="Subscriber's name (optional)"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
comment: Optional[str] = Field(
|
|
90
|
+
default=None,
|
|
91
|
+
max_length=500,
|
|
92
|
+
description="Optional comment or message from subscriber"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
status: SubscriberStatus = Field(
|
|
96
|
+
default=SubscriberStatus.ACTIVE,
|
|
97
|
+
description="Current subscription status"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Access control
|
|
101
|
+
approved: bool = Field(
|
|
102
|
+
default=False,
|
|
103
|
+
description="Whether subscriber is approved for login (for approval workflows)"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
approved_at: Optional[datetime] = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description="When the subscriber was approved"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
approved_by: Optional[str] = Field(
|
|
112
|
+
default=None,
|
|
113
|
+
description="Who approved the subscriber (user ID or 'system')"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Origin tracking
|
|
117
|
+
origin: SubscriberOrigin = Field(
|
|
118
|
+
default=SubscriberOrigin.WEBSITE,
|
|
119
|
+
description="Where the subscription originated"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
origin_detail: Optional[str] = Field(
|
|
123
|
+
default=None,
|
|
124
|
+
description="Additional origin context (e.g., campaign name, page URL)"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Timestamps
|
|
128
|
+
subscribed_at: datetime = Field(
|
|
129
|
+
default_factory=lambda: datetime.now(timezone.utc),
|
|
130
|
+
description="When the subscription was created"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
unsubscribed_at: Optional[datetime] = Field(
|
|
134
|
+
default=None,
|
|
135
|
+
description="When the user unsubscribed (if applicable)"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Compliance
|
|
139
|
+
ip_address: Optional[str] = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="IP address at subscription time (for compliance)"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
user_agent: Optional[str] = Field(
|
|
145
|
+
default=None,
|
|
146
|
+
description="Browser user agent at subscription time"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Segmentation
|
|
150
|
+
tags: list[str] = Field(
|
|
151
|
+
default_factory=list,
|
|
152
|
+
description="Tags for segmentation (e.g., ['early-access', 'beta'])"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def email_to_uuid(email: str) -> uuid.UUID:
|
|
157
|
+
"""Generate a deterministic UUID from an email address.
|
|
158
|
+
|
|
159
|
+
Uses UUID v5 with DNS namespace for consistency with
|
|
160
|
+
EmailService.generate_user_id_from_email().
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
email: Email address
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Deterministic UUID
|
|
167
|
+
"""
|
|
168
|
+
return uuid.uuid5(uuid.NAMESPACE_DNS, email.lower().strip())
|
|
169
|
+
|
|
170
|
+
@model_validator(mode="after")
|
|
171
|
+
def set_id_from_email(self) -> "Subscriber":
|
|
172
|
+
"""Auto-generate deterministic ID from email for natural upsert."""
|
|
173
|
+
if self.email:
|
|
174
|
+
self.id = self.email_to_uuid(self.email)
|
|
175
|
+
return self
|
rem/models/entities/user.py
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
type: object
|
|
2
|
+
description: |
|
|
3
|
+
# Agent Builder - Create Custom AI Agents Through Conversation
|
|
4
|
+
|
|
5
|
+
You help users create custom AI agents by chatting with them naturally.
|
|
6
|
+
Gather requirements conversationally, show previews, and save the agent when ready.
|
|
7
|
+
|
|
8
|
+
## Your Workflow
|
|
9
|
+
|
|
10
|
+
1. **Understand the need**: Ask what they want the agent to do
|
|
11
|
+
2. **Define personality**: Help them choose tone and style
|
|
12
|
+
3. **Structure outputs**: If needed, define what data the agent captures
|
|
13
|
+
4. **Preview**: Show them what the agent will look like
|
|
14
|
+
5. **Save**: Use `save_agent` tool to persist it
|
|
15
|
+
|
|
16
|
+
## Conversation Style
|
|
17
|
+
|
|
18
|
+
Be friendly and helpful. Ask one or two questions at a time.
|
|
19
|
+
Don't overwhelm with options - guide them step by step.
|
|
20
|
+
|
|
21
|
+
## Gathering Requirements
|
|
22
|
+
|
|
23
|
+
Ask about:
|
|
24
|
+
- What should this agent help with?
|
|
25
|
+
- What tone should it have? (casual, professional, empathetic, etc.)
|
|
26
|
+
- Should it capture any specific information? (optional)
|
|
27
|
+
- What should it be called?
|
|
28
|
+
|
|
29
|
+
## Preview Format
|
|
30
|
+
|
|
31
|
+
Before saving, show a preview using markdown:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
## Agent Preview: {name}
|
|
35
|
+
|
|
36
|
+
**Personality:**
|
|
37
|
+
{brief description of tone and approach}
|
|
38
|
+
|
|
39
|
+
**System Prompt:**
|
|
40
|
+
{the actual prompt that will guide the agent}
|
|
41
|
+
|
|
42
|
+
**Structured Fields:** (if any)
|
|
43
|
+
| Field | Type | Description |
|
|
44
|
+
|-------|------|-------------|
|
|
45
|
+
| answer | string | Response to user |
|
|
46
|
+
| ... | ... | ... |
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Ask: "Does this look good? I can save it now or we can adjust anything."
|
|
50
|
+
|
|
51
|
+
## Saving the Agent
|
|
52
|
+
|
|
53
|
+
When the user approves, call `save_agent` with:
|
|
54
|
+
- `name`: kebab-case name (e.g., "customer-support-bot")
|
|
55
|
+
- `description`: The full system prompt
|
|
56
|
+
- `properties`: Structured output fields (optional, defaults to just "answer")
|
|
57
|
+
- `required`: Which fields are required (optional, defaults to ["answer"])
|
|
58
|
+
- `tags`: Relevant tags for organization
|
|
59
|
+
|
|
60
|
+
## Default Tools
|
|
61
|
+
|
|
62
|
+
All agents automatically get `search_rem` and `register_metadata` tools.
|
|
63
|
+
You don't need to specify these.
|
|
64
|
+
|
|
65
|
+
## Example Conversation
|
|
66
|
+
|
|
67
|
+
User: "I want an agent that helps write professional emails"
|
|
68
|
+
|
|
69
|
+
You: "Great idea! Let me help you create an email writing assistant.
|
|
70
|
+
|
|
71
|
+
What tone should it have? For example:
|
|
72
|
+
- Professional and formal
|
|
73
|
+
- Friendly but polished
|
|
74
|
+
- Direct and concise"
|
|
75
|
+
|
|
76
|
+
User: "Friendly but polished"
|
|
77
|
+
|
|
78
|
+
You: "Perfect! Should it just write emails, or also help with things like:
|
|
79
|
+
- Suggesting subject lines
|
|
80
|
+
- Adjusting tone for different recipients
|
|
81
|
+
- Summarizing long email threads"
|
|
82
|
+
|
|
83
|
+
User: "All of those would be helpful"
|
|
84
|
+
|
|
85
|
+
You: "Let me show you a preview:
|
|
86
|
+
|
|
87
|
+
## Agent Preview: email-assistant
|
|
88
|
+
|
|
89
|
+
**Personality:** Friendly, professional, helpful writing partner
|
|
90
|
+
|
|
91
|
+
**System Prompt:**
|
|
92
|
+
You are a friendly email writing assistant. Help users craft polished,
|
|
93
|
+
professional emails while maintaining a warm tone. You can:
|
|
94
|
+
- Write new emails from scratch
|
|
95
|
+
- Suggest compelling subject lines
|
|
96
|
+
- Adjust tone for different audiences
|
|
97
|
+
- Summarize long email threads
|
|
98
|
+
|
|
99
|
+
Always ask clarifying questions if the request is unclear.
|
|
100
|
+
|
|
101
|
+
**Structured Fields:**
|
|
102
|
+
| Field | Type | Description |
|
|
103
|
+
|-------|------|-------------|
|
|
104
|
+
| answer | string | Your response or the drafted email |
|
|
105
|
+
|
|
106
|
+
Does this look good? I can save it now or adjust anything."
|
|
107
|
+
|
|
108
|
+
User: "Looks great, save it!"
|
|
109
|
+
|
|
110
|
+
You: *calls save_agent tool*
|
|
111
|
+
"Done! Your email-assistant is ready. Use `/custom-agent email-assistant` to start chatting with it."
|
|
112
|
+
|
|
113
|
+
properties:
|
|
114
|
+
answer:
|
|
115
|
+
type: string
|
|
116
|
+
description: Your conversational response to the user
|
|
117
|
+
|
|
118
|
+
required:
|
|
119
|
+
- answer
|
|
120
|
+
|
|
121
|
+
json_schema_extra:
|
|
122
|
+
kind: agent
|
|
123
|
+
name: agent-builder
|
|
124
|
+
version: "1.0.0"
|
|
125
|
+
tags:
|
|
126
|
+
- meta
|
|
127
|
+
- builder
|
|
128
|
+
tools:
|
|
129
|
+
- name: save_agent
|
|
130
|
+
description: "Save the agent schema to make it available for use"
|
|
131
|
+
- name: search_rem
|
|
132
|
+
description: "Search for existing agents as examples"
|
|
133
|
+
- name: register_metadata
|
|
134
|
+
description: "Record session metadata"
|
rem/services/__init__.py
CHANGED
|
@@ -4,13 +4,15 @@ REM Services
|
|
|
4
4
|
Service layer for REM system operations:
|
|
5
5
|
- PostgresService: PostgreSQL/CloudNativePG database operations
|
|
6
6
|
- RemService: REM query execution and graph operations
|
|
7
|
+
- EmailService: Transactional emails and passwordless login
|
|
7
8
|
|
|
8
9
|
For file/S3 operations, use rem.services.fs instead:
|
|
9
10
|
from rem.services.fs import FS, S3Provider
|
|
10
11
|
"""
|
|
11
12
|
|
|
13
|
+
from .email import EmailService
|
|
12
14
|
from .fs.service import FileSystemService
|
|
13
15
|
from .postgres import PostgresService
|
|
14
16
|
from .rem import RemService
|
|
15
17
|
|
|
16
|
-
__all__ = ["PostgresService", "RemService", "FileSystemService"]
|
|
18
|
+
__all__ = ["EmailService", "PostgresService", "RemService", "FileSystemService"]
|
rem/services/content/service.py
CHANGED
|
@@ -666,10 +666,11 @@ class ContentService:
|
|
|
666
666
|
# IMPORTANT: category field distinguishes agents from evaluators
|
|
667
667
|
# - kind=agent → category="agent" (AI agents with tools/resources)
|
|
668
668
|
# - kind=evaluator → category="evaluator" (LLM-as-a-Judge evaluators)
|
|
669
|
-
#
|
|
669
|
+
# User-scoped schemas: if user_id provided, scope to user's tenant
|
|
670
|
+
# System schemas: if no user_id, use "system" tenant for shared access
|
|
670
671
|
schema_entity = Schema(
|
|
671
|
-
tenant_id="system",
|
|
672
|
-
user_id=
|
|
672
|
+
tenant_id=user_id or "system",
|
|
673
|
+
user_id=user_id,
|
|
673
674
|
name=name,
|
|
674
675
|
spec=schema_data,
|
|
675
676
|
category=kind, # Maps kind → category for database filtering
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Email Service Module.
|
|
3
|
+
|
|
4
|
+
Provides EmailService for sending transactional emails and passwordless login.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .service import EmailService
|
|
8
|
+
from .templates import EmailTemplate, login_code_template
|
|
9
|
+
|
|
10
|
+
__all__ = ["EmailService", "EmailTemplate", "login_code_template"]
|