remdb 0.3.114__py3-none-any.whl → 0.3.172__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 (83) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +311 -0
  3. rem/agentic/agents/sse_simulator.py +2 -0
  4. rem/agentic/context.py +103 -5
  5. rem/agentic/context_builder.py +36 -9
  6. rem/agentic/mcp/tool_wrapper.py +161 -18
  7. rem/agentic/otel/setup.py +1 -0
  8. rem/agentic/providers/phoenix.py +371 -108
  9. rem/agentic/providers/pydantic_ai.py +172 -30
  10. rem/agentic/schema.py +8 -4
  11. rem/api/deps.py +3 -5
  12. rem/api/main.py +26 -4
  13. rem/api/mcp_router/resources.py +15 -10
  14. rem/api/mcp_router/server.py +11 -3
  15. rem/api/mcp_router/tools.py +418 -4
  16. rem/api/middleware/tracking.py +5 -5
  17. rem/api/routers/admin.py +218 -1
  18. rem/api/routers/auth.py +349 -6
  19. rem/api/routers/chat/completions.py +255 -7
  20. rem/api/routers/chat/models.py +81 -7
  21. rem/api/routers/chat/otel_utils.py +33 -0
  22. rem/api/routers/chat/sse_events.py +17 -1
  23. rem/api/routers/chat/streaming.py +126 -19
  24. rem/api/routers/feedback.py +134 -14
  25. rem/api/routers/messages.py +24 -15
  26. rem/api/routers/query.py +6 -3
  27. rem/auth/__init__.py +13 -3
  28. rem/auth/jwt.py +352 -0
  29. rem/auth/middleware.py +115 -10
  30. rem/auth/providers/__init__.py +4 -1
  31. rem/auth/providers/email.py +215 -0
  32. rem/cli/commands/README.md +42 -0
  33. rem/cli/commands/cluster.py +617 -168
  34. rem/cli/commands/configure.py +4 -7
  35. rem/cli/commands/db.py +66 -22
  36. rem/cli/commands/experiments.py +468 -76
  37. rem/cli/commands/schema.py +6 -5
  38. rem/cli/commands/session.py +336 -0
  39. rem/cli/dreaming.py +2 -2
  40. rem/cli/main.py +2 -0
  41. rem/config.py +8 -1
  42. rem/models/core/experiment.py +58 -14
  43. rem/models/entities/__init__.py +4 -0
  44. rem/models/entities/ontology.py +1 -1
  45. rem/models/entities/ontology_config.py +1 -1
  46. rem/models/entities/subscriber.py +175 -0
  47. rem/models/entities/user.py +1 -0
  48. rem/schemas/agents/core/agent-builder.yaml +235 -0
  49. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  50. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  51. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  52. rem/services/__init__.py +3 -1
  53. rem/services/content/service.py +4 -3
  54. rem/services/email/__init__.py +10 -0
  55. rem/services/email/service.py +513 -0
  56. rem/services/email/templates.py +360 -0
  57. rem/services/phoenix/client.py +59 -18
  58. rem/services/postgres/README.md +38 -0
  59. rem/services/postgres/diff_service.py +127 -6
  60. rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
  61. rem/services/postgres/repository.py +5 -4
  62. rem/services/postgres/schema_generator.py +205 -4
  63. rem/services/session/compression.py +120 -50
  64. rem/services/session/reload.py +14 -7
  65. rem/services/user_service.py +41 -9
  66. rem/settings.py +442 -23
  67. rem/sql/migrations/001_install.sql +156 -0
  68. rem/sql/migrations/002_install_models.sql +1951 -88
  69. rem/sql/migrations/004_cache_system.sql +548 -0
  70. rem/sql/migrations/005_schema_update.sql +145 -0
  71. rem/utils/README.md +45 -0
  72. rem/utils/__init__.py +18 -0
  73. rem/utils/files.py +157 -1
  74. rem/utils/schema_loader.py +139 -10
  75. rem/utils/sql_paths.py +146 -0
  76. rem/utils/vision.py +1 -1
  77. rem/workers/__init__.py +3 -1
  78. rem/workers/db_listener.py +579 -0
  79. rem/workers/unlogged_maintainer.py +463 -0
  80. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/METADATA +218 -180
  81. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/RECORD +83 -68
  82. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
  83. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/entry_points.txt +0 -0
