remdb 0.3.141__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 +108 -6
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/experiments.py +32 -46
- rem/models/core/experiment.py +4 -14
- 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 +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 +29 -0
- rem/settings.py +199 -4
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/files.py +157 -1
- {remdb-0.3.141.dist-info → remdb-0.3.163.dist-info}/METADATA +7 -5
- {remdb-0.3.141.dist-info → remdb-0.3.163.dist-info}/RECORD +44 -35
- {remdb-0.3.141.dist-info → remdb-0.3.163.dist-info}/WHEEL +0 -0
- {remdb-0.3.141.dist-info → remdb-0.3.163.dist-info}/entry_points.txt +0 -0
rem/agentic/agents/__init__.py
CHANGED
|
@@ -6,6 +6,8 @@ Use create_agent_from_schema_file() to instantiate agents.
|
|
|
6
6
|
|
|
7
7
|
The SSE Simulator is a special programmatic "agent" that generates
|
|
8
8
|
scripted SSE events for testing and demonstration - it doesn't use an LLM.
|
|
9
|
+
|
|
10
|
+
Agent Manager provides functions for saving/loading user-created agents.
|
|
9
11
|
"""
|
|
10
12
|
|
|
11
13
|
from .sse_simulator import (
|
|
@@ -14,9 +16,23 @@ from .sse_simulator import (
|
|
|
14
16
|
stream_error_demo,
|
|
15
17
|
)
|
|
16
18
|
|
|
19
|
+
from .agent_manager import (
|
|
20
|
+
save_agent,
|
|
21
|
+
get_agent,
|
|
22
|
+
list_agents,
|
|
23
|
+
delete_agent,
|
|
24
|
+
build_agent_spec,
|
|
25
|
+
)
|
|
26
|
+
|
|
17
27
|
__all__ = [
|
|
18
28
|
# SSE Simulator (programmatic, no LLM)
|
|
19
29
|
"stream_simulator_events",
|
|
20
30
|
"stream_minimal_demo",
|
|
21
31
|
"stream_error_demo",
|
|
32
|
+
# Agent Manager
|
|
33
|
+
"save_agent",
|
|
34
|
+
"get_agent",
|
|
35
|
+
"list_agents",
|
|
36
|
+
"delete_agent",
|
|
37
|
+
"build_agent_spec",
|
|
22
38
|
]
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Manager - Save, load, and manage user-created agents.
|
|
3
|
+
|
|
4
|
+
This module provides the core functionality for persisting agent schemas
|
|
5
|
+
to the database with user scoping.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from rem.agentic.agents.agent_manager import save_agent, get_agent, list_agents
|
|
9
|
+
|
|
10
|
+
# Save an agent
|
|
11
|
+
result = await save_agent(
|
|
12
|
+
name="my-assistant",
|
|
13
|
+
description="You are a helpful assistant.",
|
|
14
|
+
user_id="user-123"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# Get an agent
|
|
18
|
+
agent = await get_agent("my-assistant", user_id="user-123")
|
|
19
|
+
|
|
20
|
+
# List user's agents
|
|
21
|
+
agents = await list_agents(user_id="user-123")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
from loguru import logger
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DEFAULT_TOOLS = ["search_rem", "register_metadata"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_agent_spec(
|
|
32
|
+
name: str,
|
|
33
|
+
description: str,
|
|
34
|
+
properties: dict[str, Any] | None = None,
|
|
35
|
+
required: list[str] | None = None,
|
|
36
|
+
tools: list[str] | None = None,
|
|
37
|
+
tags: list[str] | None = None,
|
|
38
|
+
version: str = "1.0.0",
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Build a valid agent schema spec.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
name: Agent name in kebab-case
|
|
45
|
+
description: System prompt for the agent
|
|
46
|
+
properties: Output schema properties
|
|
47
|
+
required: Required property names
|
|
48
|
+
tools: Tool names (defaults to search_rem, register_metadata)
|
|
49
|
+
tags: Categorization tags
|
|
50
|
+
version: Semantic version
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Valid agent schema spec dict
|
|
54
|
+
"""
|
|
55
|
+
# Default properties
|
|
56
|
+
if properties is None:
|
|
57
|
+
properties = {
|
|
58
|
+
"answer": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Natural language response to the user"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Default required
|
|
65
|
+
if required is None:
|
|
66
|
+
required = ["answer"]
|
|
67
|
+
|
|
68
|
+
# Default tools
|
|
69
|
+
if tools is None:
|
|
70
|
+
tools = DEFAULT_TOOLS.copy()
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"description": description,
|
|
75
|
+
"properties": properties,
|
|
76
|
+
"required": required,
|
|
77
|
+
"json_schema_extra": {
|
|
78
|
+
"kind": "agent",
|
|
79
|
+
"name": name,
|
|
80
|
+
"version": version,
|
|
81
|
+
"tags": tags or [],
|
|
82
|
+
"tools": [{"name": t, "description": f"Tool: {t}"} for t in tools],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def save_agent(
|
|
88
|
+
name: str,
|
|
89
|
+
description: str,
|
|
90
|
+
user_id: str,
|
|
91
|
+
properties: dict[str, Any] | None = None,
|
|
92
|
+
required: list[str] | None = None,
|
|
93
|
+
tools: list[str] | None = None,
|
|
94
|
+
tags: list[str] | None = None,
|
|
95
|
+
version: str = "1.0.0",
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""
|
|
98
|
+
Save an agent schema to the database.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
name: Agent name in kebab-case (e.g., "code-reviewer")
|
|
102
|
+
description: The agent's system prompt
|
|
103
|
+
user_id: User identifier for scoping
|
|
104
|
+
properties: Output schema properties
|
|
105
|
+
required: Required property names
|
|
106
|
+
tools: Tool names
|
|
107
|
+
tags: Categorization tags
|
|
108
|
+
version: Semantic version
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dict with status, agent_name, version, message
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
RuntimeError: If database is not available
|
|
115
|
+
"""
|
|
116
|
+
from rem.models.entities import Schema
|
|
117
|
+
from rem.services.postgres import get_postgres_service
|
|
118
|
+
|
|
119
|
+
# Build the spec
|
|
120
|
+
spec = build_agent_spec(
|
|
121
|
+
name=name,
|
|
122
|
+
description=description,
|
|
123
|
+
properties=properties,
|
|
124
|
+
required=required,
|
|
125
|
+
tools=tools,
|
|
126
|
+
tags=tags,
|
|
127
|
+
version=version,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Create Schema entity (user-scoped)
|
|
131
|
+
schema_entity = Schema(
|
|
132
|
+
tenant_id=user_id,
|
|
133
|
+
user_id=user_id,
|
|
134
|
+
name=name,
|
|
135
|
+
spec=spec,
|
|
136
|
+
category="agent",
|
|
137
|
+
metadata={
|
|
138
|
+
"version": version,
|
|
139
|
+
"tags": tags or [],
|
|
140
|
+
"created_via": "agent_manager",
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Save to database
|
|
145
|
+
postgres = get_postgres_service()
|
|
146
|
+
if not postgres:
|
|
147
|
+
raise RuntimeError("Database not available")
|
|
148
|
+
|
|
149
|
+
await postgres.connect()
|
|
150
|
+
try:
|
|
151
|
+
await postgres.batch_upsert(
|
|
152
|
+
records=[schema_entity],
|
|
153
|
+
model=Schema,
|
|
154
|
+
table_name="schemas",
|
|
155
|
+
entity_key_field="name",
|
|
156
|
+
generate_embeddings=False,
|
|
157
|
+
)
|
|
158
|
+
logger.info(f"✅ Agent saved: {name} (user={user_id}, version={version})")
|
|
159
|
+
finally:
|
|
160
|
+
await postgres.disconnect()
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"status": "success",
|
|
164
|
+
"agent_name": name,
|
|
165
|
+
"version": version,
|
|
166
|
+
"message": f"Agent '{name}' saved successfully.",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def get_agent(
|
|
171
|
+
name: str,
|
|
172
|
+
user_id: str,
|
|
173
|
+
) -> dict[str, Any] | None:
|
|
174
|
+
"""
|
|
175
|
+
Get an agent schema by name.
|
|
176
|
+
|
|
177
|
+
Checks user's schemas first, then falls back to system schemas.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
name: Agent name
|
|
181
|
+
user_id: User identifier
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Agent spec dict if found, None otherwise
|
|
185
|
+
"""
|
|
186
|
+
from rem.services.postgres import get_postgres_service
|
|
187
|
+
|
|
188
|
+
postgres = get_postgres_service()
|
|
189
|
+
if not postgres:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
await postgres.connect()
|
|
193
|
+
try:
|
|
194
|
+
query = """
|
|
195
|
+
SELECT spec FROM schemas
|
|
196
|
+
WHERE LOWER(name) = LOWER($1)
|
|
197
|
+
AND category = 'agent'
|
|
198
|
+
AND (user_id = $2 OR user_id IS NULL OR tenant_id = 'system')
|
|
199
|
+
ORDER BY CASE WHEN user_id = $2 THEN 0 ELSE 1 END
|
|
200
|
+
LIMIT 1
|
|
201
|
+
"""
|
|
202
|
+
row = await postgres.fetchrow(query, name, user_id)
|
|
203
|
+
if row:
|
|
204
|
+
return row["spec"]
|
|
205
|
+
return None
|
|
206
|
+
finally:
|
|
207
|
+
await postgres.disconnect()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def list_agents(
|
|
211
|
+
user_id: str,
|
|
212
|
+
include_system: bool = True,
|
|
213
|
+
) -> list[dict[str, Any]]:
|
|
214
|
+
"""
|
|
215
|
+
List available agents for a user.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
user_id: User identifier
|
|
219
|
+
include_system: Include system agents
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of agent metadata dicts
|
|
223
|
+
"""
|
|
224
|
+
from rem.services.postgres import get_postgres_service
|
|
225
|
+
|
|
226
|
+
postgres = get_postgres_service()
|
|
227
|
+
if not postgres:
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
await postgres.connect()
|
|
231
|
+
try:
|
|
232
|
+
if include_system:
|
|
233
|
+
query = """
|
|
234
|
+
SELECT name, metadata, user_id, tenant_id
|
|
235
|
+
FROM schemas
|
|
236
|
+
WHERE category = 'agent'
|
|
237
|
+
AND (user_id = $1 OR user_id IS NULL OR tenant_id = 'system')
|
|
238
|
+
ORDER BY name
|
|
239
|
+
"""
|
|
240
|
+
rows = await postgres.fetch(query, user_id)
|
|
241
|
+
else:
|
|
242
|
+
query = """
|
|
243
|
+
SELECT name, metadata, user_id, tenant_id
|
|
244
|
+
FROM schemas
|
|
245
|
+
WHERE category = 'agent'
|
|
246
|
+
AND user_id = $1
|
|
247
|
+
ORDER BY name
|
|
248
|
+
"""
|
|
249
|
+
rows = await postgres.fetch(query, user_id)
|
|
250
|
+
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
"name": row["name"],
|
|
254
|
+
"version": row["metadata"].get("version", "1.0.0") if row["metadata"] else "1.0.0",
|
|
255
|
+
"tags": row["metadata"].get("tags", []) if row["metadata"] else [],
|
|
256
|
+
"is_system": row["tenant_id"] == "system" or row["user_id"] is None,
|
|
257
|
+
}
|
|
258
|
+
for row in rows
|
|
259
|
+
]
|
|
260
|
+
finally:
|
|
261
|
+
await postgres.disconnect()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def delete_agent(
|
|
265
|
+
name: str,
|
|
266
|
+
user_id: str,
|
|
267
|
+
) -> dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Delete a user's agent.
|
|
270
|
+
|
|
271
|
+
Only allows deleting user-owned agents, not system agents.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
name: Agent name
|
|
275
|
+
user_id: User identifier
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Dict with status and message
|
|
279
|
+
"""
|
|
280
|
+
from rem.services.postgres import get_postgres_service
|
|
281
|
+
|
|
282
|
+
postgres = get_postgres_service()
|
|
283
|
+
if not postgres:
|
|
284
|
+
raise RuntimeError("Database not available")
|
|
285
|
+
|
|
286
|
+
await postgres.connect()
|
|
287
|
+
try:
|
|
288
|
+
# Only delete user's own agents
|
|
289
|
+
query = """
|
|
290
|
+
DELETE FROM schemas
|
|
291
|
+
WHERE LOWER(name) = LOWER($1)
|
|
292
|
+
AND category = 'agent'
|
|
293
|
+
AND user_id = $2
|
|
294
|
+
RETURNING name
|
|
295
|
+
"""
|
|
296
|
+
row = await postgres.fetchrow(query, name, user_id)
|
|
297
|
+
|
|
298
|
+
if row:
|
|
299
|
+
logger.info(f"🗑️ Agent deleted: {name} (user={user_id})")
|
|
300
|
+
return {
|
|
301
|
+
"status": "success",
|
|
302
|
+
"message": f"Agent '{name}' deleted.",
|
|
303
|
+
}
|
|
304
|
+
else:
|
|
305
|
+
return {
|
|
306
|
+
"status": "error",
|
|
307
|
+
"message": f"Agent '{name}' not found or not owned by you.",
|
|
308
|
+
}
|
|
309
|
+
finally:
|
|
310
|
+
await postgres.disconnect()
|
rem/agentic/context.py
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
Agent execution context and configuration.
|
|
3
3
|
|
|
4
4
|
Design pattern for session context that can be constructed from:
|
|
5
|
+
- FastAPI Request object (preferred - extracts user from JWT via request.state)
|
|
5
6
|
- HTTP headers (X-User-Id, X-Session-Id, X-Model-Name, X-Is-Eval, etc.)
|
|
6
7
|
- Direct instantiation for testing/CLI
|
|
7
8
|
|
|
9
|
+
User ID Sources (in priority order):
|
|
10
|
+
1. request.state.user.id - From JWT token validated by auth middleware (SECURE)
|
|
11
|
+
2. X-User-Id header - Fallback for backwards compatibility (less secure)
|
|
12
|
+
|
|
8
13
|
Headers Mapping:
|
|
9
|
-
X-User-Id → context.user_id
|
|
10
14
|
X-Tenant-Id → context.tenant_id (default: "default")
|
|
11
15
|
X-Session-Id → context.session_id
|
|
12
16
|
X-Agent-Schema → context.agent_schema_uri (default: "rem")
|
|
@@ -128,13 +132,87 @@ class AgentContext(BaseModel):
|
|
|
128
132
|
logger.debug(f"No user_id from {source}, using None (anonymous/shared data)")
|
|
129
133
|
return None
|
|
130
134
|
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_request(cls, request: "Request") -> "AgentContext":
|
|
137
|
+
"""
|
|
138
|
+
Construct AgentContext from a FastAPI Request object.
|
|
139
|
+
|
|
140
|
+
This is the PREFERRED method for API endpoints. It extracts user_id
|
|
141
|
+
from the authenticated user in request.state (set by auth middleware
|
|
142
|
+
from JWT token), which is more secure than trusting X-User-Id header.
|
|
143
|
+
|
|
144
|
+
Priority for user_id:
|
|
145
|
+
1. request.state.user.id - From validated JWT token (SECURE)
|
|
146
|
+
2. X-User-Id header - Fallback for backwards compatibility
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
request: FastAPI Request object
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
AgentContext with user from JWT and other values from headers
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
@app.post("/api/v1/chat/completions")
|
|
156
|
+
async def chat(request: Request, body: ChatRequest):
|
|
157
|
+
context = AgentContext.from_request(request)
|
|
158
|
+
# context.user_id is from JWT, not header
|
|
159
|
+
"""
|
|
160
|
+
from typing import TYPE_CHECKING
|
|
161
|
+
if TYPE_CHECKING:
|
|
162
|
+
from starlette.requests import Request
|
|
163
|
+
|
|
164
|
+
# Get headers dict
|
|
165
|
+
headers = dict(request.headers)
|
|
166
|
+
normalized = {k.lower(): v for k, v in headers.items()}
|
|
167
|
+
|
|
168
|
+
# Extract user_id from authenticated user (JWT) - this is the source of truth
|
|
169
|
+
user_id = None
|
|
170
|
+
tenant_id = "default"
|
|
171
|
+
|
|
172
|
+
if hasattr(request, "state"):
|
|
173
|
+
user = getattr(request.state, "user", None)
|
|
174
|
+
if user and isinstance(user, dict):
|
|
175
|
+
user_id = user.get("id")
|
|
176
|
+
# Also get tenant_id from authenticated user if available
|
|
177
|
+
if user.get("tenant_id"):
|
|
178
|
+
tenant_id = user.get("tenant_id")
|
|
179
|
+
if user_id:
|
|
180
|
+
logger.debug(f"User ID from JWT: {user_id}")
|
|
181
|
+
|
|
182
|
+
# Fallback to X-User-Id header if no authenticated user
|
|
183
|
+
if not user_id:
|
|
184
|
+
user_id = normalized.get("x-user-id")
|
|
185
|
+
if user_id:
|
|
186
|
+
logger.debug(f"User ID from X-User-Id header (fallback): {user_id}")
|
|
187
|
+
|
|
188
|
+
# Override tenant_id from header if provided
|
|
189
|
+
header_tenant = normalized.get("x-tenant-id")
|
|
190
|
+
if header_tenant:
|
|
191
|
+
tenant_id = header_tenant
|
|
192
|
+
|
|
193
|
+
# Parse X-Is-Eval header
|
|
194
|
+
is_eval_str = normalized.get("x-is-eval", "").lower()
|
|
195
|
+
is_eval = is_eval_str in ("true", "1", "yes")
|
|
196
|
+
|
|
197
|
+
return cls(
|
|
198
|
+
user_id=user_id,
|
|
199
|
+
tenant_id=tenant_id,
|
|
200
|
+
session_id=normalized.get("x-session-id"),
|
|
201
|
+
default_model=normalized.get("x-model-name") or settings.llm.default_model,
|
|
202
|
+
agent_schema_uri=normalized.get("x-agent-schema"),
|
|
203
|
+
is_eval=is_eval,
|
|
204
|
+
)
|
|
205
|
+
|
|
131
206
|
@classmethod
|
|
132
207
|
def from_headers(cls, headers: dict[str, str]) -> "AgentContext":
|
|
133
208
|
"""
|
|
134
|
-
Construct AgentContext from HTTP headers.
|
|
209
|
+
Construct AgentContext from HTTP headers dict.
|
|
210
|
+
|
|
211
|
+
NOTE: Prefer from_request() for API endpoints as it extracts user_id
|
|
212
|
+
from the validated JWT token in request.state, which is more secure.
|
|
135
213
|
|
|
136
214
|
Reads standard headers:
|
|
137
|
-
- X-User-Id: User identifier
|
|
215
|
+
- X-User-Id: User identifier (fallback - prefer JWT)
|
|
138
216
|
- X-Tenant-Id: Tenant identifier
|
|
139
217
|
- X-Session-Id: Session identifier
|
|
140
218
|
- X-Model-Name: Model override
|
rem/agentic/context_builder.py
CHANGED
|
@@ -103,6 +103,7 @@ class ContextBuilder:
|
|
|
103
103
|
headers: dict[str, str],
|
|
104
104
|
new_messages: list[dict[str, str]] | None = None,
|
|
105
105
|
db: PostgresService | None = None,
|
|
106
|
+
user_id: str | None = None,
|
|
106
107
|
) -> tuple[AgentContext, list[ContextMessage]]:
|
|
107
108
|
"""
|
|
108
109
|
Build complete context from HTTP headers.
|
|
@@ -125,6 +126,7 @@ class ContextBuilder:
|
|
|
125
126
|
headers: HTTP request headers (case-insensitive)
|
|
126
127
|
new_messages: New messages from current request
|
|
127
128
|
db: Optional PostgresService (creates if None)
|
|
129
|
+
user_id: Override user_id from JWT token (takes precedence over X-User-Id header)
|
|
128
130
|
|
|
129
131
|
Returns:
|
|
130
132
|
Tuple of (AgentContext, messages list)
|
|
@@ -147,6 +149,17 @@ class ContextBuilder:
|
|
|
147
149
|
# Extract AgentContext from headers
|
|
148
150
|
context = AgentContext.from_headers(headers)
|
|
149
151
|
|
|
152
|
+
# Override user_id if provided (from JWT token - takes precedence over header)
|
|
153
|
+
if user_id is not None:
|
|
154
|
+
context = AgentContext(
|
|
155
|
+
user_id=user_id,
|
|
156
|
+
tenant_id=context.tenant_id,
|
|
157
|
+
session_id=context.session_id,
|
|
158
|
+
default_model=context.default_model,
|
|
159
|
+
agent_schema_uri=context.agent_schema_uri,
|
|
160
|
+
is_eval=context.is_eval,
|
|
161
|
+
)
|
|
162
|
+
|
|
150
163
|
# Initialize DB if not provided and needed (for user context or session history)
|
|
151
164
|
close_db = False
|
|
152
165
|
if db is None and (settings.chat.auto_inject_user_context or context.session_id):
|
|
@@ -184,13 +197,15 @@ class ContextBuilder:
|
|
|
184
197
|
# Add system context hint
|
|
185
198
|
messages.append(ContextMessage(role="system", content=context_hint))
|
|
186
199
|
|
|
187
|
-
# ALWAYS load session history (if session_id provided)
|
|
200
|
+
# ALWAYS load session history (if session_id provided)
|
|
201
|
+
# - Long assistant messages are compressed on load with REM LOOKUP hints
|
|
202
|
+
# - Tool messages are never compressed (contain structured metadata)
|
|
188
203
|
if context.session_id and settings.postgres.enabled:
|
|
189
204
|
store = SessionMessageStore(user_id=context.user_id or "default")
|
|
190
205
|
session_history = await store.load_session_messages(
|
|
191
206
|
session_id=context.session_id,
|
|
192
207
|
user_id=context.user_id,
|
|
193
|
-
|
|
208
|
+
compress_on_load=True, # Compress long assistant messages
|
|
194
209
|
)
|
|
195
210
|
|
|
196
211
|
# Convert to ContextMessage format
|
|
@@ -202,7 +217,7 @@ class ContextBuilder:
|
|
|
202
217
|
)
|
|
203
218
|
)
|
|
204
219
|
|
|
205
|
-
logger.debug(f"Loaded {len(session_history)}
|
|
220
|
+
logger.debug(f"Loaded {len(session_history)} messages for session {context.session_id}")
|
|
206
221
|
|
|
207
222
|
# Add new messages from request
|
|
208
223
|
if new_messages:
|
rem/api/deps.py
CHANGED
|
@@ -147,7 +147,6 @@ def is_admin(user: dict | None) -> bool:
|
|
|
147
147
|
async def get_user_filter(
|
|
148
148
|
request: Request,
|
|
149
149
|
x_user_id: str | None = None,
|
|
150
|
-
x_tenant_id: str = "default",
|
|
151
150
|
) -> dict[str, Any]:
|
|
152
151
|
"""
|
|
153
152
|
Get user-scoped filter dict for database queries.
|
|
@@ -158,7 +157,6 @@ async def get_user_filter(
|
|
|
158
157
|
Args:
|
|
159
158
|
request: FastAPI request
|
|
160
159
|
x_user_id: Optional user_id filter (admin only for cross-user)
|
|
161
|
-
x_tenant_id: Tenant ID for multi-tenancy
|
|
162
160
|
|
|
163
161
|
Returns:
|
|
164
162
|
Filter dict with appropriate user_id constraint
|
|
@@ -169,7 +167,7 @@ async def get_user_filter(
|
|
|
169
167
|
return await repo.find(filters)
|
|
170
168
|
"""
|
|
171
169
|
user = get_current_user(request)
|
|
172
|
-
filters: dict[str, Any] = {
|
|
170
|
+
filters: dict[str, Any] = {}
|
|
173
171
|
|
|
174
172
|
if is_admin(user):
|
|
175
173
|
# Admin can filter by any user or see all
|
|
@@ -185,8 +183,8 @@ async def get_user_filter(
|
|
|
185
183
|
f"User {user.get('email')} attempted to filter by user_id={x_user_id}"
|
|
186
184
|
)
|
|
187
185
|
else:
|
|
188
|
-
# Anonymous:
|
|
189
|
-
#
|
|
186
|
+
# Anonymous: use anonymous tracking ID
|
|
187
|
+
# Note: user_id should come from JWT, not from parameters
|
|
190
188
|
anon_id = getattr(request.state, "anon_id", None)
|
|
191
189
|
if anon_id:
|
|
192
190
|
filters["user_id"] = f"anon:{anon_id}"
|
rem/api/main.py
CHANGED
|
@@ -149,19 +149,38 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
149
149
|
client_host = request.client.host if request.client else "unknown"
|
|
150
150
|
user_agent = request.headers.get('user-agent', 'unknown')[:100]
|
|
151
151
|
|
|
152
|
+
# Extract auth info for logging (first 8 chars of token for debugging)
|
|
153
|
+
auth_header = request.headers.get('authorization', '')
|
|
154
|
+
auth_preview = ""
|
|
155
|
+
if auth_header.startswith('Bearer '):
|
|
156
|
+
token = auth_header[7:]
|
|
157
|
+
auth_preview = f"Bearer {token[:8]}..." if len(token) > 8 else f"Bearer {token}"
|
|
158
|
+
|
|
152
159
|
# Process request
|
|
153
160
|
response = await call_next(request)
|
|
154
161
|
|
|
162
|
+
# Extract user info set by auth middleware (after processing)
|
|
163
|
+
user = getattr(request.state, "user", None)
|
|
164
|
+
user_id = user.get("id", "none")[:12] if user else "anon"
|
|
165
|
+
user_email = user.get("email", "") if user else ""
|
|
166
|
+
|
|
155
167
|
# Determine log level based on path AND response status
|
|
156
168
|
duration_ms = (time.time() - start_time) * 1000
|
|
157
169
|
use_debug = self._should_log_at_debug(path, response.status_code)
|
|
158
170
|
log_fn = logger.debug if use_debug else logger.info
|
|
159
171
|
|
|
160
|
-
#
|
|
172
|
+
# Build user info string
|
|
173
|
+
user_info = f"user={user_id}"
|
|
174
|
+
if user_email:
|
|
175
|
+
user_info += f" ({user_email})"
|
|
176
|
+
if auth_preview:
|
|
177
|
+
user_info += f" | auth={auth_preview}"
|
|
178
|
+
|
|
179
|
+
# Log request and response together with auth info
|
|
161
180
|
log_fn(
|
|
162
181
|
f"→ REQUEST: {request.method} {path} | "
|
|
163
182
|
f"Client: {client_host} | "
|
|
164
|
-
f"
|
|
183
|
+
f"{user_info}"
|
|
165
184
|
)
|
|
166
185
|
log_fn(
|
|
167
186
|
f"← RESPONSE: {request.method} {path} | "
|
|
@@ -304,7 +323,7 @@ def create_app() -> FastAPI:
|
|
|
304
323
|
app.add_middleware(
|
|
305
324
|
AuthMiddleware,
|
|
306
325
|
protected_paths=["/api/v1"],
|
|
307
|
-
excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth"],
|
|
326
|
+
excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth", "/api/v1/slack"],
|
|
308
327
|
# Allow anonymous when auth is disabled, otherwise use setting
|
|
309
328
|
allow_anonymous=(not settings.auth.enabled) or settings.auth.allow_anonymous,
|
|
310
329
|
# MCP requires auth only when auth is fully enabled
|
rem/api/mcp_router/server.py
CHANGED
|
@@ -182,6 +182,7 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
182
182
|
list_schema,
|
|
183
183
|
read_resource,
|
|
184
184
|
register_metadata,
|
|
185
|
+
save_agent,
|
|
185
186
|
search_rem,
|
|
186
187
|
)
|
|
187
188
|
|
|
@@ -191,6 +192,7 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
191
192
|
mcp.tool()(register_metadata)
|
|
192
193
|
mcp.tool()(list_schema)
|
|
193
194
|
mcp.tool()(get_schema)
|
|
195
|
+
mcp.tool()(save_agent)
|
|
194
196
|
|
|
195
197
|
# File ingestion tool (with local path support for local servers)
|
|
196
198
|
# Wrap to inject is_local parameter
|