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.

Files changed (40) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +310 -0
  3. rem/agentic/context.py +81 -3
  4. rem/agentic/context_builder.py +18 -3
  5. rem/api/deps.py +3 -5
  6. rem/api/main.py +22 -3
  7. rem/api/mcp_router/server.py +2 -0
  8. rem/api/mcp_router/tools.py +90 -0
  9. rem/api/middleware/tracking.py +5 -5
  10. rem/api/routers/auth.py +346 -5
  11. rem/api/routers/chat/completions.py +4 -2
  12. rem/api/routers/chat/streaming.py +77 -22
  13. rem/api/routers/messages.py +24 -15
  14. rem/auth/__init__.py +13 -3
  15. rem/auth/jwt.py +352 -0
  16. rem/auth/middleware.py +42 -5
  17. rem/auth/providers/__init__.py +4 -1
  18. rem/auth/providers/email.py +215 -0
  19. rem/models/entities/__init__.py +4 -0
  20. rem/models/entities/subscriber.py +175 -0
  21. rem/models/entities/user.py +1 -0
  22. rem/schemas/agents/core/agent-builder.yaml +134 -0
  23. rem/services/__init__.py +3 -1
  24. rem/services/content/service.py +4 -3
  25. rem/services/email/__init__.py +10 -0
  26. rem/services/email/service.py +511 -0
  27. rem/services/email/templates.py +360 -0
  28. rem/services/postgres/README.md +38 -0
  29. rem/services/postgres/diff_service.py +19 -3
  30. rem/services/postgres/pydantic_to_sqlalchemy.py +37 -2
  31. rem/services/postgres/repository.py +5 -4
  32. rem/services/session/compression.py +113 -50
  33. rem/services/session/reload.py +14 -7
  34. rem/services/user_service.py +29 -0
  35. rem/settings.py +175 -0
  36. rem/sql/migrations/005_schema_update.sql +145 -0
  37. {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/METADATA +1 -1
  38. {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/RECORD +40 -31
  39. {remdb-0.3.146.dist-info → remdb-0.3.163.dist-info}/WHEEL +0 -0
  40. {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
- OAuth Authentication Middleware for FastAPI.
2
+ Authentication Middleware for FastAPI.
3
3
 
4
- Protects API endpoints by requiring valid session.
5
- Supports anonymous access with rate limiting when allow_anonymous=True.
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:
@@ -1,6 +1,7 @@
1
- """OAuth provider implementations."""
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
+ }
@@ -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
@@ -22,6 +22,7 @@ from ..core import CoreModel
22
22
  class UserTier(str, Enum):
23
23
  """User subscription tier for feature gating."""
24
24
 
25
+ BLOCKED = "blocked" # User is blocked from logging in
25
26
  ANONYMOUS = "anonymous"
26
27
  FREE = "free"
27
28
  BASIC = "basic"
@@ -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"]
@@ -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
- # Schemas (agents/evaluators) default to system tenant for shared access
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=None,
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"]