@@ -1,13 +1,49 @@
1
1
  """Session message compression and rehydration for efficient context loading.
2
2
 
3
- This module implements message compression to keep conversation history within
4
- context windows while preserving full content via REM LOOKUP.
5
-
6
- Design Pattern:
7
- - Long assistant messages (>400 chars) are stored as separate Message entities
8
- - In-memory conversation uses truncated versions with REM lookup hints
9
- - Full content retrieved on-demand via LOOKUP queries
10
- - Compression disabled when Postgres is disabled
3
+ This module implements message storage and compression to keep conversation history
4
+ within context windows while preserving full content via REM LOOKUP.
5
+
6
+ Message Types and Storage Strategy
7
+ ===================================
8
+
9
+ All messages are stored UNCOMPRESSED in the database for full audit/analysis.
10
+ Compression happens only on RELOAD when reconstructing context for the LLM.
11
+
12
+ Message Types:
13
+ - `user`: User messages - stored and reloaded as-is
14
+ - `tool`: Tool call messages (e.g., register_metadata) - stored and reloaded as-is
15
+ NEVER compressed - contains important structured metadata
16
+ - `assistant`: Assistant text responses - stored uncompressed, but MAY BE
17
+ compressed on reload if long (>400 chars) with REM LOOKUP hints
18
+
19
+ Example Session Flow:
20
+ ```
21
+ Turn 1 (stored uncompressed):
22
+ - user: "I have a headache"
23
+ - tool: register_metadata({confidence: 0.3, collected_fields: {...}})
24
+ - assistant: "I'm sorry to hear that. How long has this been going on?"
25
+
26
+ Turn 2 (stored uncompressed):
27
+ - user: "About 3 days, really bad"
28
+ - tool: register_metadata({confidence: 0.6, collected_fields: {...}})
29
+ - assistant: "Got it - 3 days. On a scale of 1-10..."
30
+
31
+ On reload (for LLM context):
32
+ - user messages: returned as-is
33
+ - tool messages: returned as-is (never compressed)
34
+ - assistant messages: compressed if long, with REM LOOKUP hint for full retrieval
35
+ ```
36
+
37
+ REM LOOKUP Pattern:
38
+ - Long assistant messages get truncated with hint: "... [REM LOOKUP session-{id}-msg-{idx}] ..."
39
+ - Agent can retrieve full content on-demand using the LOOKUP key
40
+ - Keeps context window efficient while preserving data integrity
41
+
42
+ Key Design Decisions:
43
+ 1. Store everything uncompressed - full audit trail in database
44
+ 2. Compress only on reload - optimize for LLM context window
45
+ 3. Never compress tool messages - structured metadata must stay intact
46
+ 4. REM LOOKUP enables on-demand retrieval of full assistant responses
11
47
  """
12
48
 
13
49
  from typing import Any
@@ -170,12 +206,16 @@ class SessionMessageStore:
170
206
  entity_key = truncate_key(f"session-{session_id}-msg-{message_index}")
171
207
 
172
208
  # Create Message entity for assistant response
