remdb 0.3.0__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/__init__.py +2 -0
- rem/agentic/README.md +650 -0
- rem/agentic/__init__.py +39 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +8 -0
- rem/agentic/context.py +148 -0
- rem/agentic/context_builder.py +329 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +107 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +151 -0
- rem/agentic/providers/phoenix.py +674 -0
- rem/agentic/providers/pydantic_ai.py +572 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +396 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +231 -0
- rem/api/README.md +420 -0
- rem/api/main.py +324 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +536 -0
- rem/api/mcp_router/server.py +213 -0
- rem/api/mcp_router/tools.py +584 -0
- rem/api/routers/auth.py +229 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/completions.py +281 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +124 -0
- rem/api/routers/chat/streaming.py +185 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +26 -0
- rem/auth/middleware.py +100 -0
- rem/auth/providers/__init__.py +13 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +455 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +126 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +566 -0
- rem/cli/commands/configure.py +497 -0
- rem/cli/commands/db.py +493 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1302 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +245 -0
- rem/cli/commands/schema.py +183 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +96 -0
- rem/config.py +237 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +64 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +628 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +243 -0
- rem/models/entities/__init__.py +43 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +35 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +191 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/user.py +85 -0
- rem/py.typed +0 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +128 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +16 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +806 -0
- rem/services/content/service.py +676 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +336 -0
- rem/services/dreaming/moment_service.py +264 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +120 -0
- rem/services/embeddings/worker.py +421 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +686 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +575 -0
- rem/services/postgres/__init__.py +23 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
- rem/services/postgres/register_type.py +352 -0
- rem/services/postgres/repository.py +337 -0
- rem/services/postgres/schema_generator.py +379 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +354 -0
- rem/services/rem/README.md +304 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +145 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +527 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +6 -0
- rem/services/session/compression.py +360 -0
- rem/services/session/reload.py +77 -0
- rem/settings.py +1235 -0
- rem/sql/002_install_models.sql +1068 -0
- rem/sql/background_indexes.sql +42 -0
- rem/sql/install_models.sql +1038 -0
- rem/sql/migrations/001_install.sql +503 -0
- rem/sql/migrations/002_install_models.sql +1202 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +583 -0
- rem/utils/__init__.py +43 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +423 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/markdown.py +16 -0
- rem/utils/model_helpers.py +236 -0
- rem/utils/schema_loader.py +336 -0
- rem/utils/sql_types.py +348 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +330 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +5 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- remdb-0.3.0.dist-info/METADATA +1455 -0
- remdb-0.3.0.dist-info/RECORD +187 -0
- remdb-0.3.0.dist-info/WHEEL +4 -0
- remdb-0.3.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Session message compression and rehydration for efficient context loading.
|
|
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
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from rem.models.entities import Message
|
|
18
|
+
from rem.services.postgres import PostgresService, Repository
|
|
19
|
+
from rem.settings import settings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MessageCompressor:
|
|
23
|
+
"""Compress and decompress session messages with REM lookup keys."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, truncate_length: int = 200):
|
|
26
|
+
"""
|
|
27
|
+
Initialize message compressor.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
truncate_length: Number of characters to keep from start/end (default: 200)
|
|
31
|
+
"""
|
|
32
|
+
self.truncate_length = truncate_length
|
|
33
|
+
self.min_length_for_compression = truncate_length * 2
|
|
34
|
+
|
|
35
|
+
def compress_message(
|
|
36
|
+
self, message: dict[str, Any], entity_key: str | None = None
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Compress a message by truncating long content and adding REM lookup key.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
message: Message dict with role and content
|
|
43
|
+
entity_key: Optional REM lookup key for full message recovery
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Compressed message dict
|
|
47
|
+
"""
|
|
48
|
+
content = message.get("content", "")
|
|
49
|
+
|
|
50
|
+
# Don't compress short messages or system messages
|
|
51
|
+
if (
|
|
52
|
+
len(content) <= self.min_length_for_compression
|
|
53
|
+
or message.get("role") == "system"
|
|
54
|
+
):
|
|
55
|
+
return message.copy()
|
|
56
|
+
|
|
57
|
+
# Compress long messages
|
|
58
|
+
n = self.truncate_length
|
|
59
|
+
start = content[:n]
|
|
60
|
+
end = content[-n:]
|
|
61
|
+
|
|
62
|
+
# Create compressed content with REM lookup hint
|
|
63
|
+
if entity_key:
|
|
64
|
+
compressed_content = f"{start}\n\n... [Message truncated - REM LOOKUP {entity_key} to recover full content] ...\n\n{end}"
|
|
65
|
+
else:
|
|
66
|
+
compressed_content = f"{start}\n\n... [Message truncated - {len(content) - 2*n} characters omitted] ...\n\n{end}"
|
|
67
|
+
|
|
68
|
+
compressed_message = message.copy()
|
|
69
|
+
compressed_message["content"] = compressed_content
|
|
70
|
+
compressed_message["_compressed"] = True
|
|
71
|
+
compressed_message["_original_length"] = len(content)
|
|
72
|
+
if entity_key:
|
|
73
|
+
compressed_message["_entity_key"] = entity_key
|
|
74
|
+
|
|
75
|
+
logger.debug(
|
|
76
|
+
f"Compressed message from {len(content)} to {len(compressed_content)} chars (key={entity_key})"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return compressed_message
|
|
80
|
+
|
|
81
|
+
def decompress_message(
|
|
82
|
+
self, message: dict[str, Any], full_content: str
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""
|
|
85
|
+
Decompress a message by restoring full content.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
message: Compressed message dict
|
|
89
|
+
full_content: Full content to restore
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Decompressed message dict
|
|
93
|
+
"""
|
|
94
|
+
decompressed = message.copy()
|
|
95
|
+
decompressed["content"] = full_content
|
|
96
|
+
decompressed.pop("_compressed", None)
|
|
97
|
+
decompressed.pop("_original_length", None)
|
|
98
|
+
decompressed.pop("_entity_key", None)
|
|
99
|
+
|
|
100
|
+
return decompressed
|
|
101
|
+
|
|
102
|
+
def is_compressed(self, message: dict[str, Any]) -> bool:
|
|
103
|
+
"""Check if a message is compressed."""
|
|
104
|
+
return message.get("_compressed", False)
|
|
105
|
+
|
|
106
|
+
def get_entity_key(self, message: dict[str, Any]) -> str | None:
|
|
107
|
+
"""Get REM lookup key from compressed message."""
|
|
108
|
+
return message.get("_entity_key")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class SessionMessageStore:
|
|
112
|
+
"""Store and retrieve session messages with compression."""
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
user_id: str,
|
|
117
|
+
compressor: MessageCompressor | None = None,
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Initialize session message store.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
user_id: User identifier for data isolation
|
|
124
|
+
compressor: Optional message compressor (creates default if None)
|
|
125
|
+
"""
|
|
126
|
+
self.user_id = user_id
|
|
127
|
+
self.compressor = compressor or MessageCompressor()
|
|
128
|
+
self.repo = Repository(Message)
|
|
129
|
+
|
|
130
|
+
async def store_message(
|
|
131
|
+
self,
|
|
132
|
+
session_id: str,
|
|
133
|
+
message: dict[str, Any],
|
|
134
|
+
message_index: int,
|
|
135
|
+
user_id: str | None = None,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Store a long assistant message as a Message entity for REM lookup.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
session_id: Parent session identifier
|
|
142
|
+
message: Message dict to store
|
|
143
|
+
message_index: Index of message in conversation
|
|
144
|
+
user_id: Optional user identifier
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Entity key for REM lookup (message ID)
|
|
148
|
+
"""
|
|
149
|
+
if not settings.postgres.enabled:
|
|
150
|
+
logger.debug("Postgres disabled, skipping message storage")
|
|
151
|
+
return f"msg-{message_index}"
|
|
152
|
+
|
|
153
|
+
# Create entity key for REM LOOKUP: session-{session_id}-msg-{index}
|
|
154
|
+
entity_key = f"session-{session_id}-msg-{message_index}"
|
|
155
|
+
|
|
156
|
+
# Create Message entity for assistant response
|
|
157
|
+
msg = Message(
|
|
158
|
+
content=message.get("content", ""),
|
|
159
|
+
message_type=message.get("role", "assistant"),
|
|
160
|
+
session_id=session_id,
|
|
161
|
+
tenant_id=self.user_id, # Set tenant_id to user_id (application scoped to user)
|
|
162
|
+
user_id=user_id or self.user_id,
|
|
163
|
+
metadata={
|
|
164
|
+
"message_index": message_index,
|
|
165
|
+
"entity_key": entity_key, # Store entity key for LOOKUP
|
|
166
|
+
"timestamp": message.get("timestamp"),
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Store in database
|
|
171
|
+
await self.repo.upsert(msg)
|
|
172
|
+
|
|
173
|
+
logger.debug(f"Stored assistant response: {entity_key} (id={msg.id})")
|
|
174
|
+
return entity_key
|
|
175
|
+
|
|
176
|
+
async def retrieve_message(self, entity_key: str) -> str | None:
|
|
177
|
+
"""
|
|
178
|
+
Retrieve full message content by REM lookup key.
|
|
179
|
+
|
|
180
|
+
Uses LOOKUP query pattern: finds message by entity_key in metadata.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
entity_key: REM lookup key (session-{id}-msg-{index})
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Full message content or None if not found
|
|
187
|
+
"""
|
|
188
|
+
if not settings.postgres.enabled:
|
|
189
|
+
logger.debug("Postgres disabled, cannot retrieve message")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# LOOKUP pattern: find message by entity_key in metadata
|
|
194
|
+
query = """
|
|
195
|
+
SELECT * FROM messages
|
|
196
|
+
WHERE metadata->>'entity_key' = $1
|
|
197
|
+
AND user_id = $2
|
|
198
|
+
AND deleted_at IS NULL
|
|
199
|
+
LIMIT 1
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
if not self.repo.db:
|
|
203
|
+
logger.warning("Database not available for message lookup")
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
row = await self.repo.db.fetchrow(query, entity_key, self.user_id)
|
|
207
|
+
|
|
208
|
+
if row:
|
|
209
|
+
msg = Message.model_validate(dict(row))
|
|
210
|
+
logger.debug(f"Retrieved message via LOOKUP: {entity_key}")
|
|
211
|
+
return msg.content
|
|
212
|
+
|
|
213
|
+
logger.warning(f"Message not found via LOOKUP: {entity_key}")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"Failed to retrieve message {entity_key}: {e}")
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
async def store_session_messages(
|
|
221
|
+
self,
|
|
222
|
+
session_id: str,
|
|
223
|
+
messages: list[dict[str, Any]],
|
|
224
|
+
user_id: str | None = None,
|
|
225
|
+
compress: bool = True,
|
|
226
|
+
) -> list[dict[str, Any]]:
|
|
227
|
+
"""
|
|
228
|
+
Store all session messages and return compressed versions.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
session_id: Session identifier
|
|
232
|
+
messages: List of messages to store
|
|
233
|
+
user_id: Optional user identifier
|
|
234
|
+
compress: Whether to compress messages (default: True)
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of compressed messages with REM lookup keys
|
|
238
|
+
"""
|
|
239
|
+
if not settings.postgres.enabled:
|
|
240
|
+
logger.debug("Postgres disabled, returning messages uncompressed")
|
|
241
|
+
return messages
|
|
242
|
+
|
|
243
|
+
compressed_messages = []
|
|
244
|
+
|
|
245
|
+
for idx, message in enumerate(messages):
|
|
246
|
+
content = message.get("content", "")
|
|
247
|
+
|
|
248
|
+
# Only store and compress long assistant responses
|
|
249
|
+
if (
|
|
250
|
+
message.get("role") == "assistant"
|
|
251
|
+
and len(content) > self.compressor.min_length_for_compression
|
|
252
|
+
):
|
|
253
|
+
# Store full message as separate Message entity
|
|
254
|
+
entity_key = await self.store_message(
|
|
255
|
+
session_id, message, idx, user_id
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if compress:
|
|
259
|
+
compressed_msg = self.compressor.compress_message(
|
|
260
|
+
message, entity_key
|
|
261
|
+
)
|
|
262
|
+
compressed_messages.append(compressed_msg)
|
|
263
|
+
else:
|
|
264
|
+
msg_copy = message.copy()
|
|
265
|
+
msg_copy["_entity_key"] = entity_key
|
|
266
|
+
compressed_messages.append(msg_copy)
|
|
267
|
+
else:
|
|
268
|
+
# Short assistant messages, user messages, and system messages stored as-is
|
|
269
|
+
# Store ALL messages in database for full audit trail
|
|
270
|
+
msg = Message(
|
|
271
|
+
content=content,
|
|
272
|
+
message_type=message.get("role", "user"),
|
|
273
|
+
session_id=session_id,
|
|
274
|
+
tenant_id=self.user_id, # Set tenant_id to user_id (application scoped to user)
|
|
275
|
+
user_id=user_id or self.user_id,
|
|
276
|
+
metadata={
|
|
277
|
+
"message_index": idx,
|
|
278
|
+
"timestamp": message.get("timestamp"),
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
await self.repo.upsert(msg)
|
|
282
|
+
compressed_messages.append(message.copy())
|
|
283
|
+
|
|
284
|
+
return compressed_messages
|
|
285
|
+
|
|
286
|
+
async def load_session_messages(
|
|
287
|
+
self, session_id: str, user_id: str | None = None, decompress: bool = False
|
|
288
|
+
) -> list[dict[str, Any]]:
|
|
289
|
+
"""
|
|
290
|
+
Load session messages from database.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
session_id: Session identifier
|
|
294
|
+
user_id: Optional user identifier for filtering
|
|
295
|
+
decompress: Whether to decompress messages (default: False)
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of session messages in chronological order
|
|
299
|
+
"""
|
|
300
|
+
if not settings.postgres.enabled:
|
|
301
|
+
logger.debug("Postgres disabled, returning empty message list")
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
# Load messages from repository
|
|
306
|
+
# Note: tenant_id column in messages table maps to user_id (user-scoped partitioning)
|
|
307
|
+
filters = {"session_id": session_id, "tenant_id": self.user_id}
|
|
308
|
+
if user_id:
|
|
309
|
+
filters["user_id"] = user_id
|
|
310
|
+
|
|
311
|
+
messages = await self.repo.find(filters, order_by="created_at ASC")
|
|
312
|
+
|
|
313
|
+
# Convert Message entities to dict format
|
|
314
|
+
message_dicts = []
|
|
315
|
+
for msg in messages:
|
|
316
|
+
msg_dict = {
|
|
317
|
+
"role": msg.message_type or "assistant",
|
|
318
|
+
"content": msg.content,
|
|
319
|
+
"timestamp": msg.created_at.isoformat() if msg.created_at else None,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Check if message was compressed
|
|
323
|
+
entity_key: str | None = msg.metadata.get("entity_key") if msg.metadata else None
|
|
324
|
+
if entity_key and len(msg.content) <= self.compressor.min_length_for_compression:
|
|
325
|
+
# This is a compressed reference, mark it
|
|
326
|
+
msg_dict["_compressed"] = True
|
|
327
|
+
msg_dict["_entity_key"] = entity_key
|
|
328
|
+
msg_dict["_original_length"] = msg.metadata.get("original_length", 0)
|
|
329
|
+
|
|
330
|
+
message_dicts.append(msg_dict)
|
|
331
|
+
|
|
332
|
+
# Decompress if requested
|
|
333
|
+
if decompress:
|
|
334
|
+
decompressed_messages = []
|
|
335
|
+
for message in message_dicts:
|
|
336
|
+
if self.compressor.is_compressed(message):
|
|
337
|
+
entity_key = self.compressor.get_entity_key(message)
|
|
338
|
+
if entity_key:
|
|
339
|
+
full_content = await self.retrieve_message(entity_key)
|
|
340
|
+
if full_content:
|
|
341
|
+
decompressed_messages.append(
|
|
342
|
+
self.compressor.decompress_message(
|
|
343
|
+
message, full_content
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
# Fallback to compressed version if retrieval fails
|
|
348
|
+
decompressed_messages.append(message)
|
|
349
|
+
else:
|
|
350
|
+
decompressed_messages.append(message)
|
|
351
|
+
else:
|
|
352
|
+
decompressed_messages.append(message)
|
|
353
|
+
|
|
354
|
+
return decompressed_messages
|
|
355
|
+
|
|
356
|
+
return message_dicts
|
|
357
|
+
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Failed to load session messages: {e}")
|
|
360
|
+
return []
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Session reloading logic for conversation history restoration.
|
|
2
|
+
|
|
3
|
+
This module implements session history loading from the database,
|
|
4
|
+
allowing conversations to be resumed across multiple API calls.
|
|
5
|
+
|
|
6
|
+
Design Pattern:
|
|
7
|
+
- Session identified by session_id from X-Session-Id header
|
|
8
|
+
- All messages for session loaded in chronological order
|
|
9
|
+
- Optional decompression of long assistant messages via REM LOOKUP
|
|
10
|
+
- Gracefully handles missing database (returns empty history)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from rem.services.session.compression import SessionMessageStore
|
|
16
|
+
from rem.settings import settings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def reload_session(
|
|
20
|
+
session_id: str,
|
|
21
|
+
user_id: str,
|
|
22
|
+
decompress_messages: bool = False,
|
|
23
|
+
) -> list[dict]:
|
|
24
|
+
"""
|
|
25
|
+
Reload all messages for a session from the database.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
session_id: Session/conversation identifier
|
|
29
|
+
user_id: User identifier for data isolation
|
|
30
|
+
decompress_messages: Whether to decompress long messages via REM LOOKUP
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of message dicts in chronological order (oldest first)
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
```python
|
|
37
|
+
# In completions endpoint
|
|
38
|
+
context = AgentContext.from_headers(dict(request.headers))
|
|
39
|
+
|
|
40
|
+
# Reload previous conversation history
|
|
41
|
+
history = await reload_session(
|
|
42
|
+
session_id=context.session_id,
|
|
43
|
+
user_id=context.user_id,
|
|
44
|
+
decompress_messages=False, # Use compressed versions for efficiency
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Combine with new user message
|
|
48
|
+
messages = history + [{"role": "user", "content": prompt}]
|
|
49
|
+
```
|
|
50
|
+
"""
|
|
51
|
+
if not settings.postgres.enabled:
|
|
52
|
+
logger.debug("Postgres disabled, returning empty session history")
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
if not session_id:
|
|
56
|
+
logger.debug("No session_id provided, returning empty history")
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Create message store for this session
|
|
61
|
+
store = SessionMessageStore(user_id=user_id)
|
|
62
|
+
|
|
63
|
+
# Load messages (optionally decompressed)
|
|
64
|
+
messages = await store.load_session_messages(
|
|
65
|
+
session_id=session_id, user_id=user_id, decompress=decompress_messages
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logger.info(
|
|
69
|
+
f"Reloaded {len(messages)} messages for session {session_id} "
|
|
70
|
+
f"(decompressed={decompress_messages})"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return messages
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Failed to reload session {session_id}: {e}")
|
|
77
|
+
return []
|