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,310 @@
|
|
|
1
|
+
"""Convert stored session messages to pydantic-ai native message format.
|
|
2
|
+
|
|
3
|
+
This module enables proper conversation history replay by converting our simplified
|
|
4
|
+
storage format into pydantic-ai's native ModelRequest/ModelResponse types.
|
|
5
|
+
|
|
6
|
+
Key insight: When we store tool results, we only store the result (ToolReturnPart).
|
|
7
|
+
But LLM APIs require matching ToolCallPart for each ToolReturnPart. So we synthesize
|
|
8
|
+
the ToolCallPart from stored metadata (tool_name, tool_call_id) and arguments.
|
|
9
|
+
|
|
10
|
+
Tool arguments can come from two places:
|
|
11
|
+
- Parent tool calls (ask_agent): tool_arguments stored in metadata (content = result)
|
|
12
|
+
- Child tool calls (register_metadata): arguments parsed from content (content = args as JSON)
|
|
13
|
+
|
|
14
|
+
Storage format (our simplified format):
|
|
15
|
+
{"role": "user", "content": "..."}
|
|
16
|
+
{"role": "assistant", "content": "..."}
|
|
17
|
+
{"role": "tool", "content": "{...}", "tool_name": "...", "tool_call_id": "...", "tool_arguments": {...}} # optional
|
|
18
|
+
|
|
19
|
+
Pydantic-ai format (what the LLM expects):
|
|
20
|
+
ModelRequest(parts=[UserPromptPart(content="...")])
|
|
21
|
+
ModelResponse(parts=[TextPart(content="..."), ToolCallPart(...)]) # Call
|
|
22
|
+
ModelRequest(parts=[ToolReturnPart(...)]) # Result
|
|
23
|
+
|
|
24
|
+
Example usage:
|
|
25
|
+
from rem.services.session.pydantic_messages import session_to_pydantic_messages
|
|
26
|
+
|
|
27
|
+
# Load session history
|
|
28
|
+
session_history = await store.load_session_messages(session_id)
|
|
29
|
+
|
|
30
|
+
# Convert to pydantic-ai format
|
|
31
|
+
message_history = session_to_pydantic_messages(session_history)
|
|
32
|
+
|
|
33
|
+
# Use with agent.run()
|
|
34
|
+
result = await agent.run(user_prompt, message_history=message_history)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import re
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from loguru import logger
|
|
42
|
+
from pydantic_ai.messages import (
|
|
43
|
+
ModelMessage,
|
|
44
|
+
ModelRequest,
|
|
45
|
+
ModelResponse,
|
|
46
|
+
SystemPromptPart,
|
|
47
|
+
TextPart,
|
|
48
|
+
ToolCallPart,
|
|
49
|
+
ToolReturnPart,
|
|
50
|
+
UserPromptPart,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sanitize_tool_name(tool_name: str) -> str:
|
|
55
|
+
"""Sanitize tool name for OpenAI API compatibility.
|
|
56
|
+
|
|
57
|
+
OpenAI requires tool names to match pattern: ^[a-zA-Z0-9_-]+$
|
|
58
|
+
This replaces invalid characters (like colons) with underscores.
|
|
59
|
+
"""
|
|
60
|
+
return re.sub(r'[^a-zA-Z0-9_-]', '_', tool_name)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def session_to_pydantic_messages(
|
|
64
|
+
session_history: list[dict[str, Any]],
|
|
65
|
+
system_prompt: str | None = None,
|
|
66
|
+
) -> list[ModelMessage]:
|
|
67
|
+
"""Convert stored session messages to pydantic-ai ModelMessage format.
|
|
68
|
+
|
|
69
|
+
Handles the conversion of our simplified storage format to pydantic-ai's
|
|
70
|
+
native message types, including synthesizing ToolCallPart for tool results.
|
|
71
|
+
|
|
72
|
+
IMPORTANT: pydantic-ai only auto-adds system prompts when message_history is empty.
|
|
73
|
+
When passing message_history to agent.run(), you MUST include the system prompt
|
|
74
|
+
via the system_prompt parameter here.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
session_history: List of message dicts from SessionMessageStore.load_session_messages()
|
|
78
|
+
Each dict has: role, content, and optionally tool_name, tool_call_id, tool_arguments
|
|
79
|
+
system_prompt: The agent's system prompt (from schema description). This is REQUIRED
|
|
80
|
+
for proper agent behavior on subsequent turns, as pydantic-ai won't add it
|
|
81
|
+
automatically when message_history is provided.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of ModelMessage (ModelRequest | ModelResponse) ready for agent.run(message_history=...)
|
|
85
|
+
|
|
86
|
+
Note:
|
|
87
|
+
- System prompts ARE included as SystemPromptPart when system_prompt is provided
|
|
88
|
+
- Tool results require synthesized ToolCallPart to satisfy LLM API requirements
|
|
89
|
+
- The first message in session_history should be "user" role (from context builder)
|
|
90
|
+
"""
|
|
91
|
+
messages: list[ModelMessage] = []
|
|
92
|
+
|
|
93
|
+
# CRITICAL: Prepend agent's system prompt if provided
|
|
94
|
+
# This ensures the agent's instructions are present on every turn
|
|
95
|
+
# pydantic-ai only auto-adds system prompts when message_history is empty
|
|
96
|
+
if system_prompt:
|
|
97
|
+
messages.append(ModelRequest(parts=[SystemPromptPart(content=system_prompt)]))
|
|
98
|
+
logger.debug(f"Prepended agent system prompt ({len(system_prompt)} chars) to message history")
|
|
99
|
+
|
|
100
|
+
# Track pending tool results to batch them with assistant responses
|
|
101
|
+
# When we see a tool message, we need to:
|
|
102
|
+
# 1. Add a ModelResponse with ToolCallPart (synthesized)
|
|
103
|
+
# 2. Add a ModelRequest with ToolReturnPart (actual result)
|
|
104
|
+
|
|
105
|
+
i = 0
|
|
106
|
+
while i < len(session_history):
|
|
107
|
+
msg = session_history[i]
|
|
108
|
+
role = msg.get("role", "")
|
|
109
|
+
content = msg.get("content", "")
|
|
110
|
+
|
|
111
|
+
if role == "user":
|
|
112
|
+
# User messages become ModelRequest with UserPromptPart
|
|
113
|
+
messages.append(ModelRequest(parts=[UserPromptPart(content=content)]))
|
|
114
|
+
|
|
115
|
+
elif role == "assistant":
|
|
116
|
+
# Assistant text becomes ModelResponse with TextPart
|
|
117
|
+
# Check if there are following tool messages that should be grouped
|
|
118
|
+
tool_calls = []
|
|
119
|
+
tool_returns = []
|
|
120
|
+
|
|
121
|
+
# Look ahead for tool messages that follow this assistant message
|
|
122
|
+
j = i + 1
|
|
123
|
+
while j < len(session_history) and session_history[j].get("role") == "tool":
|
|
124
|
+
tool_msg = session_history[j]
|
|
125
|
+
tool_name = tool_msg.get("tool_name", "unknown_tool")
|
|
126
|
+
tool_call_id = tool_msg.get("tool_call_id", f"call_{j}")
|
|
127
|
+
tool_content = tool_msg.get("content", "{}")
|
|
128
|
+
|
|
129
|
+
# tool_arguments: prefer explicit field, fallback to parsing content
|
|
130
|
+
tool_arguments = tool_msg.get("tool_arguments")
|
|
131
|
+
if tool_arguments is None and isinstance(tool_content, str) and tool_content:
|
|
132
|
+
try:
|
|
133
|
+
tool_arguments = json.loads(tool_content)
|
|
134
|
+
except json.JSONDecodeError:
|
|
135
|
+
tool_arguments = {}
|
|
136
|
+
|
|
137
|
+
# Parse tool content if it's a JSON string
|
|
138
|
+
if isinstance(tool_content, str):
|
|
139
|
+
try:
|
|
140
|
+
tool_result = json.loads(tool_content)
|
|
141
|
+
except json.JSONDecodeError:
|
|
142
|
+
tool_result = {"raw": tool_content}
|
|
143
|
+
else:
|
|
144
|
+
tool_result = tool_content
|
|
145
|
+
|
|
146
|
+
# Sanitize tool name for OpenAI API compatibility
|
|
147
|
+
safe_tool_name = _sanitize_tool_name(tool_name)
|
|
148
|
+
|
|
149
|
+
# Synthesize ToolCallPart (what the model "called")
|
|
150
|
+
tool_calls.append(ToolCallPart(
|
|
151
|
+
tool_name=safe_tool_name,
|
|
152
|
+
args=tool_arguments if tool_arguments else {},
|
|
153
|
+
tool_call_id=tool_call_id,
|
|
154
|
+
))
|
|
155
|
+
|
|
156
|
+
# Create ToolReturnPart (the actual result)
|
|
157
|
+
tool_returns.append(ToolReturnPart(
|
|
158
|
+
tool_name=safe_tool_name,
|
|
159
|
+
content=tool_result,
|
|
160
|
+
tool_call_id=tool_call_id,
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
j += 1
|
|
164
|
+
|
|
165
|
+
# Build the assistant's ModelResponse
|
|
166
|
+
response_parts = []
|
|
167
|
+
|
|
168
|
+
# Add tool calls first (if any)
|
|
169
|
+
response_parts.extend(tool_calls)
|
|
170
|
+
|
|
171
|
+
# Add text content (if any)
|
|
172
|
+
if content:
|
|
173
|
+
response_parts.append(TextPart(content=content))
|
|
174
|
+
|
|
175
|
+
# Only add ModelResponse if we have parts
|
|
176
|
+
if response_parts:
|
|
177
|
+
messages.append(ModelResponse(
|
|
178
|
+
parts=response_parts,
|
|
179
|
+
model_name="recovered", # We don't store model name
|
|
180
|
+
))
|
|
181
|
+
|
|
182
|
+
# Add tool returns as ModelRequest (required by LLM API)
|
|
183
|
+
if tool_returns:
|
|
184
|
+
messages.append(ModelRequest(parts=tool_returns))
|
|
185
|
+
|
|
186
|
+
# Skip the tool messages we just processed
|
|
187
|
+
i = j - 1
|
|
188
|
+
|
|
189
|
+
elif role == "tool":
|
|
190
|
+
# Orphan tool message (no preceding assistant) - synthesize both parts
|
|
191
|
+
tool_name = msg.get("tool_name", "unknown_tool")
|
|
192
|
+
tool_call_id = msg.get("tool_call_id", f"call_{i}")
|
|
193
|
+
tool_content = msg.get("content", "{}")
|
|
194
|
+
|
|
195
|
+
# tool_arguments: prefer explicit field, fallback to parsing content
|
|
196
|
+
tool_arguments = msg.get("tool_arguments")
|
|
197
|
+
if tool_arguments is None and isinstance(tool_content, str) and tool_content:
|
|
198
|
+
try:
|
|
199
|
+
tool_arguments = json.loads(tool_content)
|
|
200
|
+
except json.JSONDecodeError:
|
|
201
|
+
tool_arguments = {}
|
|
202
|
+
|
|
203
|
+
# Parse tool content
|
|
204
|
+
if isinstance(tool_content, str):
|
|
205
|
+
try:
|
|
206
|
+
tool_result = json.loads(tool_content)
|
|
207
|
+
except json.JSONDecodeError:
|
|
208
|
+
tool_result = {"raw": tool_content}
|
|
209
|
+
else:
|
|
210
|
+
tool_result = tool_content
|
|
211
|
+
|
|
212
|
+
# Sanitize tool name for OpenAI API compatibility
|
|
213
|
+
safe_tool_name = _sanitize_tool_name(tool_name)
|
|
214
|
+
|
|
215
|
+
# Synthesize the tool call (ModelResponse with ToolCallPart)
|
|
216
|
+
messages.append(ModelResponse(
|
|
217
|
+
parts=[ToolCallPart(
|
|
218
|
+
tool_name=safe_tool_name,
|
|
219
|
+
args=tool_arguments if tool_arguments else {},
|
|
220
|
+
tool_call_id=tool_call_id,
|
|
221
|
+
)],
|
|
222
|
+
model_name="recovered",
|
|
223
|
+
))
|
|
224
|
+
|
|
225
|
+
# Add the tool return (ModelRequest with ToolReturnPart)
|
|
226
|
+
messages.append(ModelRequest(
|
|
227
|
+
parts=[ToolReturnPart(
|
|
228
|
+
tool_name=safe_tool_name,
|
|
229
|
+
content=tool_result,
|
|
230
|
+
tool_call_id=tool_call_id,
|
|
231
|
+
)]
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
elif role == "system":
|
|
235
|
+
# Skip system messages - pydantic-ai handles these via Agent.system_prompt
|
|
236
|
+
logger.debug("Skipping system message in session history (handled by Agent)")
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
logger.warning(f"Unknown message role in session history: {role}")
|
|
240
|
+
|
|
241
|
+
i += 1
|
|
242
|
+
|
|
243
|
+
logger.debug(f"Converted {len(session_history)} stored messages to {len(messages)} pydantic-ai messages")
|
|
244
|
+
return messages
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def audit_session_history(
|
|
248
|
+
session_id: str,
|
|
249
|
+
agent_name: str,
|
|
250
|
+
prompt: str,
|
|
251
|
+
raw_session_history: list[dict[str, Any]],
|
|
252
|
+
pydantic_messages_count: int,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Dump session history to a YAML file for debugging.
|
|
256
|
+
|
|
257
|
+
Only runs when DEBUG__AUDIT_SESSION=true. Writes to DEBUG__AUDIT_DIR (default /tmp).
|
|
258
|
+
Appends to the same file for a session, so all agent invocations are in one place.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
session_id: The session identifier
|
|
262
|
+
agent_name: Name of the agent being invoked
|
|
263
|
+
prompt: The prompt being sent to the agent
|
|
264
|
+
raw_session_history: The raw session messages from the database
|
|
265
|
+
pydantic_messages_count: Count of converted pydantic-ai messages
|
|
266
|
+
"""
|
|
267
|
+
from ...settings import settings
|
|
268
|
+
|
|
269
|
+
if not settings.debug.audit_session:
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
import yaml
|
|
274
|
+
from pathlib import Path
|
|
275
|
+
from ...utils.date_utils import utc_now, to_iso
|
|
276
|
+
|
|
277
|
+
audit_dir = Path(settings.debug.audit_dir)
|
|
278
|
+
audit_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
audit_file = audit_dir / f"{session_id}.yaml"
|
|
280
|
+
|
|
281
|
+
# Create entry for this agent invocation
|
|
282
|
+
entry = {
|
|
283
|
+
"timestamp": to_iso(utc_now()),
|
|
284
|
+
"agent_name": agent_name,
|
|
285
|
+
"prompt": prompt,
|
|
286
|
+
"raw_history_count": len(raw_session_history),
|
|
287
|
+
"pydantic_messages_count": pydantic_messages_count,
|
|
288
|
+
"raw_session_history": raw_session_history,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Load existing data or create new
|
|
292
|
+
existing_data: dict[str, Any] = {"session_id": session_id, "invocations": []}
|
|
293
|
+
if audit_file.exists():
|
|
294
|
+
with open(audit_file) as f:
|
|
295
|
+
loaded = yaml.safe_load(f)
|
|
296
|
+
if loaded:
|
|
297
|
+
# Ensure session_id is always present (backfill if missing)
|
|
298
|
+
existing_data = {
|
|
299
|
+
"session_id": loaded.get("session_id", session_id),
|
|
300
|
+
"invocations": loaded.get("invocations", []),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Append this invocation
|
|
304
|
+
existing_data["invocations"].append(entry)
|
|
305
|
+
|
|
306
|
+
with open(audit_file, "w") as f:
|
|
307
|
+
yaml.dump(existing_data, f, default_flow_style=False, allow_unicode=True)
|
|
308
|
+
logger.info(f"DEBUG: Session audit updated: {audit_file}")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.warning(f"DEBUG: Failed to dump session audit: {e}")
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
- Long assistant messages compressed on load with REM LOOKUP hints
|
|
10
|
+
- Tool messages (register_metadata, etc.) are NEVER compressed
|
|
11
|
+
- Gracefully handles missing database (returns empty history)
|
|
12
|
+
|
|
13
|
+
Message Types on Reload:
|
|
14
|
+
- user: Returned as-is
|
|
15
|
+
- tool: Returned with metadata (tool_call_id, tool_name). tool_arguments may be in
|
|
16
|
+
metadata (parent calls) or parsed from content (child calls) by pydantic_messages.py
|
|
17
|
+
- assistant: Compressed on load if long (>400 chars), with REM LOOKUP for recovery
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from loguru import logger
|
|
21
|
+
|
|
22
|
+
from rem.services.session.compression import SessionMessageStore
|
|
23
|
+
from rem.settings import settings
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def reload_session(
|
|
27
|
+
session_id: str,
|
|
28
|
+
user_id: str,
|
|
29
|
+
compress_on_load: bool = True,
|
|
30
|
+
) -> list[dict]:
|
|
31
|
+
"""
|
|
32
|
+
Reload all messages for a session from the database.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
session_id: Session/conversation identifier
|
|
36
|
+
user_id: User identifier for data isolation
|
|
37
|
+
compress_on_load: Whether to compress long assistant messages (default: True)
|
|
38
|
+
Tool messages are NEVER compressed.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of message dicts in chronological order (oldest first)
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
# In completions endpoint
|
|
46
|
+
context = AgentContext.from_headers(dict(request.headers))
|
|
47
|
+
|
|
48
|
+
# Reload previous conversation history
|
|
49
|
+
history = await reload_session(
|
|
50
|
+
session_id=context.session_id,
|
|
51
|
+
user_id=context.user_id,
|
|
52
|
+
compress_on_load=True, # Compress long assistant messages
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Combine with new user message
|
|
56
|
+
messages = history + [{"role": "user", "content": prompt}]
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
if not settings.postgres.enabled:
|
|
60
|
+
logger.debug("Postgres disabled, returning empty session history")
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
if not session_id:
|
|
64
|
+
logger.debug("No session_id provided, returning empty history")
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Create message store for this session
|
|
69
|
+
store = SessionMessageStore(user_id=user_id)
|
|
70
|
+
|
|
71
|
+
# Load messages (assistant messages compressed on load, tool messages never compressed)
|
|
72
|
+
messages = await store.load_session_messages(
|
|
73
|
+
session_id=session_id, user_id=user_id, compress_on_load=compress_on_load
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
logger.debug(
|
|
77
|
+
f"Reloaded {len(messages)} messages for session {session_id} "
|
|
78
|
+
f"(compress_on_load={compress_on_load})"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return messages
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Failed to reload session {session_id}: {e}")
|
|
85
|
+
return []
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Service - User account management.
|
|
3
|
+
|
|
4
|
+
Handles user creation, profile updates, and session linking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from rem.utils.date_utils import utc_now
|
|
8
|
+
from rem.utils.user_id import email_to_user_id
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from ..models.entities.user import User, UserTier
|
|
14
|
+
from .postgres.repository import Repository
|
|
15
|
+
from .postgres.service import PostgresService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserService:
|
|
19
|
+
"""
|
|
20
|
+
Service for managing user accounts and sessions.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, db: PostgresService):
|
|
24
|
+
self.db = db
|
|
25
|
+
self.repo = Repository(User, "users", db=db)
|
|
26
|
+
|
|
27
|
+
async def get_or_create_user(
|
|
28
|
+
self,
|
|
29
|
+
email: str,
|
|
30
|
+
tenant_id: str = "default",
|
|
31
|
+
name: str = "New User",
|
|
32
|
+
avatar_url: Optional[str] = None,
|
|
33
|
+
) -> User:
|
|
34
|
+
"""
|
|
35
|
+
Get existing user by email or create a new one.
|
|
36
|
+
"""
|
|
37
|
+
users = await self.repo.find(filters={"email": email}, limit=1)
|
|
38
|
+
|
|
39
|
+
if users:
|
|
40
|
+
user = users[0]
|
|
41
|
+
# Update profile if needed (e.g., name/avatar from OAuth)
|
|
42
|
+
updated = False
|
|
43
|
+
if name and user.name == "New User": # Only update if placeholder
|
|
44
|
+
user.name = name
|
|
45
|
+
updated = True
|
|
46
|
+
|
|
47
|
+
# Store avatar in metadata if provided
|
|
48
|
+
if avatar_url:
|
|
49
|
+
user.metadata = user.metadata or {}
|
|
50
|
+
if user.metadata.get("avatar_url") != avatar_url:
|
|
51
|
+
user.metadata["avatar_url"] = avatar_url
|
|
52
|
+
updated = True
|
|
53
|
+
|
|
54
|
+
if updated:
|
|
55
|
+
user.updated_at = utc_now()
|
|
56
|
+
await self.repo.upsert(user)
|
|
57
|
+
|
|
58
|
+
return user
|
|
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)
|
|
64
|
+
user = User(
|
|
65
|
+
id=hashed_id, # Database id = hash of email
|
|
66
|
+
tenant_id=tenant_id,
|
|
67
|
+
user_id=hashed_id, # user_id = hash of email (same as id)
|
|
68
|
+
name=email, # Email as entity_key for REM LOOKUP
|
|
69
|
+
email=email,
|
|
70
|
+
tier=UserTier.FREE,
|
|
71
|
+
created_at=utc_now(),
|
|
72
|
+
updated_at=utc_now(),
|
|
73
|
+
metadata={"avatar_url": avatar_url} if avatar_url else {},
|
|
74
|
+
)
|
|
75
|
+
await self.repo.upsert(user)
|
|
76
|
+
logger.info(f"Created new user: {email}")
|
|
77
|
+
return user
|
|
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
|
+
|
|
108
|
+
async def link_anonymous_session(self, user: User, anon_id: str) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Link an anonymous session ID to a user account.
|
|
111
|
+
|
|
112
|
+
This allows merging history from the anonymous session into the user's profile.
|
|
113
|
+
"""
|
|
114
|
+
if not anon_id:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
# Check if already linked
|
|
118
|
+
if anon_id in user.anonymous_ids:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
# Add to list
|
|
122
|
+
user.anonymous_ids.append(anon_id)
|
|
123
|
+
user.updated_at = utc_now()
|
|
124
|
+
|
|
125
|
+
# Save
|
|
126
|
+
await self.repo.upsert(user)
|
|
127
|
+
logger.info(f"Linked anonymous session {anon_id} to user {user.email}")
|
|
128
|
+
|
|
129
|
+
# TODO: Migrate/Merge actual data (rate limit counts, history) if needed.
|
|
130
|
+
# For now, we just link the IDs so future queries can include data from this anon_id.
|