209
+ # Use pre-generated id from message dict if available (for frontend feedback)
173
210
  msg = Message(
211
+ id=message.get("id"), # Use pre-generated ID if provided
174
212
  content=message.get("content", ""),
175
213
  message_type=message.get("role", "assistant"),
176
214
  session_id=session_id,
177
215
  tenant_id=self.user_id, # Set tenant_id to user_id (application scoped to user)
178
216
  user_id=user_id or self.user_id,
217
+ trace_id=message.get("trace_id"),
218
+ span_id=message.get("span_id"),
179
219
  metadata={
180
220
  "message_index": message_index,
181
221
  "entity_key": entity_key, # Store entity key for LOOKUP
@@ -281,18 +321,33 @@ class SessionMessageStore:
281
321
  msg_copy["_entity_key"] = entity_key
282
322
  compressed_messages.append(msg_copy)
283
323
  else:
284
- # Short assistant messages, user messages, and system messages stored as-is
324
+ # Short assistant messages, user messages, tool messages, and system messages stored as-is
285
325
  # Store ALL messages in database for full audit trail
326
+ # Build metadata dict with standard fields
327
+ msg_metadata = {
328
+ "message_index": idx,
329
+ "timestamp": message.get("timestamp"),
330
+ }
331
+
332
+ # For tool messages, include tool call details in metadata
333
+ if message.get("role") == "tool":
334
+ if message.get("tool_call_id"):
335
+ msg_metadata["tool_call_id"] = message.get("tool_call_id")
336
+ if message.get("tool_name"):
337
+ msg_metadata["tool_name"] = message.get("tool_name")
338
+ if message.get("tool_arguments"):
339
+ msg_metadata["tool_arguments"] = message.get("tool_arguments")
340
+
286
341
  msg = Message(
342
+ id=message.get("id"), # Use pre-generated ID if provided
287
343
  content=content,
288
344
  message_type=message.get("role", "user"),
289
345
  session_id=session_id,
290
346
  tenant_id=self.user_id, # Set tenant_id to user_id (application scoped to user)
291
347
  user_id=user_id or self.user_id,
292
- metadata={
293
- "message_index": idx,
294
- "timestamp": message.get("timestamp"),
295
- },
348
+ trace_id=message.get("trace_id"),
349
+ span_id=message.get("span_id"),
350
+ metadata=msg_metadata,
296
351
  )
297
352
  await self.repo.upsert(msg)
298
353
  compressed_messages.append(message.copy())
@@ -300,18 +355,24 @@ class SessionMessageStore:
300
355
  return compressed_messages
301
356
 
302
357
  async def load_session_messages(
303
- self, session_id: str, user_id: str | None = None, decompress: bool = False
358
+ self, session_id: str, user_id: str | None = None, compress_on_load: bool = True
304
359
  ) -> list[dict[str, Any]]:
305
360
  """
306
- Load session messages from database.
361
+ Load session messages from database, optionally compressing long assistant messages.
362
+
363
+ Compression on Load:
364
+ - Tool messages (role: "tool") are NEVER compressed - they contain structured metadata
365
+ - User messages are returned as-is
366
+ - Assistant messages MAY be compressed if long (>400 chars) with REM LOOKUP hints
307
367
 
308
368
  Args:
309
369
  session_id: Session identifier
310
370
  user_id: Optional user identifier for filtering
311
- decompress: Whether to decompress messages (default: False)
371
+ compress_on_load: Whether to compress long assistant messages (default: True)
312
372
 
313
373
  Returns:
314
- List of session messages in chronological order
374
+ List of session messages in chronological order, with long assistant
375
+ messages optionally compressed with REM LOOKUP hints
315
376
  """
316
377
  if not settings.postgres.enabled:
317
378
  logger.debug("Postgres disabled, returning empty message list")
@@ -328,49 +389,58 @@ class SessionMessageStore:
328
389
 
329
390
  # Convert Message entities to dict format
330
391
  message_dicts = []
331
- for msg in messages:
392
+ for idx, msg in enumerate(messages):
393
+ role = msg.message_type or "assistant"
332
394
  msg_dict = {
333
- "role": msg.message_type or "assistant",
395
+ "role": role,
334
396
  "content": msg.content,
335
397
  "timestamp": msg.created_at.isoformat() if msg.created_at else None,
336
398
  }
337
399
 
338
- # Check if message was compressed
339
- entity_key: str | None = msg.metadata.get("entity_key") if msg.metadata else None
340
- if entity_key and len(msg.content) <= self.compressor.min_length_for_compression:
341
- # This is a compressed reference, mark it
342
- msg_dict["_compressed"] = True
343
- msg_dict["_entity_key"] = entity_key
344
- msg_dict["_original_length"] = msg.metadata.get("original_length", 0)
400
+ # For tool messages, reconstruct tool call metadata
401
+ if role == "tool" and msg.metadata:
402
+ if msg.metadata.get("tool_call_id"):
403
+ msg_dict["tool_call_id"] = msg.metadata["tool_call_id"]
404
+ if msg.metadata.get("tool_name"):
405
+ msg_dict["tool_name"] = msg.metadata["tool_name"]
406
+ if msg.metadata.get("tool_arguments"):
407
+ msg_dict["tool_arguments"] = msg.metadata["tool_arguments"]
408
+
409
+ # Compress long ASSISTANT messages on load (never tool messages)
410
+ if (
411
+ compress_on_load
412
+ and role == "assistant"
413
+ and len(msg.content) > self.compressor.min_length_for_compression
414
+ ):
415
+ # Generate entity key for REM LOOKUP
416
+ entity_key = truncate_key(f"session-{session_id}-msg-{idx}")
417
+ msg_dict = self.compressor.compress_message(msg_dict, entity_key)
345
418
 
346
419
  message_dicts.append(msg_dict)
347
420
 
348
- # Decompress if requested
349
- if decompress:
350
- decompressed_messages = []
351
- for message in message_dicts:
352
- if self.compressor.is_compressed(message):
353
- entity_key = self.compressor.get_entity_key(message)
354
- if entity_key:
355
- full_content = await self.retrieve_message(entity_key)
356
- if full_content:
357
- decompressed_messages.append(
358
- self.compressor.decompress_message(
359
- message, full_content
360
- )
361
- )
362
- else:
363
- # Fallback to compressed version if retrieval fails
364
- decompressed_messages.append(message)
365
- else:
366
- decompressed_messages.append(message)
367
- else:
368
- decompressed_messages.append(message)
369
-
370
- return decompressed_messages
371
-
421
+ logger.debug(
422
+ f"Loaded {len(message_dicts)} messages for session {session_id} "
423
+ f"(compress_on_load={compress_on_load})"
424
+ )
372
425
  return message_dicts
373
426
 
374
427
  except Exception as e:
375
428
  logger.error(f"Failed to load session messages: {e}")
376
429
  return []
430
+
431
+ async def retrieve_full_message(self, session_id: str, message_index: int) -> str | None:
432
+ """
433
+ Retrieve full message content by session and message index (for REM LOOKUP).
434
+
435
+ This is used when an agent needs to recover full content from a compressed
436
+ message that has a REM LOOKUP hint.
437
+
438
+ Args:
439
+ session_id: Session identifier
440
+ message_index: Index of message in session (from REM LOOKUP key)
441
+
442
+ Returns:
443
+ Full message content or None if not found
444
+ """
445
+ entity_key = truncate_key(f"session-{session_id}-msg-{message_index}")
446
+ return await self.retrieve_message(entity_key)
@@ -6,8 +6,14 @@ allowing conversations to be resumed across multiple API calls.
6
6
  Design Pattern:
7
7
  - Session identified by session_id from X-Session-Id header
8
8
  - All messages for session loaded in chronological order
9
- - Optional decompression of long assistant messages via REM LOOKUP
9
+ - Long assistant messages compressed on load with REM LOOKUP hints
10
+ - Tool messages (register_metadata, etc.) are NEVER compressed
10
11
  - Gracefully handles missing database (returns empty history)
12
+
13
+ Message Types on Reload:
14
+ - user: Returned as-is
15
+ - tool: Returned as-is with metadata (tool_call_id, tool_name, tool_arguments)
16
+ - assistant: Compressed on load if long (>400 chars), with REM LOOKUP for recovery
11
17
  """
12
18
 
13
19
  from loguru import logger
@@ -19,7 +25,7 @@ from rem.settings import settings
19
25
  async def reload_session(
20
26
  session_id: str,
21
27
  user_id: str,
22
- decompress_messages: bool = False,
28
+ compress_on_load: bool = True,
23
29
  ) -> list[dict]:
24
30
  """
25
31
  Reload all messages for a session from the database.
@@ -27,7 +33,8 @@ async def reload_session(
27
33
  Args:
28
34
  session_id: Session/conversation identifier
29
35
  user_id: User identifier for data isolation
30
- decompress_messages: Whether to decompress long messages via REM LOOKUP
36
+ compress_on_load: Whether to compress long assistant messages (default: True)
37
+ Tool messages are NEVER compressed.
31
38
 
32
39
  Returns:
33
40
  List of message dicts in chronological order (oldest first)
@@ -41,7 +48,7 @@ async def reload_session(
41
48
  history = await reload_session(
42
49
  session_id=context.session_id,
43
50
  user_id=context.user_id,
44
- decompress_messages=False, # Use compressed versions for efficiency
51
+ compress_on_load=True, # Compress long assistant messages
45
52
  )
46
53
 
47
54
  # Combine with new user message
@@ -60,14 +67,14 @@ async def reload_session(
60
67
  # Create message store for this session
61
68
  store = SessionMessageStore(user_id=user_id)
62
69
 
63
- # Load messages (optionally decompressed)
70
+ # Load messages (assistant messages compressed on load, tool messages never compressed)
64
71
  messages = await store.load_session_messages(
65
- session_id=session_id, user_id=user_id, decompress=decompress_messages
72
+ session_id=session_id, user_id=user_id, compress_on_load=compress_on_load
66
73
  )
67
74
 
68
75
  logger.debug(
69
76
  f"Reloaded {len(messages)} messages for session {session_id} "
70
- f"(decompressed={decompress_messages})"
77
+ f"(compress_on_load={compress_on_load})"
71
78
  )
72
79
 
73
80
  return messages
@@ -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 datetime import datetime
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 = datetime.utcnow()
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=email, # Use email as user_id for now? Or UUID?
63
- # The User model has 'user_id' field but also 'id' UUID.
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=datetime.utcnow(),
69
- updated_at=datetime.utcnow(),
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 = datetime.utcnow()
123
+ user.updated_at = utc_now()
92
124
 
93
125
  # Save
94
126
  await self.repo.upsert(user)