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,453 @@
1
+ """
2
+ CLI command for viewing and simulating session conversations.
3
+
4
+ Usage:
5
+ rem session show <user_id> [--session-id] [--role user|assistant|system]
6
+ rem session show <user_id> --simulate-next [--save] [--custom-sim-prompt "..."]
7
+
8
+ Examples:
9
+ # Show all messages for a user
10
+ rem session show 11111111-1111-1111-1111-111111111001
11
+
12
+ # Show only user messages
13
+ rem session show 11111111-1111-1111-1111-111111111001 --role user
14
+
15
+ # Simulate next user message
16
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next
17
+
18
+ # Simulate with custom prompt and save
19
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next --save \
20
+ --custom-sim-prompt "Respond as an anxious patient"
21
+ """
22
+
23
+ import asyncio
24
+ from pathlib import Path
25
+ from typing import Literal
26
+
27
+ import click
28
+ import yaml
29
+ from loguru import logger
30
+
31
+ from ...models.entities.user import User
32
+ from ...models.entities.message import Message
33
+ from ...services.postgres import get_postgres_service
34
+ from ...services.postgres.repository import Repository
35
+ from ...settings import settings
36
+
37
+
38
+ SIMULATOR_PROMPT = """You are simulating a patient in a mental health conversation.
39
+
40
+ ## Context
41
+ You are continuing a conversation with a clinical evaluation agent. Based on the
42
+ user profile and conversation history below, generate the next realistic patient message.
43
+
44
+ ## User Profile
45
+ {user_profile}
46
+
47
+ ## Conversation History
48
+ {conversation_history}
49
+
50
+ ## Instructions
51
+ - Stay in character as the patient described in the profile
52
+ - Your response should be natural, conversational, and consistent with the patient's presentation
53
+ - Consider the patient's risk level, symptoms, and communication style
54
+ - Do NOT include any metadata or role labels - just the raw message content
55
+ - Keep responses concise (1-3 sentences typical for conversation)
56
+
57
+ Generate the next patient message:"""
58
+
59
+
60
+ async def _load_user_and_messages(
61
+ user_id: str,
62
+ session_id: str | None = None,
63
+ role_filter: str | None = None,
64
+ limit: int = 100,
65
+ ) -> tuple[User | None, list[Message]]:
66
+ """Load user profile and messages from database."""
67
+ pg = get_postgres_service()
68
+ if not pg:
69
+ logger.error("PostgreSQL not available")
70
+ return None, []
71
+
72
+ await pg.connect()
73
+
74
+ try:
75
+ # Load user
76
+ user_repo = Repository(User, "users", db=pg)
77
+ user = await user_repo.get_by_id(user_id, tenant_id="default")
78
+
79
+ # Load messages
80
+ message_repo = Repository(Message, "messages", db=pg)
81
+ filters = {"user_id": user_id}
82
+ if session_id:
83
+ filters["session_id"] = session_id
84
+
85
+ messages = await message_repo.find(
86
+ filters=filters,
87
+ order_by="created_at ASC",
88
+ limit=limit,
89
+ )
90
+
91
+ # Filter by role if specified
92
+ if role_filter:
93
+ messages = [m for m in messages if m.message_type == role_filter]
94
+
95
+ return user, messages
96
+
97
+ finally:
98
+ await pg.disconnect()
99
+
100
+
101
+ def _format_user_yaml(user: User | None) -> str:
102
+ """Format user profile as YAML."""
103
+ if not user:
104
+ return "# No user found"
105
+
106
+ data = {
107
+ "id": str(user.id),
108
+ "name": user.name,
109
+ "summary": user.summary,
110
+ "interests": user.interests,
111
+ "preferred_topics": user.preferred_topics,
112
+ "metadata": user.metadata,
113
+ }
114
+ return yaml.dump(data, default_flow_style=False, allow_unicode=True)
115
+
116
+
117
+ def _format_messages_yaml(messages: list[Message]) -> str:
118
+ """Format messages as YAML."""
119
+ if not messages:
120
+ return "# No messages found"
121
+
122
+ data = []
123
+ for msg in messages:
124
+ data.append({
125
+ "role": msg.message_type or "unknown",
126
+ "content": msg.content,
127
+ "session_id": msg.session_id,
128
+ "created_at": msg.created_at.isoformat() if msg.created_at else None,
129
+ })
130
+ return yaml.dump(data, default_flow_style=False, allow_unicode=True)
131
+
132
+
133
+ def _format_conversation_for_llm(messages: list[Message]) -> str:
134
+ """Format conversation history for LLM context."""
135
+ lines = []
136
+ for msg in messages:
137
+ role = msg.message_type or "unknown"
138
+ lines.append(f"[{role.upper()}]: {msg.content}")
139
+ return "\n\n".join(lines) if lines else "(No previous messages)"
140
+
141
+
142
+ async def _simulate_next_message(
143
+ user: User | None,
144
+ messages: list[Message],
145
+ custom_prompt: str | None = None,
146
+ ) -> str:
147
+ """Use LLM to simulate the next patient message."""
148
+ from pydantic_ai import Agent
149
+
150
+ # Build context
151
+ user_profile = _format_user_yaml(user) if user else "Unknown patient"
152
+ conversation_history = _format_conversation_for_llm(messages)
153
+
154
+ # Use custom prompt or default
155
+ if custom_prompt:
156
+ # Check if it's a file path
157
+ if Path(custom_prompt).exists():
158
+ prompt_template = Path(custom_prompt).read_text()
159
+ else:
160
+ prompt_template = custom_prompt
161
+ # Simple variable substitution
162
+ prompt = prompt_template.replace("{user_profile}", user_profile)
163
+ prompt = prompt.replace("{conversation_history}", conversation_history)
164
+ else:
165
+ prompt = SIMULATOR_PROMPT.format(
166
+ user_profile=user_profile,
167
+ conversation_history=conversation_history,
168
+ )
169
+
170
+ # Create simple agent for simulation
171
+ agent = Agent(
172
+ model=settings.llm.default_model,
173
+ system_prompt="You are a patient simulator. Generate realistic patient responses.",
174
+ )
175
+
176
+ result = await agent.run(prompt)
177
+ return result.output
178
+
179
+
180
+ async def _save_message(
181
+ user_id: str,
182
+ session_id: str | None,
183
+ content: str,
184
+ role: str = "user",
185
+ ) -> Message:
186
+ """Save a simulated message to the database."""
187
+ from uuid import uuid4
188
+
189
+ pg = get_postgres_service()
190
+ if not pg:
191
+ raise RuntimeError("PostgreSQL not available")
192
+
193
+ await pg.connect()
194
+
195
+ try:
196
+ message_repo = Repository(Message, "messages", db=pg)
197
+
198
+ message = Message(
199
+ id=uuid4(),
200
+ user_id=user_id,
201
+ tenant_id="default",
202
+ session_id=session_id or str(uuid4()),
203
+ content=content,
204
+ message_type=role,
205
+ )
206
+
207
+ await message_repo.upsert(message)
208
+ return message
209
+
210
+ finally:
211
+ await pg.disconnect()
212
+
213
+
214
+ @click.group()
215
+ def session():
216
+ """Session viewing and simulation commands."""
217
+ pass
218
+
219
+
220
+ @session.command("show")
221
+ @click.argument("user_id")
222
+ @click.option("--session-id", "-s", help="Filter by session ID")
223
+ @click.option(
224
+ "--role", "-r",
225
+ type=click.Choice(["user", "assistant", "system", "tool"]),
226
+ help="Filter messages by role",
227
+ )
228
+ @click.option("--limit", "-l", default=100, help="Max messages to load")
229
+ @click.option("--simulate-next", is_flag=True, help="Simulate the next patient message")
230
+ @click.option("--save", is_flag=True, help="Save simulated message to database")
231
+ @click.option(
232
+ "--custom-sim-prompt", "-p",
233
+ help="Custom simulation prompt (text or file path)",
234
+ )
235
+ def show(
236
+ user_id: str,
237
+ session_id: str | None,
238
+ role: str | None,
239
+ limit: int,
240
+ simulate_next: bool,
241
+ save: bool,
242
+ custom_sim_prompt: str | None,
243
+ ):
244
+ """
245
+ Show user profile and session messages.
246
+
247
+ USER_ID: The user identifier to load.
248
+
249
+ Examples:
250
+
251
+ # Show user and all messages
252
+ rem session show 11111111-1111-1111-1111-111111111001
253
+
254
+ # Show only assistant responses
255
+ rem session show 11111111-1111-1111-1111-111111111001 --role assistant
256
+
257
+ # Simulate next patient message
258
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next
259
+
260
+ # Simulate and save to database
261
+ rem session show 11111111-1111-1111-1111-111111111001 --simulate-next --save
262
+ """
263
+ asyncio.run(_show_async(
264
+ user_id=user_id,
265
+ session_id=session_id,
266
+ role_filter=role,
267
+ limit=limit,
268
+ simulate_next=simulate_next,
269
+ save=save,
270
+ custom_sim_prompt=custom_sim_prompt,
271
+ ))
272
+
273
+
274
+ async def _show_async(
275
+ user_id: str,
276
+ session_id: str | None,
277
+ role_filter: str | None,
278
+ limit: int,
279
+ simulate_next: bool,
280
+ save: bool,
281
+ custom_sim_prompt: str | None,
282
+ ):
283
+ """Async implementation of show command."""
284
+ # Load data
285
+ user, messages = await _load_user_and_messages(
286
+ user_id=user_id,
287
+ session_id=session_id,
288
+ role_filter=role_filter if not simulate_next else None, # Need all messages for simulation
289
+ limit=limit,
290
+ )
291
+
292
+ # Display user profile
293
+ click.echo("\n# User Profile")
294
+ click.echo("---")
295
+ click.echo(_format_user_yaml(user))
296
+
297
+ # Display messages (apply filter for display if simulating)
298
+ display_messages = messages
299
+ if simulate_next and role_filter:
300
+ display_messages = [m for m in messages if m.message_type == role_filter]
301
+
302
+ click.echo("\n# Messages")
303
+ click.echo("---")
304
+ click.echo(_format_messages_yaml(display_messages))
305
+
306
+ # Simulate next message if requested
307
+ if simulate_next:
308
+ click.echo("\n# Simulated Next Message")
309
+ click.echo("---")
310
+
311
+ try:
312
+ simulated = await _simulate_next_message(
313
+ user=user,
314
+ messages=messages,
315
+ custom_prompt=custom_sim_prompt,
316
+ )
317
+ click.echo(f"role: user")
318
+ click.echo(f"content: |\n {simulated}")
319
+
320
+ if save:
321
+ saved_msg = await _save_message(
322
+ user_id=user_id,
323
+ session_id=session_id,
324
+ content=simulated,
325
+ role="user",
326
+ )
327
+ logger.success(f"Saved message: {saved_msg.id}")
328
+
329
+ except Exception as e:
330
+ logger.error(f"Simulation failed: {e}")
331
+ raise
332
+
333
+
334
+ @session.command("clone")
335
+ @click.argument("session_id")
336
+ @click.option("--to-turn", "-t", type=int, help="Clone up to turn N (counting user messages only)")
337
+ @click.option("--name", "-n", help="Name/description for the cloned session")
338
+ def clone(session_id: str, to_turn: int | None, name: str | None):
339
+ """
340
+ Clone a session for exploring alternate conversation paths.
341
+
342
+ SESSION_ID: The session ID to clone.
343
+
344
+ Examples:
345
+
346
+ # Clone entire session
347
+ rem session clone 810f1f2d-d5a1-4c02-83b6-67040b47f7c0
348
+
349
+ # Clone up to turn 3 (first 3 user messages and their responses)
350
+ rem session clone 810f1f2d-d5a1-4c02-83b6-67040b47f7c0 --to-turn 3
351
+
352
+ # Clone with a descriptive name
353
+ rem session clone 810f1f2d-d5a1-4c02-83b6-67040b47f7c0 -n "Alternate anxiety path"
354
+ """
355
+ asyncio.run(_clone_async(session_id, to_turn, name))
356
+
357
+
358
+ async def _clone_async(
359
+ session_id: str,
360
+ to_turn: int | None,
361
+ name: str | None,
362
+ ):
363
+ """Async implementation of clone command."""
364
+ from uuid import uuid4
365
+ from ...models.entities.session import Session, SessionMode
366
+
367
+ pg = get_postgres_service()
368
+ if not pg:
369
+ logger.error("PostgreSQL not available")
370
+ return
371
+
372
+ await pg.connect()
373
+
374
+ try:
375
+ # Load original session messages
376
+ message_repo = Repository(Message, "messages", db=pg)
377
+ messages = await message_repo.find(
378
+ filters={"session_id": session_id},
379
+ order_by="created_at ASC",
380
+ limit=1000,
381
+ )
382
+
383
+ if not messages:
384
+ logger.error(f"No messages found for session {session_id}")
385
+ return
386
+
387
+ # If --to-turn specified, filter messages up to that turn (user messages)
388
+ if to_turn is not None:
389
+ user_count = 0
390
+ cutoff_idx = len(messages)
391
+ for idx, msg in enumerate(messages):
392
+ if msg.message_type == "user":
393
+ user_count += 1
394
+ if user_count > to_turn:
395
+ cutoff_idx = idx
396
+ break
397
+ messages = messages[:cutoff_idx]
398
+ logger.info(f"Cloning {len(messages)} messages (up to turn {to_turn})")
399
+ else:
400
+ logger.info(f"Cloning all {len(messages)} messages")
401
+
402
+ # Generate new session ID
403
+ new_session_id = str(uuid4())
404
+
405
+ # Get user_id and tenant_id from first message
406
+ first_msg = messages[0]
407
+ user_id = first_msg.user_id
408
+ tenant_id = first_msg.tenant_id or "default"
409
+
410
+ # Create Session record with CLONE mode and lineage
411
+ session_repo = Repository(Session, "sessions", db=pg)
412
+ new_session = Session(
413
+ id=uuid4(),
414
+ name=name or f"Clone of {session_id[:8]}",
415
+ mode=SessionMode.CLONE,
416
+ original_trace_id=session_id,
417
+ description=f"Cloned from session {session_id}" + (f" at turn {to_turn}" if to_turn else ""),
418
+ user_id=user_id,
419
+ tenant_id=tenant_id,
420
+ message_count=len(messages),
421
+ )
422
+ await session_repo.upsert(new_session)
423
+ logger.info(f"Created session record: {new_session.id}")
424
+
425
+ # Copy messages with new session_id
426
+ for msg in messages:
427
+ new_msg = Message(
428
+ id=uuid4(),
429
+ user_id=msg.user_id,
430
+ tenant_id=msg.tenant_id,
431
+ session_id=str(new_session.id),
432
+ content=msg.content,
433
+ message_type=msg.message_type,
434
+ metadata=msg.metadata,
435
+ )
436
+ await message_repo.upsert(new_msg)
437
+
438
+ click.echo(f"\n✅ Cloned session successfully!")
439
+ click.echo(f" Original: {session_id}")
440
+ click.echo(f" New: {new_session.id}")
441
+ click.echo(f" Messages: {len(messages)}")
442
+ if to_turn:
443
+ click.echo(f" Turns: {to_turn}")
444
+ click.echo(f"\nContinue this session with:")
445
+ click.echo(f" rem ask <agent> \"your message\" --session-id {new_session.id}")
446
+
447
+ finally:
448
+ await pg.disconnect()
449
+
450
+
451
+ def register_command(cli_group):
452
+ """Register the session command group."""
453
+ cli_group.add_command(session)