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.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -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/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -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 +132 -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 +18 -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 +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -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/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -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 +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -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 +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -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/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- 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)
|