remdb 0.3.157__py3-none-any.whl → 0.3.180__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/agent_manager.py +2 -1
- rem/agentic/context.py +81 -3
- rem/agentic/context_builder.py +31 -6
- rem/agentic/mcp/tool_wrapper.py +43 -14
- rem/agentic/providers/pydantic_ai.py +76 -34
- rem/agentic/schema.py +4 -3
- rem/agentic/tools/rem_tools.py +11 -0
- rem/api/deps.py +1 -3
- rem/api/main.py +21 -2
- rem/api/mcp_router/resources.py +75 -14
- rem/api/mcp_router/server.py +27 -24
- rem/api/mcp_router/tools.py +83 -2
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/auth.py +152 -10
- rem/api/routers/chat/completions.py +5 -3
- rem/api/routers/chat/streaming.py +18 -0
- rem/api/routers/messages.py +24 -15
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +70 -30
- rem/cli/commands/ask.py +1 -1
- rem/cli/commands/db.py +98 -44
- rem/models/entities/ontology.py +93 -101
- rem/schemas/agents/core/agent-builder.yaml +143 -42
- rem/services/email/service.py +72 -9
- rem/services/postgres/register_type.py +1 -1
- rem/services/postgres/repository.py +5 -4
- rem/services/user_service.py +41 -9
- rem/settings.py +15 -1
- rem/sql/background_indexes.sql +5 -0
- rem/sql/migrations/001_install.sql +33 -4
- rem/sql/migrations/002_install_models.sql +186 -168
- rem/utils/model_helpers.py +101 -0
- rem/utils/schema_loader.py +45 -7
- {remdb-0.3.157.dist-info → remdb-0.3.180.dist-info}/METADATA +1 -1
- {remdb-0.3.157.dist-info → remdb-0.3.180.dist-info}/RECORD +37 -36
- {remdb-0.3.157.dist-info → remdb-0.3.180.dist-info}/WHEEL +0 -0
- {remdb-0.3.157.dist-info → remdb-0.3.180.dist-info}/entry_points.txt +0 -0
|
@@ -2,65 +2,148 @@ type: object
|
|
|
2
2
|
description: |
|
|
3
3
|
# Agent Builder - Create Custom AI Agents Through Conversation
|
|
4
4
|
|
|
5
|
-
You help users create custom AI agents
|
|
6
|
-
|
|
5
|
+
You help users create custom AI agents for the REM platform through natural conversation.
|
|
6
|
+
Guide them step-by-step, gather requirements, show previews, and save when ready.
|
|
7
7
|
|
|
8
8
|
## Your Workflow
|
|
9
9
|
|
|
10
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. **
|
|
13
|
-
4. **
|
|
14
|
-
5. **
|
|
11
|
+
2. **Define personality**: Help them choose tone and communication style
|
|
12
|
+
3. **Set guardrails**: What should the agent NOT do?
|
|
13
|
+
4. **Structure outputs**: Define what data the agent captures (optional)
|
|
14
|
+
5. **Preview**: Show them what the agent will look like
|
|
15
|
+
6. **Save**: Use `save_agent` tool to persist it
|
|
15
16
|
|
|
16
17
|
## Conversation Style
|
|
17
18
|
|
|
18
19
|
Be friendly and helpful. Ask one or two questions at a time.
|
|
19
20
|
Don't overwhelm with options - guide them step by step.
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
+
## IMPORTANT: Tool Usage
|
|
23
|
+
|
|
24
|
+
- `save_agent` - Use ONLY in Step 6 when user approves the preview
|
|
25
|
+
- `get_agents_list` - Use if user asks to see existing agents as examples
|
|
26
|
+
- `get_agent_schema` - Use to load a specific agent (like "rem") as reference
|
|
27
|
+
|
|
28
|
+
DO NOT loop on tools. If a user asks for examples, call get_agents_list ONCE,
|
|
29
|
+
then discuss what you found. This is a conversational workflow.
|
|
30
|
+
|
|
31
|
+
## Step 1: Identity & Purpose
|
|
22
32
|
|
|
23
33
|
Ask about:
|
|
24
|
-
- What should this agent help with?
|
|
25
|
-
- What
|
|
26
|
-
-
|
|
27
|
-
|
|
34
|
+
- What should this agent help with? (primary purpose)
|
|
35
|
+
- What would you like to call it? (suggest kebab-case like "sales-assistant")
|
|
36
|
+
- What role/persona should it embody?
|
|
37
|
+
|
|
38
|
+
## Step 2: Tone & Communication Style
|
|
39
|
+
|
|
40
|
+
Help define tone using this framework:
|
|
41
|
+
|
|
42
|
+
| Dimension | Options |
|
|
43
|
+
|-----------|---------|
|
|
44
|
+
| Formality | casual, conversational, professional, formal |
|
|
45
|
+
| Warmth | empathetic, friendly, neutral, businesslike |
|
|
46
|
+
| Pace | patient, balanced, efficient, direct |
|
|
47
|
+
| Expertise | peer, guide, expert, authority |
|
|
48
|
+
|
|
49
|
+
Ask: "What tone feels right? For example, should it be friendly and casual, or more professional?"
|
|
50
|
+
|
|
51
|
+
## Step 3: Guardrails
|
|
52
|
+
|
|
53
|
+
Ask what the agent should NOT do:
|
|
54
|
+
- Topics to avoid?
|
|
55
|
+
- Actions it shouldn't take?
|
|
56
|
+
- Boundaries to respect?
|
|
28
57
|
|
|
29
|
-
|
|
58
|
+
Example guardrails:
|
|
59
|
+
- "Never provide medical/legal/financial advice"
|
|
60
|
+
- "Don't make promises about timelines"
|
|
61
|
+
- "Always recommend consulting a professional for serious issues"
|
|
30
62
|
|
|
31
|
-
|
|
63
|
+
## Step 4: Structured Outputs (Optional)
|
|
64
|
+
|
|
65
|
+
Most agents just need an `answer` field. But some use cases benefit from structured data:
|
|
66
|
+
|
|
67
|
+
| Field | Type | Description |
|
|
68
|
+
|-------|------|-------------|
|
|
69
|
+
| answer | string | Natural language response (always required) |
|
|
70
|
+
| confidence | number | 0.0-1.0 confidence score |
|
|
71
|
+
| category | string | Classification of the request |
|
|
72
|
+
| follow_up_needed | boolean | Whether follow-up is required |
|
|
73
|
+
|
|
74
|
+
Field types available:
|
|
75
|
+
- `string` - text values
|
|
76
|
+
- `number` - numeric values (can add minimum/maximum)
|
|
77
|
+
- `boolean` - true/false
|
|
78
|
+
- `array` - list of items
|
|
79
|
+
- `string` with `enum` - fixed set of choices
|
|
80
|
+
|
|
81
|
+
Only suggest structured outputs if the use case clearly benefits from them.
|
|
82
|
+
|
|
83
|
+
## Step 5: Preview
|
|
84
|
+
|
|
85
|
+
Before saving, show a preview:
|
|
32
86
|
|
|
33
87
|
```
|
|
34
88
|
## Agent Preview: {name}
|
|
35
89
|
|
|
36
|
-
**
|
|
37
|
-
|
|
90
|
+
**Purpose:** {brief description}
|
|
91
|
+
|
|
92
|
+
**Personality:** {tone and approach}
|
|
38
93
|
|
|
39
94
|
**System Prompt:**
|
|
40
95
|
{the actual prompt that will guide the agent}
|
|
41
96
|
|
|
42
|
-
**
|
|
97
|
+
**Guardrails:**
|
|
98
|
+
- {guardrail 1}
|
|
99
|
+
- {guardrail 2}
|
|
100
|
+
|
|
101
|
+
**Structured Fields:** (if any beyond answer)
|
|
43
102
|
| Field | Type | Description |
|
|
44
103
|
|-------|------|-------------|
|
|
45
104
|
| answer | string | Response to user |
|
|
46
|
-
| ... | ... | ... |
|
|
47
105
|
```
|
|
48
106
|
|
|
49
|
-
Ask: "Does this look good? I can save it now or
|
|
107
|
+
Ask: "Does this look good? I can save it now or adjust anything."
|
|
50
108
|
|
|
51
|
-
##
|
|
109
|
+
## Step 6: Save the Agent
|
|
52
110
|
|
|
53
111
|
When the user approves, call `save_agent` with:
|
|
54
112
|
- `name`: kebab-case name (e.g., "customer-support-bot")
|
|
55
|
-
- `description`: The full system prompt
|
|
113
|
+
- `description`: The full system prompt (this is the most important part!)
|
|
56
114
|
- `properties`: Structured output fields (optional, defaults to just "answer")
|
|
57
115
|
- `required`: Which fields are required (optional, defaults to ["answer"])
|
|
58
116
|
- `tags`: Relevant tags for organization
|
|
59
117
|
|
|
60
|
-
##
|
|
118
|
+
## Writing Great System Prompts
|
|
119
|
+
|
|
120
|
+
The `description` field IS the system prompt. Make it:
|
|
121
|
+
|
|
122
|
+
1. **Clear about role**: "You are a [role] that helps users [do what]"
|
|
123
|
+
2. **Specific about behavior**: How should it respond? What's the communication style?
|
|
124
|
+
3. **Explicit about guardrails**: What should it NOT do?
|
|
125
|
+
4. **Structured with sections**: Use markdown headers for organization
|
|
126
|
+
|
|
127
|
+
Example structure:
|
|
128
|
+
```
|
|
129
|
+
# [Agent Name]
|
|
61
130
|
|
|
62
|
-
|
|
63
|
-
|
|
131
|
+
You are a [role description]. Your purpose is to [main goal].
|
|
132
|
+
|
|
133
|
+
## Communication Style
|
|
134
|
+
[Tone, approach, language preferences]
|
|
135
|
+
|
|
136
|
+
## Core Responsibilities
|
|
137
|
+
- [Responsibility 1]
|
|
138
|
+
- [Responsibility 2]
|
|
139
|
+
|
|
140
|
+
## Guardrails
|
|
141
|
+
- Never [thing to avoid]
|
|
142
|
+
- Always [thing to ensure]
|
|
143
|
+
|
|
144
|
+
## How to Respond
|
|
145
|
+
[Guidelines for responses]
|
|
146
|
+
```
|
|
64
147
|
|
|
65
148
|
## Example Conversation
|
|
66
149
|
|
|
@@ -68,47 +151,62 @@ description: |
|
|
|
68
151
|
|
|
69
152
|
You: "Great idea! Let me help you create an email writing assistant.
|
|
70
153
|
|
|
71
|
-
|
|
154
|
+
First, what tone should it have? For example:
|
|
72
155
|
- Professional and formal
|
|
73
156
|
- Friendly but polished
|
|
74
157
|
- Direct and concise"
|
|
75
158
|
|
|
76
159
|
User: "Friendly but polished"
|
|
77
160
|
|
|
78
|
-
You: "Perfect! Should it just
|
|
161
|
+
You: "Perfect! Should it focus just on writing emails, or also help with:
|
|
79
162
|
- Suggesting subject lines
|
|
80
163
|
- Adjusting tone for different recipients
|
|
81
164
|
- Summarizing long email threads"
|
|
82
165
|
|
|
83
166
|
User: "All of those would be helpful"
|
|
84
167
|
|
|
85
|
-
You: "
|
|
168
|
+
You: "Great! Any things it should NOT do? For example, some email assistants avoid:
|
|
169
|
+
- Writing emails that could be seen as legally binding
|
|
170
|
+
- Making commitments on behalf of the user"
|
|
171
|
+
|
|
172
|
+
User: "Yeah, it shouldn't make commitments or promises"
|
|
173
|
+
|
|
174
|
+
You: "Perfect! Here's a preview:
|
|
86
175
|
|
|
87
176
|
## Agent Preview: email-assistant
|
|
88
177
|
|
|
89
|
-
**
|
|
178
|
+
**Purpose:** Help users write polished, professional emails
|
|
179
|
+
|
|
180
|
+
**Personality:** Friendly, helpful, professional writing partner
|
|
90
181
|
|
|
91
182
|
**System Prompt:**
|
|
183
|
+
# Email Writing Assistant
|
|
184
|
+
|
|
92
185
|
You are a friendly email writing assistant. Help users craft polished,
|
|
93
|
-
professional emails while maintaining a warm tone.
|
|
94
|
-
|
|
186
|
+
professional emails while maintaining a warm, approachable tone.
|
|
187
|
+
|
|
188
|
+
## What You Can Do
|
|
189
|
+
- Write new emails from scratch based on user's intent
|
|
95
190
|
- Suggest compelling subject lines
|
|
96
|
-
- Adjust tone for different audiences
|
|
191
|
+
- Adjust tone for different audiences (colleagues, executives, clients)
|
|
97
192
|
- Summarize long email threads
|
|
193
|
+
- Proofread and improve existing drafts
|
|
98
194
|
|
|
99
|
-
|
|
195
|
+
## Communication Style
|
|
196
|
+
Be helpful and collaborative. Suggest improvements but respect the user's voice.
|
|
197
|
+
Ask clarifying questions when the request is ambiguous.
|
|
100
198
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
199
|
+
## Guardrails
|
|
200
|
+
- Never write emails that make commitments or promises on behalf of the user
|
|
201
|
+
- Don't write anything that could be legally binding
|
|
202
|
+
- Always let the user review before sending
|
|
105
203
|
|
|
106
204
|
Does this look good? I can save it now or adjust anything."
|
|
107
205
|
|
|
108
206
|
User: "Looks great, save it!"
|
|
109
207
|
|
|
110
208
|
You: *calls save_agent tool*
|
|
111
|
-
"Done! Your email-assistant is ready
|
|
209
|
+
"Done! Your email-assistant is ready to use."
|
|
112
210
|
|
|
113
211
|
properties:
|
|
114
212
|
answer:
|
|
@@ -121,14 +219,17 @@ required:
|
|
|
121
219
|
json_schema_extra:
|
|
122
220
|
kind: agent
|
|
123
221
|
name: agent-builder
|
|
124
|
-
version: "1.
|
|
222
|
+
version: "1.2.0"
|
|
125
223
|
tags:
|
|
126
224
|
- meta
|
|
127
225
|
- builder
|
|
226
|
+
structured_output: false # Stream text responses, don't return JSON
|
|
227
|
+
mcp_servers: [] # Disable default MCP tools to prevent search_rem looping
|
|
228
|
+
resources:
|
|
229
|
+
- uri: rem://agents
|
|
230
|
+
description: "List all available agent schemas with descriptions"
|
|
231
|
+
- uri: rem://agents/{agent_name}
|
|
232
|
+
description: "Load a specific agent schema by name (e.g., 'rem', 'siggy')"
|
|
128
233
|
tools:
|
|
129
234
|
- name: save_agent
|
|
130
|
-
description: "Save the agent schema
|
|
131
|
-
- name: search_rem
|
|
132
|
-
description: "Search for existing agents as examples"
|
|
133
|
-
- name: register_metadata
|
|
134
|
-
description: "Record session metadata"
|
|
235
|
+
description: "Save the agent schema. Only call when user approves the preview in Step 6."
|
rem/services/email/service.py
CHANGED
|
@@ -26,6 +26,9 @@ logger = logging.getLogger(__name__)
|
|
|
26
26
|
class EmailService:
|
|
27
27
|
"""Service for sending transactional emails and passwordless login."""
|
|
28
28
|
|
|
29
|
+
# Store last login code for mock mode testing
|
|
30
|
+
_last_login_code: dict[str, str] = {}
|
|
31
|
+
|
|
29
32
|
def __init__(
|
|
30
33
|
self,
|
|
31
34
|
smtp_host: str | None = None,
|
|
@@ -35,6 +38,7 @@ class EmailService:
|
|
|
35
38
|
app_password: str | None = None,
|
|
36
39
|
use_tls: bool = True,
|
|
37
40
|
login_code_expiry_minutes: int = 10,
|
|
41
|
+
mock_mode: bool | None = None,
|
|
38
42
|
):
|
|
39
43
|
"""
|
|
40
44
|
Initialize EmailService.
|
|
@@ -50,6 +54,7 @@ class EmailService:
|
|
|
50
54
|
app_password: SMTP app password
|
|
51
55
|
use_tls: Use TLS encryption
|
|
52
56
|
login_code_expiry_minutes: Login code expiry in minutes
|
|
57
|
+
mock_mode: If True, don't send real emails (log code instead)
|
|
53
58
|
"""
|
|
54
59
|
# Import settings lazily to avoid circular imports
|
|
55
60
|
from ...settings import settings
|
|
@@ -65,16 +70,31 @@ class EmailService:
|
|
|
65
70
|
or settings.email.login_code_expiry_minutes
|
|
66
71
|
)
|
|
67
72
|
|
|
68
|
-
if not
|
|
73
|
+
# Mock mode: enabled via setting or if not configured
|
|
74
|
+
if mock_mode is not None:
|
|
75
|
+
self._mock_mode = mock_mode
|
|
76
|
+
elif hasattr(settings.email, 'mock_mode'):
|
|
77
|
+
self._mock_mode = settings.email.mock_mode
|
|
78
|
+
else:
|
|
79
|
+
# Auto-enable mock mode if email is not configured
|
|
80
|
+
self._mock_mode = not self._app_password
|
|
81
|
+
|
|
82
|
+
if not self._app_password and not self._mock_mode:
|
|
69
83
|
logger.warning(
|
|
70
84
|
"Email app password not configured. "
|
|
71
85
|
"Set EMAIL__APP_PASSWORD to enable email sending."
|
|
72
86
|
)
|
|
73
87
|
|
|
88
|
+
if self._mock_mode:
|
|
89
|
+
logger.info(
|
|
90
|
+
"Email service running in MOCK MODE. "
|
|
91
|
+
"Codes will be logged but not emailed."
|
|
92
|
+
)
|
|
93
|
+
|
|
74
94
|
@property
|
|
75
95
|
def is_configured(self) -> bool:
|
|
76
|
-
"""Check if email service is properly configured."""
|
|
77
|
-
return bool(self._sender_email and self._app_password)
|
|
96
|
+
"""Check if email service is properly configured (or in mock mode)."""
|
|
97
|
+
return self._mock_mode or bool(self._sender_email and self._app_password)
|
|
78
98
|
|
|
79
99
|
def _create_smtp_connection(self) -> smtplib.SMTP:
|
|
80
100
|
"""Create and authenticate SMTP connection."""
|
|
@@ -107,6 +127,13 @@ class EmailService:
|
|
|
107
127
|
logger.error("Email service not configured. Cannot send email.")
|
|
108
128
|
return False
|
|
109
129
|
|
|
130
|
+
# Mock mode - log but don't send
|
|
131
|
+
if self._mock_mode:
|
|
132
|
+
logger.info(
|
|
133
|
+
f"[MOCK EMAIL] To: {to_email}, Subject: {template.subject}"
|
|
134
|
+
)
|
|
135
|
+
return True
|
|
136
|
+
|
|
110
137
|
try:
|
|
111
138
|
# Create message
|
|
112
139
|
msg = MIMEMultipart("alternative")
|
|
@@ -152,13 +179,29 @@ class EmailService:
|
|
|
152
179
|
"""
|
|
153
180
|
return "".join(random.choices(string.digits, k=6))
|
|
154
181
|
|
|
182
|
+
@classmethod
|
|
183
|
+
def get_mock_code(cls, email: str) -> str | None:
|
|
184
|
+
"""
|
|
185
|
+
Get the last login code sent to an email (mock mode only).
|
|
186
|
+
|
|
187
|
+
For testing purposes - retrieves the code that would have been
|
|
188
|
+
sent in mock mode.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
email: Email address to look up
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The login code or None if not found
|
|
195
|
+
"""
|
|
196
|
+
return cls._last_login_code.get(email.lower().strip())
|
|
197
|
+
|
|
155
198
|
@staticmethod
|
|
156
199
|
def generate_user_id_from_email(email: str) -> str:
|
|
157
200
|
"""
|
|
158
201
|
Generate a deterministic UUID from email address.
|
|
159
202
|
|
|
160
|
-
Uses
|
|
161
|
-
Same email always produces same UUID.
|
|
203
|
+
Uses the centralized email_to_user_id() for consistency.
|
|
204
|
+
Same email always produces same UUID (bijection).
|
|
162
205
|
|
|
163
206
|
Args:
|
|
164
207
|
email: Email address
|
|
@@ -166,7 +209,8 @@ class EmailService:
|
|
|
166
209
|
Returns:
|
|
167
210
|
UUID string
|
|
168
211
|
"""
|
|
169
|
-
|
|
212
|
+
from rem.utils.user_id import email_to_user_id
|
|
213
|
+
return email_to_user_id(email)
|
|
170
214
|
|
|
171
215
|
async def send_login_code(
|
|
172
216
|
self,
|
|
@@ -254,6 +298,15 @@ class EmailService:
|
|
|
254
298
|
if sent:
|
|
255
299
|
result["success"] = True
|
|
256
300
|
result["code_sent"] = True
|
|
301
|
+
|
|
302
|
+
# Store code for mock mode retrieval
|
|
303
|
+
if self._mock_mode:
|
|
304
|
+
EmailService._last_login_code[email] = code
|
|
305
|
+
logger.info(
|
|
306
|
+
f"[MOCK MODE] Login code for {email}: {code} "
|
|
307
|
+
f"(expires at {expires_at.isoformat()})"
|
|
308
|
+
)
|
|
309
|
+
|
|
257
310
|
logger.info(
|
|
258
311
|
f"Login code sent to {email}, "
|
|
259
312
|
f"user_id={user_id}, expires at {expires_at.isoformat()}"
|
|
@@ -323,8 +376,17 @@ class EmailService:
|
|
|
323
376
|
await user_repo.upsert(existing_user)
|
|
324
377
|
return {"allowed": True, "error": None}
|
|
325
378
|
else:
|
|
326
|
-
# New user - check if
|
|
327
|
-
|
|
379
|
+
# New user - first check if they're a subscriber (by email lookup)
|
|
380
|
+
from ...models.entities import Subscriber
|
|
381
|
+
subscriber_repo = Repository(Subscriber, db=db)
|
|
382
|
+
existing_subscriber = await subscriber_repo.find_one({"email": email})
|
|
383
|
+
|
|
384
|
+
if existing_subscriber:
|
|
385
|
+
# Subscriber exists - allow them to create account
|
|
386
|
+
# (approved field may not exist in older schemas, so just check existence)
|
|
387
|
+
logger.info(f"Subscriber {email} creating user account")
|
|
388
|
+
elif settings and hasattr(settings, 'email') and settings.email.trusted_domain_list:
|
|
389
|
+
# Not an approved subscriber - check if domain is trusted
|
|
328
390
|
if not settings.email.is_domain_trusted(email):
|
|
329
391
|
email_domain = email.split("@")[-1]
|
|
330
392
|
logger.warning(f"Untrusted domain attempted signup: {email_domain}")
|
|
@@ -341,7 +403,8 @@ class EmailService:
|
|
|
341
403
|
new_user = User(
|
|
342
404
|
id=uuid.UUID(user_id),
|
|
343
405
|
tenant_id=tenant_id,
|
|
344
|
-
|
|
406
|
+
user_id=user_id, # UUID5 hash of email (same as id)
|
|
407
|
+
name=email, # Full email as entity_key for LOOKUP
|
|
345
408
|
email=email,
|
|
346
409
|
role=user_role,
|
|
347
410
|
metadata=login_metadata,
|
|
@@ -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
|
rem/services/user_service.py
CHANGED
|
@@ -4,7 +4,8 @@ User Service - User account management.
|
|
|
4
4
|
Handles user creation, profile updates, and session linking.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from rem.utils.date_utils import utc_now
|
|
8
|
+
from rem.utils.user_id import email_to_user_id
|
|
8
9
|
from typing import Optional
|
|
9
10
|
|
|
10
11
|
from loguru import logger
|
|
@@ -51,28 +52,59 @@ class UserService:
|
|
|
51
52
|
updated = True
|
|
52
53
|
|
|
53
54
|
if updated:
|
|
54
|
-
user.updated_at =
|
|
55
|
+
user.updated_at = utc_now()
|
|
55
56
|
await self.repo.upsert(user)
|
|
56
57
|
|
|
57
58
|
return user
|
|
58
59
|
|
|
59
60
|
# Create new user
|
|
61
|
+
# id and user_id = UUID5 hash of email (deterministic bijection)
|
|
62
|
+
# name = email (entity_key for LOOKUP by email in KV store)
|
|
63
|
+
hashed_id = email_to_user_id(email)
|
|
60
64
|
user = User(
|
|
65
|
+
id=hashed_id, # Database id = hash of email
|
|
61
66
|
tenant_id=tenant_id,
|
|
62
|
-
user_id=
|
|
63
|
-
#
|
|
64
|
-
# Usually user_id is the external ID or email.
|
|
65
|
-
name=name,
|
|
67
|
+
user_id=hashed_id, # user_id = hash of email (same as id)
|
|
68
|
+
name=email, # Email as entity_key for REM LOOKUP
|
|
66
69
|
email=email,
|
|
67
70
|
tier=UserTier.FREE,
|
|
68
|
-
created_at=
|
|
69
|
-
updated_at=
|
|
71
|
+
created_at=utc_now(),
|
|
72
|
+
updated_at=utc_now(),
|
|
70
73
|
metadata={"avatar_url": avatar_url} if avatar_url else {},
|
|
71
74
|
)
|
|
72
75
|
await self.repo.upsert(user)
|
|
73
76
|
logger.info(f"Created new user: {email}")
|
|
74
77
|
return user
|
|
75
78
|
|
|
79
|
+
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
|
80
|
+
"""
|
|
81
|
+
Get a user by their UUID.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
user_id: The user's UUID
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
User if found, None otherwise
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
return await self.repo.get_by_id(user_id)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"Could not find user by id {user_id}: {e}")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
async def get_user_by_email(self, email: str) -> Optional[User]:
|
|
96
|
+
"""
|
|
97
|
+
Get a user by their email address.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
email: The user's email
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
User if found, None otherwise
|
|
104
|
+
"""
|
|
105
|
+
users = await self.repo.find(filters={"email": email}, limit=1)
|
|
106
|
+
return users[0] if users else None
|
|
107
|
+
|
|
76
108
|
async def link_anonymous_session(self, user: User, anon_id: str) -> None:
|
|
77
109
|
"""
|
|
78
110
|
Link an anonymous session ID to a user account.
|
|
@@ -88,7 +120,7 @@ class UserService:
|
|
|
88
120
|
|
|
89
121
|
# Add to list
|
|
90
122
|
user.anonymous_ids.append(anon_id)
|
|
91
|
-
user.updated_at =
|
|
123
|
+
user.updated_at = utc_now()
|
|
92
124
|
|
|
93
125
|
# Save
|
|
94
126
|
await self.repo.upsert(user)
|
rem/settings.py
CHANGED
|
@@ -77,6 +77,7 @@ class LLMSettings(BaseSettings):
|
|
|
77
77
|
LLM__ANTHROPIC_API_KEY or ANTHROPIC_API_KEY - Anthropic API key
|
|
78
78
|
LLM__EMBEDDING_PROVIDER or EMBEDDING_PROVIDER - Default embedding provider (openai)
|
|
79
79
|
LLM__EMBEDDING_MODEL or EMBEDDING_MODEL - Default embedding model name
|
|
80
|
+
LLM__DEFAULT_STRUCTURED_OUTPUT - Default structured output mode (False = streaming text)
|
|
80
81
|
"""
|
|
81
82
|
|
|
82
83
|
model_config = SettingsConfigDict(
|
|
@@ -138,6 +139,11 @@ class LLMSettings(BaseSettings):
|
|
|
138
139
|
description="Default embedding model (provider-specific model name)",
|
|
139
140
|
)
|
|
140
141
|
|
|
142
|
+
default_structured_output: bool = Field(
|
|
143
|
+
default=False,
|
|
144
|
+
description="Default structured output mode for agents. False = streaming text (easier), True = JSON schema validation",
|
|
145
|
+
)
|
|
146
|
+
|
|
141
147
|
@field_validator("openai_api_key", mode="before")
|
|
142
148
|
@classmethod
|
|
143
149
|
def validate_openai_api_key(cls, v):
|
|
@@ -1028,7 +1034,7 @@ class ChatSettings(BaseSettings):
|
|
|
1028
1034
|
- Prevents context window bloat while maintaining conversation continuity
|
|
1029
1035
|
|
|
1030
1036
|
User Context (on-demand by default):
|
|
1031
|
-
- Agent system prompt includes: "User
|
|
1037
|
+
- Agent system prompt includes: "User: {email}. To load user profile: Use REM LOOKUP \"{email}\""
|
|
1032
1038
|
- Agent decides whether to load profile based on query
|
|
1033
1039
|
- More efficient for queries that don't need personalization
|
|
1034
1040
|
|
|
@@ -1114,6 +1120,14 @@ class APISettings(BaseSettings):
|
|
|
1114
1120
|
),
|
|
1115
1121
|
)
|
|
1116
1122
|
|
|
1123
|
+
rate_limit_enabled: bool = Field(
|
|
1124
|
+
default=True,
|
|
1125
|
+
description=(
|
|
1126
|
+
"Enable rate limiting for API endpoints. "
|
|
1127
|
+
"Set to false to disable rate limiting entirely (useful for development)."
|
|
1128
|
+
),
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1117
1131
|
|
|
1118
1132
|
class ModelsSettings(BaseSettings):
|
|
1119
1133
|
"""
|
rem/sql/background_indexes.sql
CHANGED
|
@@ -21,6 +21,11 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_moments_vector_hnsw
|
|
|
21
21
|
ON embeddings_moments
|
|
22
22
|
USING hnsw (embedding vector_cosine_ops);
|
|
23
23
|
|
|
24
|
+
-- HNSW vector index for embeddings_ontologies
|
|
25
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_ontologies_vector_hnsw
|
|
26
|
+
ON embeddings_ontologies
|
|
27
|
+
USING hnsw (embedding vector_cosine_ops);
|
|
28
|
+
|
|
24
29
|
-- HNSW vector index for embeddings_ontology_configs
|
|
25
30
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_ontology_configs_vector_hnsw
|
|
26
31
|
ON embeddings_ontology_configs
|