remdb 0.3.146__py3-none-any.whl → 0.3.181__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 +311 -0
- rem/agentic/context.py +81 -3
- rem/agentic/context_builder.py +36 -9
- 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 +3 -5
- rem/api/main.py +22 -3
- rem/api/mcp_router/resources.py +75 -14
- rem/api/mcp_router/server.py +28 -23
- rem/api/mcp_router/tools.py +177 -2
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/auth.py +352 -6
- rem/api/routers/chat/completions.py +5 -3
- rem/api/routers/chat/streaming.py +95 -22
- rem/api/routers/messages.py +24 -15
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +70 -30
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/ask.py +1 -1
- rem/cli/commands/db.py +118 -54
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/ontology.py +93 -101
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -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 +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/worker.py +26 -12
- 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/register_type.py +1 -1
- rem/services/postgres/repository.py +37 -25
- rem/services/postgres/schema_generator.py +5 -5
- rem/services/postgres/sql_builder.py +6 -5
- rem/services/session/compression.py +113 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +41 -9
- rem/settings.py +182 -1
- rem/sql/background_indexes.sql +5 -0
- rem/sql/migrations/001_install.sql +33 -4
- rem/sql/migrations/002_install_models.sql +204 -186
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/model_helpers.py +101 -0
- rem/utils/schema_loader.py +45 -7
- {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/METADATA +1 -1
- {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/RECORD +57 -48
- {remdb-0.3.146.dist-info → remdb-0.3.181.dist-info}/WHEEL +0 -0
- {remdb-0.3.146.dist-info → remdb-0.3.181.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,311 @@
|
|
|
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
|
+
# Note: tenant_id defaults to "default" for anonymous users
|
|
132
|
+
schema_entity = Schema(
|
|
133
|
+
tenant_id=user_id or "default",
|
|
134
|
+
user_id=user_id,
|
|
135
|
+
name=name,
|
|
136
|
+
spec=spec,
|
|
137
|
+
category="agent",
|
|
138
|
+
metadata={
|
|
139
|
+
"version": version,
|
|
140
|
+
"tags": tags or [],
|
|
141
|
+
"created_via": "agent_manager",
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Save to database
|
|
146
|
+
postgres = get_postgres_service()
|
|
147
|
+
if not postgres:
|
|
148
|
+
raise RuntimeError("Database not available")
|
|
149
|
+
|
|
150
|
+
await postgres.connect()
|
|
151
|
+
try:
|
|
152
|
+
await postgres.batch_upsert(
|
|
153
|
+
records=[schema_entity],
|
|
154
|
+
model=Schema,
|
|
155
|
+
table_name="schemas",
|
|
156
|
+
entity_key_field="name",
|
|
157
|
+
generate_embeddings=False,
|
|
158
|
+
)
|
|
159
|
+
logger.info(f"✅ Agent saved: {name} (user={user_id}, version={version})")
|
|
160
|
+
finally:
|
|
161
|
+
await postgres.disconnect()
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"status": "success",
|
|
165
|
+
"agent_name": name,
|
|
166
|
+
"version": version,
|
|
167
|
+
"message": f"Agent '{name}' saved successfully.",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def get_agent(
|
|
172
|
+
name: str,
|
|
173
|
+
user_id: str,
|
|
174
|
+
) -> dict[str, Any] | None:
|
|
175
|
+
"""
|
|
176
|
+
Get an agent schema by name.
|
|
177
|
+
|
|
178
|
+
Checks user's schemas first, then falls back to system schemas.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
name: Agent name
|
|
182
|
+
user_id: User identifier
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Agent spec dict if found, None otherwise
|
|
186
|
+
"""
|
|
187
|
+
from rem.services.postgres import get_postgres_service
|
|
188
|
+
|
|
189
|
+
postgres = get_postgres_service()
|
|
190
|
+
if not postgres:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
await postgres.connect()
|
|
194
|
+
try:
|
|
195
|
+
query = """
|
|
196
|
+
SELECT spec FROM schemas
|
|
197
|
+
WHERE LOWER(name) = LOWER($1)
|
|
198
|
+
AND category = 'agent'
|
|
199
|
+
AND (user_id = $2 OR user_id IS NULL OR tenant_id = 'system')
|
|
200
|
+
ORDER BY CASE WHEN user_id = $2 THEN 0 ELSE 1 END
|
|
201
|
+
LIMIT 1
|
|
202
|
+
"""
|
|
203
|
+
row = await postgres.fetchrow(query, name, user_id)
|
|
204
|
+
if row:
|
|
205
|
+
return row["spec"]
|
|
206
|
+
return None
|
|
207
|
+
finally:
|
|
208
|
+
await postgres.disconnect()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def list_agents(
|
|
212
|
+
user_id: str,
|
|
213
|
+
include_system: bool = True,
|
|
214
|
+
) -> list[dict[str, Any]]:
|
|
215
|
+
"""
|
|
216
|
+
List available agents for a user.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
user_id: User identifier
|
|
220
|
+
include_system: Include system agents
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
List of agent metadata dicts
|
|
224
|
+
"""
|
|
225
|
+
from rem.services.postgres import get_postgres_service
|
|
226
|
+
|
|
227
|
+
postgres = get_postgres_service()
|
|
228
|
+
if not postgres:
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
await postgres.connect()
|
|
232
|
+
try:
|
|
233
|
+
if include_system:
|
|
234
|
+
query = """
|
|
235
|
+
SELECT name, metadata, user_id, tenant_id
|
|
236
|
+
FROM schemas
|
|
237
|
+
WHERE category = 'agent'
|
|
238
|
+
AND (user_id = $1 OR user_id IS NULL OR tenant_id = 'system')
|
|
239
|
+
ORDER BY name
|
|
240
|
+
"""
|
|
241
|
+
rows = await postgres.fetch(query, user_id)
|
|
242
|
+
else:
|
|
243
|
+
query = """
|
|
244
|
+
SELECT name, metadata, user_id, tenant_id
|
|
245
|
+
FROM schemas
|
|
246
|
+
WHERE category = 'agent'
|
|
247
|
+
AND user_id = $1
|
|
248
|
+
ORDER BY name
|
|
249
|
+
"""
|
|
250
|
+
rows = await postgres.fetch(query, user_id)
|
|
251
|
+
|
|
252
|
+
return [
|
|
253
|
+
{
|
|
254
|
+
"name": row["name"],
|
|
255
|
+
"version": row["metadata"].get("version", "1.0.0") if row["metadata"] else "1.0.0",
|
|
256
|
+
"tags": row["metadata"].get("tags", []) if row["metadata"] else [],
|
|
257
|
+
"is_system": row["tenant_id"] == "system" or row["user_id"] is None,
|
|
258
|
+
}
|
|
259
|
+
for row in rows
|
|
260
|
+
]
|
|
261
|
+
finally:
|
|
262
|
+
await postgres.disconnect()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def delete_agent(
|
|
266
|
+
name: str,
|
|
267
|
+
user_id: str,
|
|
268
|
+
) -> dict[str, Any]:
|
|
269
|
+
"""
|
|
270
|
+
Delete a user's agent.
|
|
271
|
+
|
|
272
|
+
Only allows deleting user-owned agents, not system agents.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
name: Agent name
|
|
276
|
+
user_id: User identifier
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dict with status and message
|
|
280
|
+
"""
|
|
281
|
+
from rem.services.postgres import get_postgres_service
|
|
282
|
+
|
|
283
|
+
postgres = get_postgres_service()
|
|
284
|
+
if not postgres:
|
|
285
|
+
raise RuntimeError("Database not available")
|
|
286
|
+
|
|
287
|
+
await postgres.connect()
|
|
288
|
+
try:
|
|
289
|
+
# Only delete user's own agents
|
|
290
|
+
query = """
|
|
291
|
+
DELETE FROM schemas
|
|
292
|
+
WHERE LOWER(name) = LOWER($1)
|
|
293
|
+
AND category = 'agent'
|
|
294
|
+
AND user_id = $2
|
|
295
|
+
RETURNING name
|
|
296
|
+
"""
|
|
297
|
+
row = await postgres.fetchrow(query, name, user_id)
|
|
298
|
+
|
|
299
|
+
if row:
|
|
300
|
+
logger.info(f"🗑️ Agent deleted: {name} (user={user_id})")
|
|
301
|
+
return {
|
|
302
|
+
"status": "success",
|
|
303
|
+
"message": f"Agent '{name}' deleted.",
|
|
304
|
+
}
|
|
305
|
+
else:
|
|
306
|
+
return {
|
|
307
|
+
"status": "error",
|
|
308
|
+
"message": f"Agent '{name}' not found or not owned by you.",
|
|
309
|
+
}
|
|
310
|
+
finally:
|
|
311
|
+
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
|
@@ -12,7 +12,7 @@ User Context (on-demand by default):
|
|
|
12
12
|
- System message includes REM LOOKUP hint for user profile
|
|
13
13
|
- Agent decides whether to load profile based on query
|
|
14
14
|
- More efficient for queries that don't need personalization
|
|
15
|
-
- Example: "User
|
|
15
|
+
- Example: "User: sarah@example.com. To load user profile: Use REM LOOKUP \"sarah@example.com\""
|
|
16
16
|
|
|
17
17
|
User Context (auto-inject when enabled):
|
|
18
18
|
- Set CHAT__AUTO_INJECT_USER_CONTEXT=true
|
|
@@ -40,7 +40,7 @@ Usage (on-demand, default):
|
|
|
40
40
|
|
|
41
41
|
# Messages list structure (on-demand):
|
|
42
42
|
# [
|
|
43
|
-
# {"role": "system", "content": "Today's date: 2025-11-22\nUser
|
|
43
|
+
# {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com\nTo load user profile: Use REM LOOKUP \"sarah@example.com\"\nSession ID: sess-123\nTo load session history: Use REM LOOKUP messages?session_id=sess-123"},
|
|
44
44
|
# {"role": "user", "content": "What's next for the API migration?"}
|
|
45
45
|
# ]
|
|
46
46
|
|
|
@@ -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.
|
|
@@ -114,7 +115,7 @@ class ContextBuilder:
|
|
|
114
115
|
- Agent can retrieve full content on-demand using REM LOOKUP
|
|
115
116
|
|
|
116
117
|
User Context (on-demand by default):
|
|
117
|
-
- System message includes REM LOOKUP hint: "User
|
|
118
|
+
- System message includes REM LOOKUP hint: "User: {email}. To load user profile: Use REM LOOKUP \"{email}\""
|
|
118
119
|
- Agent decides whether to load profile based on query
|
|
119
120
|
|
|
120
121
|
User Context (auto-inject when enabled):
|
|
@@ -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)
|
|
@@ -135,7 +137,7 @@ class ContextBuilder:
|
|
|
135
137
|
|
|
136
138
|
# messages structure:
|
|
137
139
|
# [
|
|
138
|
-
# {"role": "system", "content": "Today's date: 2025-11-22\nUser
|
|
140
|
+
# {"role": "system", "content": "Today's date: 2025-11-22\nUser: sarah@example.com\nTo load user profile: Use REM LOOKUP \"sarah@example.com\""},
|
|
139
141
|
# {"role": "user", "content": "Previous message"},
|
|
140
142
|
# {"role": "assistant", "content": "Start of long response... [REM LOOKUP session-123-msg-1] ...end"},
|
|
141
143
|
# {"role": "user", "content": "New message"}
|
|
@@ -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):
|
|
@@ -178,19 +191,29 @@ class ContextBuilder:
|
|
|
178
191
|
context_hint += "\n\nNo user context available (anonymous or new user)."
|
|
179
192
|
elif context.user_id:
|
|
180
193
|
# On-demand: Provide hint to use REM LOOKUP
|
|
181
|
-
|
|
182
|
-
|
|
194
|
+
# user_id is UUID5 hash of email - load user to get email for display and LOOKUP
|
|
195
|
+
user_repo = Repository(User, "users", db=db)
|
|
196
|
+
user = await user_repo.get_by_id(context.user_id, context.tenant_id)
|
|
197
|
+
if user and user.email:
|
|
198
|
+
# Show email (more useful than UUID) and LOOKUP hint
|
|
199
|
+
context_hint += f"\n\nUser: {user.email}"
|
|
200
|
+
context_hint += f"\nTo load user profile: Use REM LOOKUP \"{user.email}\""
|
|
201
|
+
else:
|
|
202
|
+
context_hint += f"\n\nUser ID: {context.user_id}"
|
|
203
|
+
context_hint += "\nUser profile not available."
|
|
183
204
|
|
|
184
205
|
# Add system context hint
|
|
185
206
|
messages.append(ContextMessage(role="system", content=context_hint))
|
|
186
207
|
|
|
187
|
-
# ALWAYS load session history (if session_id provided)
|
|
208
|
+
# ALWAYS load session history (if session_id provided)
|
|
209
|
+
# - Long assistant messages are compressed on load with REM LOOKUP hints
|
|
210
|
+
# - Tool messages are never compressed (contain structured metadata)
|
|
188
211
|
if context.session_id and settings.postgres.enabled:
|
|
189
212
|
store = SessionMessageStore(user_id=context.user_id or "default")
|
|
190
213
|
session_history = await store.load_session_messages(
|
|
191
214
|
session_id=context.session_id,
|
|
192
215
|
user_id=context.user_id,
|
|
193
|
-
|
|
216
|
+
compress_on_load=True, # Compress long assistant messages
|
|
194
217
|
)
|
|
195
218
|
|
|
196
219
|
# Convert to ContextMessage format
|
|
@@ -202,7 +225,7 @@ class ContextBuilder:
|
|
|
202
225
|
)
|
|
203
226
|
)
|
|
204
227
|
|
|
205
|
-
logger.debug(f"Loaded {len(session_history)}
|
|
228
|
+
logger.debug(f"Loaded {len(session_history)} messages for session {context.session_id}")
|
|
206
229
|
|
|
207
230
|
# Add new messages from request
|
|
208
231
|
if new_messages:
|
|
@@ -224,6 +247,9 @@ class ContextBuilder:
|
|
|
224
247
|
"""
|
|
225
248
|
Load user profile from database and format as context.
|
|
226
249
|
|
|
250
|
+
user_id is always a UUID5 hash of email (bijection).
|
|
251
|
+
Looks up user by their id field in the database.
|
|
252
|
+
|
|
227
253
|
Returns formatted string with:
|
|
228
254
|
- User summary (generated by dreaming worker)
|
|
229
255
|
- Current projects
|
|
@@ -237,6 +263,7 @@ class ContextBuilder:
|
|
|
237
263
|
|
|
238
264
|
try:
|
|
239
265
|
user_repo = Repository(User, "users", db=db)
|
|
266
|
+
# user_id is UUID5 hash of email - look up by database id
|
|
240
267
|
user = await user_repo.get_by_id(user_id, tenant_id)
|
|
241
268
|
|
|
242
269
|
if not user:
|