remdb 0.3.242__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 (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,488 @@
1
+ """Session message compression and rehydration for efficient context loading.
2
+
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
47
+ """
48
+
49
+ from typing import Any
50
+
51
+ from loguru import logger
52
+
53
+ # Max length for entity keys (kv_store.entity_key is varchar(255))
54
+ MAX_ENTITY_KEY_LENGTH = 255
55
+
56
+
57
+ def truncate_key(key: str, max_length: int = MAX_ENTITY_KEY_LENGTH) -> str:
58
+ """Truncate a key to max length, preserving useful suffix if possible."""
59
+ if len(key) <= max_length:
60
+ return key
61
+ # Keep first part and add hash suffix for uniqueness
62
+ import hashlib
63
+ hash_suffix = hashlib.md5(key.encode()).hexdigest()[:8]
64
+ truncated = key[:max_length - 9] + "-" + hash_suffix
65
+ logger.warning(f"Truncated key from {len(key)} to {len(truncated)} chars: {key[:50]}...")
66
+ return truncated
67
+
68
+ from rem.models.entities import Message, Session
69
+ from rem.services.postgres import PostgresService, Repository
70
+ from rem.settings import settings
71
+
72
+
73
+ class MessageCompressor:
74
+ """Compress and decompress session messages with REM lookup keys."""
75
+
76
+ def __init__(self, truncate_length: int = 200):
77
+ """
78
+ Initialize message compressor.
79
+
80
+ Args:
81
+ truncate_length: Number of characters to keep from start/end (default: 200)
82
+ """
83
+ self.truncate_length = truncate_length
84
+ self.min_length_for_compression = truncate_length * 2
85
+
86
+ def compress_message(
87
+ self, message: dict[str, Any], entity_key: str | None = None
88
+ ) -> dict[str, Any]:
89
+ """
90
+ Compress a message by truncating long content and adding REM lookup key.
91
+
92
+ Args:
93
+ message: Message dict with role and content
94
+ entity_key: Optional REM lookup key for full message recovery
95
+
96
+ Returns:
97
+ Compressed message dict
98
+ """
99
+ content = message.get("content", "")
100
+
101
+ # Don't compress short messages or system messages
102
+ if (
103
+ len(content) <= self.min_length_for_compression
104
+ or message.get("role") == "system"
105
+ ):
106
+ return message.copy()
107
+
108
+ # Compress long messages
109
+ n = self.truncate_length
110
+ start = content[:n]
111
+ end = content[-n:]
112
+
113
+ # Create compressed content with REM lookup hint
114
+ if entity_key:
115
+ compressed_content = f"{start}\n\n... [Message truncated - REM LOOKUP {entity_key} to recover full content] ...\n\n{end}"
116
+ else:
117
+ compressed_content = f"{start}\n\n... [Message truncated - {len(content) - 2*n} characters omitted] ...\n\n{end}"
118
+
119
+ compressed_message = message.copy()
120
+ compressed_message["content"] = compressed_content
121
+ compressed_message["_compressed"] = True
122
+ compressed_message["_original_length"] = len(content)
123
+ if entity_key:
124
+ compressed_message["_entity_key"] = entity_key
125
+
126
+ logger.debug(
127
+ f"Compressed message from {len(content)} to {len(compressed_content)} chars (key={entity_key})"
128
+ )
129
+
130
+ return compressed_message
131
+
132
+ def decompress_message(
133
+ self, message: dict[str, Any], full_content: str
134
+ ) -> dict[str, Any]:
135
+ """
136
+ Decompress a message by restoring full content.
137
+
138
+ Args:
139
+ message: Compressed message dict
140
+ full_content: Full content to restore
141
+
142
+ Returns:
143
+ Decompressed message dict
144
+ """
145
+ decompressed = message.copy()
146
+ decompressed["content"] = full_content
147
+ decompressed.pop("_compressed", None)
148
+ decompressed.pop("_original_length", None)
149
+ decompressed.pop("_entity_key", None)
150
+
151
+ return decompressed
152
+
153
+ def is_compressed(self, message: dict[str, Any]) -> bool:
154
+ """Check if a message is compressed."""
155
+ return message.get("_compressed", False)
156
+
157
+ def get_entity_key(self, message: dict[str, Any]) -> str | None:
158
+ """Get REM lookup key from compressed message."""
159
+ return message.get("_entity_key")
160
+
161
+
162
+ class SessionMessageStore:
163
+ """Store and retrieve session messages with compression."""
164
+
165
+ def __init__(
166
+ self,
167
+ user_id: str,
168
+ compressor: MessageCompressor | None = None,
169
+ ):
170
+ """
171
+ Initialize session message store.
172
+
173
+ Args:
174
+ user_id: User identifier for data isolation
175
+ compressor: Optional message compressor (creates default if None)
176
+ """
177
+ self.user_id = user_id
178
+ self.compressor = compressor or MessageCompressor()
179
+ self.repo = Repository(Message)
180
+ self._session_repo = Repository(Session, table_name="sessions")
181
+
182
+ async def _ensure_session_exists(
183
+ self,
184
+ session_id: str,
185
+ user_id: str | None = None,
186
+ ) -> None:
187
+ """
188
+ Ensure session exists, creating it if necessary.
189
+
190
+ Args:
191
+ session_id: Session UUID from X-Session-Id header
192
+ user_id: Optional user identifier
193
+ """
194
+ try:
195
+ # Check if session already exists by UUID
196
+ existing = await self._session_repo.get_by_id(session_id)
197
+ if existing:
198
+ return # Session already exists
199
+
200
+ # Create new session with the provided UUID as id
201
+ session = Session(
202
+ id=session_id, # Use the provided UUID as session id
203
+ name=session_id, # Default name to UUID, can be updated later
204
+ user_id=user_id or self.user_id,
205
+ tenant_id=self.user_id, # tenant_id set to user_id for scoping
206
+ )
207
+ await self._session_repo.upsert(session)
208
+ logger.info(f"Created session {session_id} for user {user_id or self.user_id}")
209
+
210
+ except Exception as e:
211
+ # Log but don't fail - session creation is best-effort
212
+ logger.warning(f"Failed to ensure session exists: {e}")
213
+
214
+ async def store_message(
215
+ self,
216
+ session_id: str,
217
+ message: dict[str, Any],
218
+ message_index: int,
219
+ user_id: str | None = None,
220
+ ) -> str:
221
+ """
222
+ Store a long assistant message as a Message entity for REM lookup.
223
+
224
+ Args:
225
+ session_id: Parent session identifier
226
+ message: Message dict to store
227
+ message_index: Index of message in conversation
228
+ user_id: Optional user identifier
229
+
230
+ Returns:
231
+ Entity key for REM lookup (message ID)
232
+ """
233
+ if not settings.postgres.enabled:
234
+ logger.debug("Postgres disabled, skipping message storage")
235
+ return f"msg-{message_index}"
236
+
237
+ # Create entity key for REM LOOKUP: session-{session_id}-msg-{index}
238
+ # Truncate to avoid exceeding kv_store.entity_key varchar(255) limit
239
+ entity_key = truncate_key(f"session-{session_id}-msg-{message_index}")
240
+
241
+ # Create Message entity for assistant response
242
+ # Use pre-generated id from message dict if available (for frontend feedback)
243
+ msg = Message(
244
+ id=message.get("id"), # Use pre-generated ID if provided
245
+ content=message.get("content", ""),
246
+ message_type=message.get("role", "assistant"),
247
+ session_id=session_id,
248
+ tenant_id=self.user_id, # Set tenant_id to user_id (application scoped to user)
249
+ user_id=user_id or self.user_id,
250
+ trace_id=message.get("trace_id"),
251
+ span_id=message.get("span_id"),
252
+ metadata={
253
+ "message_index": message_index,
254
+ "entity_key": entity_key, # Store entity key for LOOKUP
255
+ "timestamp": message.get("timestamp"),
256
+ },
257
+ )
258
+
259
+ # Store in database
260
+ await self.repo.upsert(msg)
261
+
262
+ logger.debug(f"Stored assistant response: {entity_key} (id={msg.id})")
263
+ return entity_key
264
+
265
+ async def retrieve_message(self, entity_key: str) -> str | None:
266
+ """
267
+ Retrieve full message content by REM lookup key.
268
+
269
+ Uses LOOKUP query pattern: finds message by entity_key in metadata.
270
+
271
+ Args:
272
+ entity_key: REM lookup key (session-{id}-msg-{index})
273
+
274
+ Returns:
275
+ Full message content or None if not found
276
+ """
277
+ if not settings.postgres.enabled:
278
+ logger.debug("Postgres disabled, cannot retrieve message")
279
+ return None
280
+
281
+ try:
282
+ # LOOKUP pattern: find message by entity_key in metadata
283
+ query = """
284
+ SELECT * FROM messages
285
+ WHERE metadata->>'entity_key' = $1
286
+ AND user_id = $2
287
+ AND deleted_at IS NULL
288
+ LIMIT 1
289
+ """
290
+
291
+ if not self.repo.db:
292
+ logger.warning("Database not available for message lookup")
293
+ return None
294
+
295
+ row = await self.repo.db.fetchrow(query, entity_key, self.user_id)
296
+
297
+ if row:
298
+ msg = Message.model_validate(dict(row))
299
+ logger.debug(f"Retrieved message via LOOKUP: {entity_key}")
300
+ return msg.content
301
+
302
+ logger.warning(f"Message not found via LOOKUP: {entity_key}")
303
+ return None
304
+
305
+ except Exception as e:
306
+ logger.error(f"Failed to retrieve message {entity_key}: {e}")
307
+ return None
308
+
309
+ async def store_session_messages(
310
+ self,
311
+ session_id: str,
312
+ messages: list[dict[str, Any]],
313
+ user_id: str | None = None,
314
+ compress: bool = True,
315
+ ) -> list[dict[str, Any]]:
316
+ """
317
+ Store all session messages and return compressed versions.
318
+
319
+ Ensures session exists before storing messages.
320
+
321
+ Args:
322
+ session_id: Session UUID
323
+ messages: List of messages to store
324
+ user_id: Optional user identifier
325
+ compress: Whether to compress messages (default: True)
326
+
327
+ Returns:
328
+ List of compressed messages with REM lookup keys
329
+ """
330
+ if not settings.postgres.enabled:
331
+ logger.debug("Postgres disabled, returning messages uncompressed")
332
+ return messages
333
+
334
+ # Ensure session exists before storing messages
335
+ await self._ensure_session_exists(session_id, user_id)
336
+
337
+ compressed_messages = []
338
+
339
+ for idx, message in enumerate(messages):
340
+ content = message.get("content", "")
341
+
342
+ # Only store and compress long assistant responses
343
+ if (
344
+ message.get("role") == "assistant"
345
+ and len(content) > self.compressor.min_length_for_compression
346
+ ):
347
+ # Store full message as separate Message entity
348
+ entity_key = await self.store_message(
349
+ session_id, message, idx, user_id
350
+ )
351
+
352
+ if compress:
353
+ compressed_msg = self.compressor.compress_message(
354
+ message, entity_key
355
+ )
356
+ compressed_messages.append(compressed_msg)
357
+ else:
358
+ msg_copy = message.copy()
359
+ msg_copy["_entity_key"] = entity_key
360
+ compressed_messages.append(msg_copy)
361
+ else:
362
+ # Short assistant messages, user messages, tool messages, and system messages stored as-is
363
+ # Store ALL messages in database for full audit trail
364
+ # Build metadata dict with standard fields
365
+ msg_metadata = {
366
+ "message_index": idx,
367
+ "timestamp": message.get("timestamp"),
368
+ }
369
+
370
+ # For tool messages, include tool call details in metadata
371
+ # Note: tool_arguments is stored only when provided (parent tool calls)
372
+ # For child tool calls (e.g., register_metadata), args are in content as JSON
373
+ if message.get("role") == "tool":
374
+ if message.get("tool_call_id"):
375
+ msg_metadata["tool_call_id"] = message.get("tool_call_id")
376
+ if message.get("tool_name"):
377
+ msg_metadata["tool_name"] = message.get("tool_name")
378
+ if message.get("tool_arguments"):
379
+ msg_metadata["tool_arguments"] = message.get("tool_arguments")
380
+
381
+ msg = Message(
382
+ id=message.get("id"), # Use pre-generated ID if provided
383
+ content=content,
384
+ message_type=message.get("role", "user"),
385
+ session_id=session_id,
386
+ tenant_id=self.user_id, # Set tenant_id to user_id (application scoped to user)
387
+ user_id=user_id or self.user_id,
388
+ trace_id=message.get("trace_id"),
389
+ span_id=message.get("span_id"),
390
+ metadata=msg_metadata,
391
+ )
392
+ await self.repo.upsert(msg)
393
+ compressed_messages.append(message.copy())
394
+
395
+ return compressed_messages
396
+
397
+ async def load_session_messages(
398
+ self, session_id: str, user_id: str | None = None, compress_on_load: bool = True
399
+ ) -> list[dict[str, Any]]:
400
+ """
401
+ Load session messages from database, optionally compressing long assistant messages.
402
+
403
+ Compression on Load:
404
+ - Tool messages (role: "tool") are NEVER compressed - they contain structured metadata
405
+ - User messages are returned as-is
406
+ - Assistant messages MAY be compressed if long (>400 chars) with REM LOOKUP hints
407
+
408
+ Args:
409
+ session_id: Session identifier
410
+ user_id: Optional user identifier for filtering
411
+ compress_on_load: Whether to compress long assistant messages (default: True)
412
+
413
+ Returns:
414
+ List of session messages in chronological order, with long assistant
415
+ messages optionally compressed with REM LOOKUP hints
416
+ """
417
+ if not settings.postgres.enabled:
418
+ logger.debug("Postgres disabled, returning empty message list")
419
+ return []
420
+
421
+ try:
422
+ # Load messages from repository
423
+ # Note: tenant_id column in messages table maps to user_id (user-scoped partitioning)
424
+ filters = {"session_id": session_id, "tenant_id": self.user_id}
425
+ if user_id:
426
+ filters["user_id"] = user_id
427
+
428
+ messages = await self.repo.find(filters, order_by="created_at ASC")
429
+
430
+ # Convert Message entities to dict format
431
+ message_dicts = []
432
+ for idx, msg in enumerate(messages):
433
+ role = msg.message_type or "assistant"
434
+ msg_dict = {
435
+ "role": role,
436
+ "content": msg.content,
437
+ "timestamp": msg.created_at.isoformat() if msg.created_at else None,
438
+ }
439
+
440
+ # For tool messages, reconstruct tool call metadata
441
+ # Note: tool_arguments may be in metadata (parent calls) or parsed from
442
+ # content (child calls like register_metadata) by pydantic_messages.py
443
+ if role == "tool" and msg.metadata:
444
+ if msg.metadata.get("tool_call_id"):
445
+ msg_dict["tool_call_id"] = msg.metadata["tool_call_id"]
446
+ if msg.metadata.get("tool_name"):
447
+ msg_dict["tool_name"] = msg.metadata["tool_name"]
448
+ if msg.metadata.get("tool_arguments"):
449
+ msg_dict["tool_arguments"] = msg.metadata["tool_arguments"]
450
+
451
+ # Compress long ASSISTANT messages on load (never tool messages)
452
+ if (
453
+ compress_on_load
454
+ and role == "assistant"
455
+ and len(msg.content) > self.compressor.min_length_for_compression
456
+ ):
457
+ # Generate entity key for REM LOOKUP
458
+ entity_key = truncate_key(f"session-{session_id}-msg-{idx}")
459
+ msg_dict = self.compressor.compress_message(msg_dict, entity_key)
460
+
461
+ message_dicts.append(msg_dict)
462
+
463
+ logger.debug(
464
+ f"Loaded {len(message_dicts)} messages for session {session_id} "
465
+ f"(compress_on_load={compress_on_load})"
466
+ )
467
+ return message_dicts
468
+
469
+ except Exception as e:
470
+ logger.error(f"Failed to load session messages: {e}")
471
+ return []
472
+
473
+ async def retrieve_full_message(self, session_id: str, message_index: int) -> str | None:
474
+ """
475
+ Retrieve full message content by session and message index (for REM LOOKUP).
476
+
477
+ This is used when an agent needs to recover full content from a compressed
478
+ message that has a REM LOOKUP hint.
479
+
480
+ Args:
481
+ session_id: Session identifier
482
+ message_index: Index of message in session (from REM LOOKUP key)
483
+
484
+ Returns:
485
+ Full message content or None if not found
486
+ """
487
+ entity_key = truncate_key(f"session-{session_id}-msg-{message_index}")
488
+ return await self.retrieve_message(entity_key)