remdb 0.2.6__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 (187) hide show
  1. rem/__init__.py +2 -0
  2. rem/agentic/README.md +650 -0
  3. rem/agentic/__init__.py +39 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +8 -0
  6. rem/agentic/context.py +148 -0
  7. rem/agentic/context_builder.py +329 -0
  8. rem/agentic/mcp/__init__.py +0 -0
  9. rem/agentic/mcp/tool_wrapper.py +107 -0
  10. rem/agentic/otel/__init__.py +5 -0
  11. rem/agentic/otel/setup.py +151 -0
  12. rem/agentic/providers/phoenix.py +674 -0
  13. rem/agentic/providers/pydantic_ai.py +572 -0
  14. rem/agentic/query.py +117 -0
  15. rem/agentic/query_helper.py +89 -0
  16. rem/agentic/schema.py +396 -0
  17. rem/agentic/serialization.py +245 -0
  18. rem/agentic/tools/__init__.py +5 -0
  19. rem/agentic/tools/rem_tools.py +231 -0
  20. rem/api/README.md +420 -0
  21. rem/api/main.py +324 -0
  22. rem/api/mcp_router/prompts.py +182 -0
  23. rem/api/mcp_router/resources.py +536 -0
  24. rem/api/mcp_router/server.py +213 -0
  25. rem/api/mcp_router/tools.py +584 -0
  26. rem/api/routers/auth.py +229 -0
  27. rem/api/routers/chat/__init__.py +5 -0
  28. rem/api/routers/chat/completions.py +281 -0
  29. rem/api/routers/chat/json_utils.py +76 -0
  30. rem/api/routers/chat/models.py +124 -0
  31. rem/api/routers/chat/streaming.py +185 -0
  32. rem/auth/README.md +258 -0
  33. rem/auth/__init__.py +26 -0
  34. rem/auth/middleware.py +100 -0
  35. rem/auth/providers/__init__.py +13 -0
  36. rem/auth/providers/base.py +376 -0
  37. rem/auth/providers/google.py +163 -0
  38. rem/auth/providers/microsoft.py +237 -0
  39. rem/cli/README.md +455 -0
  40. rem/cli/__init__.py +8 -0
  41. rem/cli/commands/README.md +126 -0
  42. rem/cli/commands/__init__.py +3 -0
  43. rem/cli/commands/ask.py +565 -0
  44. rem/cli/commands/configure.py +423 -0
  45. rem/cli/commands/db.py +493 -0
  46. rem/cli/commands/dreaming.py +324 -0
  47. rem/cli/commands/experiments.py +1124 -0
  48. rem/cli/commands/mcp.py +66 -0
  49. rem/cli/commands/process.py +245 -0
  50. rem/cli/commands/schema.py +183 -0
  51. rem/cli/commands/serve.py +106 -0
  52. rem/cli/dreaming.py +363 -0
  53. rem/cli/main.py +88 -0
  54. rem/config.py +237 -0
  55. rem/mcp_server.py +41 -0
  56. rem/models/core/__init__.py +49 -0
  57. rem/models/core/core_model.py +64 -0
  58. rem/models/core/engram.py +333 -0
  59. rem/models/core/experiment.py +628 -0
  60. rem/models/core/inline_edge.py +132 -0
  61. rem/models/core/rem_query.py +243 -0
  62. rem/models/entities/__init__.py +43 -0
  63. rem/models/entities/file.py +57 -0
  64. rem/models/entities/image_resource.py +88 -0
  65. rem/models/entities/message.py +35 -0
  66. rem/models/entities/moment.py +123 -0
  67. rem/models/entities/ontology.py +191 -0
  68. rem/models/entities/ontology_config.py +131 -0
  69. rem/models/entities/resource.py +95 -0
  70. rem/models/entities/schema.py +87 -0
  71. rem/models/entities/user.py +85 -0
  72. rem/py.typed +0 -0
  73. rem/schemas/README.md +507 -0
  74. rem/schemas/__init__.py +6 -0
  75. rem/schemas/agents/README.md +92 -0
  76. rem/schemas/agents/core/moment-builder.yaml +178 -0
  77. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  78. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  79. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  80. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  81. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  82. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  83. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  84. rem/schemas/agents/examples/hello-world.yaml +37 -0
  85. rem/schemas/agents/examples/query.yaml +54 -0
  86. rem/schemas/agents/examples/simple.yaml +21 -0
  87. rem/schemas/agents/examples/test.yaml +29 -0
  88. rem/schemas/agents/rem.yaml +128 -0
  89. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  90. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  91. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  92. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  93. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  94. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  95. rem/services/__init__.py +16 -0
  96. rem/services/audio/INTEGRATION.md +308 -0
  97. rem/services/audio/README.md +376 -0
  98. rem/services/audio/__init__.py +15 -0
  99. rem/services/audio/chunker.py +354 -0
  100. rem/services/audio/transcriber.py +259 -0
  101. rem/services/content/README.md +1269 -0
  102. rem/services/content/__init__.py +5 -0
  103. rem/services/content/providers.py +806 -0
  104. rem/services/content/service.py +657 -0
  105. rem/services/dreaming/README.md +230 -0
  106. rem/services/dreaming/__init__.py +53 -0
  107. rem/services/dreaming/affinity_service.py +336 -0
  108. rem/services/dreaming/moment_service.py +264 -0
  109. rem/services/dreaming/ontology_service.py +54 -0
  110. rem/services/dreaming/user_model_service.py +297 -0
  111. rem/services/dreaming/utils.py +39 -0
  112. rem/services/embeddings/__init__.py +11 -0
  113. rem/services/embeddings/api.py +120 -0
  114. rem/services/embeddings/worker.py +421 -0
  115. rem/services/fs/README.md +662 -0
  116. rem/services/fs/__init__.py +62 -0
  117. rem/services/fs/examples.py +206 -0
  118. rem/services/fs/examples_paths.py +204 -0
  119. rem/services/fs/git_provider.py +935 -0
  120. rem/services/fs/local_provider.py +760 -0
  121. rem/services/fs/parsing-hooks-examples.md +172 -0
  122. rem/services/fs/paths.py +276 -0
  123. rem/services/fs/provider.py +460 -0
  124. rem/services/fs/s3_provider.py +1042 -0
  125. rem/services/fs/service.py +186 -0
  126. rem/services/git/README.md +1075 -0
  127. rem/services/git/__init__.py +17 -0
  128. rem/services/git/service.py +469 -0
  129. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  130. rem/services/phoenix/README.md +453 -0
  131. rem/services/phoenix/__init__.py +46 -0
  132. rem/services/phoenix/client.py +686 -0
  133. rem/services/phoenix/config.py +88 -0
  134. rem/services/phoenix/prompt_labels.py +477 -0
  135. rem/services/postgres/README.md +575 -0
  136. rem/services/postgres/__init__.py +23 -0
  137. rem/services/postgres/migration_service.py +427 -0
  138. rem/services/postgres/pydantic_to_sqlalchemy.py +232 -0
  139. rem/services/postgres/register_type.py +352 -0
  140. rem/services/postgres/repository.py +337 -0
  141. rem/services/postgres/schema_generator.py +379 -0
  142. rem/services/postgres/service.py +802 -0
  143. rem/services/postgres/sql_builder.py +354 -0
  144. rem/services/rem/README.md +304 -0
  145. rem/services/rem/__init__.py +23 -0
  146. rem/services/rem/exceptions.py +71 -0
  147. rem/services/rem/executor.py +293 -0
  148. rem/services/rem/parser.py +145 -0
  149. rem/services/rem/queries.py +196 -0
  150. rem/services/rem/query.py +371 -0
  151. rem/services/rem/service.py +527 -0
  152. rem/services/session/README.md +374 -0
  153. rem/services/session/__init__.py +6 -0
  154. rem/services/session/compression.py +360 -0
  155. rem/services/session/reload.py +77 -0
  156. rem/settings.py +1235 -0
  157. rem/sql/002_install_models.sql +1068 -0
  158. rem/sql/background_indexes.sql +42 -0
  159. rem/sql/install_models.sql +1038 -0
  160. rem/sql/migrations/001_install.sql +503 -0
  161. rem/sql/migrations/002_install_models.sql +1202 -0
  162. rem/utils/AGENTIC_CHUNKING.md +597 -0
  163. rem/utils/README.md +583 -0
  164. rem/utils/__init__.py +43 -0
  165. rem/utils/agentic_chunking.py +622 -0
  166. rem/utils/batch_ops.py +343 -0
  167. rem/utils/chunking.py +108 -0
  168. rem/utils/clip_embeddings.py +276 -0
  169. rem/utils/dict_utils.py +98 -0
  170. rem/utils/embeddings.py +423 -0
  171. rem/utils/examples/embeddings_example.py +305 -0
  172. rem/utils/examples/sql_types_example.py +202 -0
  173. rem/utils/markdown.py +16 -0
  174. rem/utils/model_helpers.py +236 -0
  175. rem/utils/schema_loader.py +229 -0
  176. rem/utils/sql_types.py +348 -0
  177. rem/utils/user_id.py +81 -0
  178. rem/utils/vision.py +330 -0
  179. rem/workers/README.md +506 -0
  180. rem/workers/__init__.py +5 -0
  181. rem/workers/dreaming.py +502 -0
  182. rem/workers/engram_processor.py +312 -0
  183. rem/workers/sqs_file_processor.py +193 -0
  184. remdb-0.2.6.dist-info/METADATA +1191 -0
  185. remdb-0.2.6.dist-info/RECORD +187 -0
  186. remdb-0.2.6.dist-info/WHEEL +4 -0
  187. remdb-0.2.6.dist-info/entry_points.txt +2 -0
rem/cli/dreaming.py ADDED
@@ -0,0 +1,363 @@
1
+ """
2
+ REM Dreaming CLI - Memory indexing and insight extraction.
3
+
4
+ Command-line interface for running dreaming workers to build the
5
+ REM knowledge graph through user model updates, moment construction,
6
+ and resource affinity operations.
7
+
8
+ Commands:
9
+ - user-model: Update user profiles from activity
10
+ - moments: Extract temporal narratives from resources
11
+ - affinity: Build semantic relationships between resources
12
+ - custom: Run custom extractors on user's resources/sessions
13
+ - full: Run complete dreaming workflow (all operations)
14
+
15
+ Usage Examples:
16
+ ```bash
17
+ # Update user model for specific user
18
+ rem-dreaming user-model --user-id=user-123
19
+
20
+ # Extract moments with custom lookback
21
+ rem-dreaming moments --user-id=user-123 --lookback-hours=48
22
+
23
+ # Build resource affinity (semantic mode, fast)
24
+ rem-dreaming affinity --user-id=user-123
25
+
26
+ # Build resource affinity (LLM mode, intelligent but expensive)
27
+ rem-dreaming affinity --user-id=user-123 --use-llm --limit=100
28
+
29
+ # Run custom extractor on user's data
30
+ rem-dreaming custom --user-id=user-123 --extractor cv-parser-v1
31
+ rem-dreaming custom --user-id=user-123 --extractor contract-analyzer-v1 --lookback-hours=168
32
+
33
+ # Run full workflow for user
34
+ rem-dreaming full --user-id=user-123
35
+
36
+ # Process all active users (daily cron)
37
+ rem-dreaming full --all-users
38
+
39
+ # Process with custom REM API endpoint
40
+ rem-dreaming full --user-id=user-123 --rem-api-url=http://localhost:8000
41
+ ```
42
+
43
+ Environment Variables:
44
+ - REM_API_URL: REM API endpoint (default: http://rem-api:8000)
45
+ - REM_EMBEDDING_PROVIDER: Embedding provider (default: text-embedding-3-small)
46
+ - REM_DEFAULT_MODEL: LLM model (default: gpt-4o)
47
+ - REM_LOOKBACK_HOURS: Default lookback window (default: 24)
48
+ - OPENAI_API_KEY: OpenAI API key
49
+
50
+ Exit Codes:
51
+ - 0: Success
52
+ - 1: Validation error (missing required args)
53
+ - 2: Execution error (worker failed)
54
+ """
55
+
56
+ import asyncio
57
+ import os
58
+ import sys
59
+ from typing import Optional
60
+
61
+ import typer
62
+ from rich.console import Console
63
+ from rich.progress import Progress, SpinnerColumn, TextColumn
64
+
65
+ from rem.workers.dreaming import (
66
+ AffinityMode,
67
+ DreamingWorker,
68
+ TaskType,
69
+ )
70
+
71
+ app = typer.Typer(
72
+ name="rem-dreaming",
73
+ help="REM dreaming worker for memory indexing",
74
+ add_completion=False,
75
+ )
76
+ console = Console()
77
+
78
+
79
+ def get_worker() -> DreamingWorker:
80
+ """Create dreaming worker from environment."""
81
+ return DreamingWorker(
82
+ rem_api_url=os.getenv("REM_API_URL", "http://rem-api:8000"),
83
+ embedding_provider=os.getenv(
84
+ "REM_EMBEDDING_PROVIDER", "text-embedding-3-small"
85
+ ),
86
+ default_model=os.getenv("REM_DEFAULT_MODEL", "gpt-4o"),
87
+ lookback_hours=int(os.getenv("REM_LOOKBACK_HOURS", "24")),
88
+ )
89
+
90
+
91
+ @app.command()
92
+ def user_model(
93
+ user_id: str = typer.Option(..., help="User ID to process"),
94
+ max_sessions: int = typer.Option(100, help="Max sessions to analyze"),
95
+ max_moments: int = typer.Option(20, help="Max moments to include"),
96
+ max_resources: int = typer.Option(20, help="Max resources to include"),
97
+ ):
98
+ """
99
+ Update user model from recent activity.
100
+
101
+ Reads recent sessions, moments, and resources to generate
102
+ a comprehensive user profile summary using LLM analysis.
103
+ """
104
+
105
+ async def run():
106
+ worker = get_worker()
107
+ try:
108
+ with Progress(
109
+ SpinnerColumn(),
110
+ TextColumn("[progress.description]{task.description}"),
111
+ console=console,
112
+ ) as progress:
113
+ task = progress.add_task(
114
+ f"Updating user model for {user_id}...", total=None
115
+ )
116
+
117
+ result = await worker.update_user_model(
118
+ user_id=user_id,
119
+ max_sessions=max_sessions,
120
+ max_moments=max_moments,
121
+ max_resources=max_resources,
122
+ )
123
+
124
+ progress.update(task, completed=True)
125
+ console.print(f"[green]✓[/green] User model updated")
126
+ console.print(result)
127
+ except Exception as e:
128
+ console.print(f"[red]✗[/red] Failed: {e}", style="red")
129
+ sys.exit(2)
130
+ finally:
131
+ await worker.close()
132
+
133
+ asyncio.run(run())
134
+
135
+
136
+ @app.command()
137
+ def moments(
138
+ user_id: str = typer.Option(..., help="User ID to process"),
139
+ lookback_hours: Optional[int] = typer.Option(
140
+ None, help="Hours to look back (default: from env)"
141
+ ),
142
+ limit: Optional[int] = typer.Option(None, help="Max resources to process"),
143
+ ):
144
+ """
145
+ Extract moments from resources.
146
+
147
+ Analyzes recent resources to identify temporal narratives
148
+ (meetings, coding sessions, conversations) and creates
149
+ Moment entities with temporal boundaries and metadata.
150
+ """
151
+
152
+ async def run():
153
+ worker = get_worker()
154
+ try:
155
+ with Progress(
156
+ SpinnerColumn(),
157
+ TextColumn("[progress.description]{task.description}"),
158
+ console=console,
159
+ ) as progress:
160
+ task = progress.add_task(
161
+ f"Constructing moments for {user_id}...", total=None
162
+ )
163
+
164
+ result = await worker.construct_moments(
165
+ user_id=user_id,
166
+ lookback_hours=lookback_hours,
167
+ limit=limit,
168
+ )
169
+
170
+ progress.update(task, completed=True)
171
+ console.print(f"[green]✓[/green] Moments constructed")
172
+ console.print(result)
173
+ except Exception as e:
174
+ console.print(f"[red]✗[/red] Failed: {e}", style="red")
175
+ sys.exit(2)
176
+ finally:
177
+ await worker.close()
178
+
179
+ asyncio.run(run())
180
+
181
+
182
+ @app.command()
183
+ def affinity(
184
+ user_id: str = typer.Option(..., help="User ID to process"),
185
+ use_llm: bool = typer.Option(
186
+ False, "--use-llm", help="Use LLM mode (expensive, use --limit)"
187
+ ),
188
+ lookback_hours: Optional[int] = typer.Option(
189
+ None, help="Hours to look back (default: from env)"
190
+ ),
191
+ limit: Optional[int] = typer.Option(
192
+ None, help="Max resources to process (REQUIRED for LLM mode)"
193
+ ),
194
+ ):
195
+ """
196
+ Build resource affinity graph.
197
+
198
+ Creates semantic relationships between resources using either
199
+ vector similarity (fast, default) or LLM analysis (intelligent but expensive).
200
+
201
+ Semantic Mode (default):
202
+ - Fast vector similarity search
203
+ - No LLM calls, just embedding cosine similarity
204
+ - Good for frequent updates
205
+
206
+ LLM Mode (--use-llm):
207
+ - Intelligent relationship assessment
208
+ - Expensive: ALWAYS use --limit to control costs
209
+ - Good for deep analysis (weekly or monthly)
210
+
211
+ Example:
212
+ # Semantic mode (fast, cheap)
213
+ rem-dreaming affinity --user-id=user-123
214
+
215
+ # LLM mode (intelligent, expensive)
216
+ rem-dreaming affinity --user-id=user-123 --use-llm --limit=100
217
+ """
218
+ if use_llm and not limit:
219
+ console.print(
220
+ "[red]Error:[/red] --limit is REQUIRED when using --use-llm to control costs",
221
+ style="red",
222
+ )
223
+ sys.exit(1)
224
+
225
+ async def run():
226
+ worker = get_worker()
227
+ try:
228
+ mode = AffinityMode.LLM if use_llm else AffinityMode.SEMANTIC
229
+ mode_str = "LLM" if use_llm else "semantic"
230
+
231
+ with Progress(
232
+ SpinnerColumn(),
233
+ TextColumn("[progress.description]{task.description}"),
234
+ console=console,
235
+ ) as progress:
236
+ task = progress.add_task(
237
+ f"Building {mode_str} affinity for {user_id}...", total=None
238
+ )
239
+
240
+ result = await worker.build_affinity(
241
+ user_id=user_id,
242
+ mode=mode,
243
+ lookback_hours=lookback_hours,
244
+ limit=limit,
245
+ )
246
+
247
+ progress.update(task, completed=True)
248
+ console.print(f"[green]✓[/green] Resource affinity built ({mode_str} mode)")
249
+ console.print(result)
250
+ except Exception as e:
251
+ console.print(f"[red]✗[/red] Failed: {e}", style="red")
252
+ sys.exit(2)
253
+ finally:
254
+ await worker.close()
255
+
256
+ asyncio.run(run())
257
+
258
+
259
+ @app.command()
260
+ def full(
261
+ user_id: Optional[str] = typer.Option(None, help="User ID (or --all-users)"),
262
+ all_users: bool = typer.Option(
263
+ False, "--all-users", help="Process all active users"
264
+ ),
265
+ use_llm_affinity: bool = typer.Option(
266
+ False, "--use-llm-affinity", help="Use LLM mode for affinity (expensive)"
267
+ ),
268
+ lookback_hours: Optional[int] = typer.Option(
269
+ None, help="Hours to look back (default: from env)"
270
+ ),
271
+ ):
272
+ """
273
+ Run complete dreaming workflow.
274
+
275
+ Executes all dreaming operations in sequence:
276
+ 1. Update user model
277
+ 2. Construct moments
278
+ 3. Build resource affinity
279
+
280
+ Recommended for daily cron execution.
281
+
282
+ Examples:
283
+ # Process single user
284
+ rem-dreaming full --user-id=user-123
285
+
286
+ # Process all active users (daily cron)
287
+ rem-dreaming full --all-users
288
+
289
+ # Use LLM affinity mode (expensive)
290
+ rem-dreaming full --user-id=user-123 --use-llm-affinity
291
+ """
292
+ if not user_id and not all_users:
293
+ console.print(
294
+ "[red]Error:[/red] Either --user-id or --all-users is required",
295
+ style="red",
296
+ )
297
+ sys.exit(1)
298
+
299
+ if user_id and all_users:
300
+ console.print(
301
+ "[red]Error:[/red] Cannot use both --user-id and --all-users",
302
+ style="red",
303
+ )
304
+ sys.exit(1)
305
+
306
+ async def run():
307
+ worker = get_worker()
308
+ try:
309
+ if all_users:
310
+ with Progress(
311
+ SpinnerColumn(),
312
+ TextColumn("[progress.description]{task.description}"),
313
+ console=console,
314
+ ) as progress:
315
+ task = progress.add_task("Processing all users...", total=None)
316
+
317
+ results = await worker.process_all_users(
318
+ task_type=TaskType.FULL,
319
+ use_llm_affinity=use_llm_affinity,
320
+ lookback_hours=lookback_hours,
321
+ )
322
+
323
+ progress.update(task, completed=True)
324
+ console.print(
325
+ f"[green]✓[/green] Processed {len(results)} users"
326
+ )
327
+ for result in results:
328
+ console.print(result)
329
+ else:
330
+ with Progress(
331
+ SpinnerColumn(),
332
+ TextColumn("[progress.description]{task.description}"),
333
+ console=console,
334
+ ) as progress:
335
+ task = progress.add_task(
336
+ f"Running full workflow for {user_id}...", total=None
337
+ )
338
+
339
+ result = await worker.process_full(
340
+ user_id=user_id,
341
+ use_llm_affinity=use_llm_affinity,
342
+ lookback_hours=lookback_hours,
343
+ )
344
+
345
+ progress.update(task, completed=True)
346
+ console.print(f"[green]✓[/green] Full workflow completed")
347
+ console.print(result)
348
+ except Exception as e:
349
+ console.print(f"[red]✗[/red] Failed: {e}", style="red")
350
+ sys.exit(2)
351
+ finally:
352
+ await worker.close()
353
+
354
+ asyncio.run(run())
355
+
356
+
357
+ def main():
358
+ """Entry point."""
359
+ app()
360
+
361
+
362
+ if __name__ == "__main__":
363
+ main()
rem/cli/main.py ADDED
@@ -0,0 +1,88 @@
1
+ """
2
+ REM CLI entry point.
3
+
4
+ Usage:
5
+ rem db schema generate --models src/rem/models/entities
6
+ rem db schema validate
7
+ rem db migrate up
8
+ rem dev run-server
9
+ """
10
+
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import click
15
+ from loguru import logger
16
+
17
+
18
+ @click.group()
19
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging")
20
+ def cli(verbose: bool):
21
+ """REM - Resources Entities Moments system CLI."""
22
+ if verbose:
23
+ logger.remove()
24
+ logger.add(sys.stderr, level="DEBUG")
25
+ else:
26
+ logger.remove()
27
+ logger.add(sys.stderr, level="INFO")
28
+
29
+
30
+ @cli.group()
31
+ def db():
32
+ """Database operations (schema, migrate, status, etc.)."""
33
+ pass
34
+
35
+
36
+ @db.group()
37
+ def schema():
38
+ """Database schema management commands."""
39
+ pass
40
+
41
+
42
+ @cli.group()
43
+ def dev():
44
+ """Development utilities."""
45
+ pass
46
+
47
+
48
+ @cli.group()
49
+ def process():
50
+ """File processing commands."""
51
+ pass
52
+
53
+
54
+ @cli.group()
55
+ def dreaming():
56
+ """Memory indexing and knowledge extraction."""
57
+ pass
58
+
59
+
60
+ # Register commands
61
+ from .commands.schema import register_commands as register_schema_commands
62
+ from .commands.db import register_commands as register_db_commands
63
+ from .commands.process import register_commands as register_process_commands
64
+ from .commands.ask import register_command as register_ask_command
65
+ from .commands.dreaming import register_commands as register_dreaming_commands
66
+ from .commands.experiments import experiments as experiments_group
67
+ from .commands.configure import register_command as register_configure_command
68
+ from .commands.serve import register_command as register_serve_command
69
+ from .commands.mcp import register_command as register_mcp_command
70
+
71
+ register_schema_commands(schema)
72
+ register_db_commands(db)
73
+ register_process_commands(process)
74
+ register_dreaming_commands(dreaming)
75
+ register_ask_command(cli)
76
+ register_configure_command(cli)
77
+ register_serve_command(cli)
78
+ register_mcp_command(cli)
79
+ cli.add_command(experiments_group)
80
+
81
+
82
+ def main():
83
+ """Main entry point for CLI."""
84
+ cli()
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
rem/config.py ADDED
@@ -0,0 +1,237 @@
1
+ """
2
+ REM Configuration Management.
3
+
4
+ Provides persistent configuration in ~/.rem/config.yaml with environment variable overrides.
5
+
6
+ Configuration Precedence (highest to lowest):
7
+ 1. Environment variables (POSTGRES__CONNECTION_STRING, etc.)
8
+ 2. ~/.rem/config.yaml (user configuration)
9
+ 3. Default values (from settings.py)
10
+
11
+ File Format (~/.rem/config.yaml):
12
+ postgres:
13
+ connection_string: postgresql://user:pass@localhost:5432/rem
14
+ pool_min_size: 5
15
+ pool_max_size: 20
16
+
17
+ llm:
18
+ default_model: anthropic:claude-sonnet-4-5-20250929
19
+ openai_api_key: sk-...
20
+ anthropic_api_key: sk-ant-...
21
+
22
+ s3:
23
+ bucket_name: rem-storage
24
+ region: us-east-1
25
+ endpoint_url: http://localhost:9000
26
+
27
+ # Additional custom environment variables
28
+ env:
29
+ MY_CUSTOM_VAR: value
30
+
31
+ Usage:
32
+ from rem.config import load_config, get_config_path, ensure_config_dir
33
+
34
+ # Load configuration and merge with environment
35
+ config = load_config()
36
+
37
+ # Get configuration file path
38
+ config_path = get_config_path()
39
+
40
+ # Ensure ~/.rem directory exists
41
+ ensure_config_dir()
42
+ """
43
+
44
+ import os
45
+ from pathlib import Path
46
+ from typing import Any
47
+
48
+ import yaml
49
+ from loguru import logger
50
+
51
+
52
+ def get_rem_home() -> Path:
53
+ """
54
+ Get REM home directory (~/.rem).
55
+
56
+ Returns:
57
+ Path to ~/.rem directory
58
+ """
59
+ return Path.home() / ".rem"
60
+
61
+
62
+ def ensure_config_dir() -> Path:
63
+ """
64
+ Ensure ~/.rem directory exists.
65
+
66
+ Returns:
67
+ Path to ~/.rem directory
68
+ """
69
+ rem_home = get_rem_home()
70
+ rem_home.mkdir(exist_ok=True, mode=0o700) # User-only permissions
71
+ return rem_home
72
+
73
+
74
+ def get_config_path() -> Path:
75
+ """
76
+ Get path to configuration file (~/.rem/config.yaml).
77
+
78
+ Returns:
79
+ Path to configuration file
80
+ """
81
+ return get_rem_home() / "config.yaml"
82
+
83
+
84
+ def config_exists() -> bool:
85
+ """
86
+ Check if configuration file exists.
87
+
88
+ Returns:
89
+ True if ~/.rem/config.yaml exists
90
+ """
91
+ return get_config_path().exists()
92
+
93
+
94
+ def load_config() -> dict[str, Any]:
95
+ """
96
+ Load configuration from ~/.rem/config.yaml.
97
+
98
+ Returns:
99
+ Configuration dictionary (empty if file doesn't exist)
100
+ """
101
+ config_path = get_config_path()
102
+
103
+ if not config_path.exists():
104
+ logger.debug(f"Configuration file not found: {config_path}")
105
+ return {}
106
+
107
+ try:
108
+ with open(config_path, "r") as f:
109
+ config = yaml.safe_load(f) or {}
110
+ logger.debug(f"Loaded configuration from {config_path}")
111
+ return config
112
+ except Exception as e:
113
+ logger.warning(f"Failed to load configuration from {config_path}: {e}")
114
+ return {}
115
+
116
+
117
+ def save_config(config: dict[str, Any]) -> None:
118
+ """
119
+ Save configuration to ~/.rem/config.yaml.
120
+
121
+ Args:
122
+ config: Configuration dictionary to save
123
+ """
124
+ ensure_config_dir()
125
+ config_path = get_config_path()
126
+
127
+ try:
128
+ with open(config_path, "w") as f:
129
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
130
+ logger.info(f"Configuration saved to {config_path}")
131
+ except Exception as e:
132
+ logger.error(f"Failed to save configuration to {config_path}: {e}")
133
+ raise
134
+
135
+
136
+ def merge_config_to_env(config: dict[str, Any]) -> None:
137
+ """
138
+ Merge configuration file into environment variables.
139
+
140
+ This allows Pydantic Settings to pick up values from the config file
141
+ by setting environment variables before settings initialization.
142
+
143
+ Precedence:
144
+ - Existing environment variables are NOT overwritten
145
+ - Only sets env vars if they don't already exist
146
+
147
+ Args:
148
+ config: Configuration dictionary from ~/.rem/config.yaml
149
+
150
+ Example:
151
+ config = {"postgres": {"connection_string": "postgresql://..."}}
152
+ merge_config_to_env(config)
153
+ # Sets POSTGRES__CONNECTION_STRING if not already set
154
+ """
155
+ # Handle custom env vars first
156
+ if "env" in config:
157
+ for key, value in config["env"].items():
158
+ if key not in os.environ:
159
+ os.environ[key] = str(value)
160
+ logger.debug(f"Set env var from config: {key}")
161
+
162
+ # Convert nested config to environment variables
163
+ for section, values in config.items():
164
+ if section == "env":
165
+ continue # Already handled
166
+
167
+ if not isinstance(values, dict):
168
+ continue
169
+
170
+ for key, value in values.items():
171
+ # Convert to environment variable format (SECTION__KEY)
172
+ env_key = f"{section.upper()}__{key.upper()}"
173
+
174
+ # Only set if not already in environment
175
+ if env_key not in os.environ:
176
+ os.environ[env_key] = str(value)
177
+ logger.debug(f"Set env var from config: {env_key}")
178
+
179
+
180
+ def validate_config(config: dict[str, Any]) -> list[str]:
181
+ """
182
+ Validate configuration for required fields.
183
+
184
+ Args:
185
+ config: Configuration dictionary
186
+
187
+ Returns:
188
+ List of validation error messages (empty if valid)
189
+ """
190
+ errors = []
191
+
192
+ # Postgres connection is required
193
+ postgres = config.get("postgres", {})
194
+ if not postgres.get("connection_string"):
195
+ errors.append("PostgreSQL connection string is required (postgres.connection_string)")
196
+
197
+ # Validate connection string format
198
+ conn_str = postgres.get("connection_string", "")
199
+ if conn_str and not conn_str.startswith("postgresql://"):
200
+ errors.append("PostgreSQL connection string must start with 'postgresql://'")
201
+
202
+ return errors
203
+
204
+
205
+ def get_default_config() -> dict[str, Any]:
206
+ """
207
+ Get default configuration template for new installations.
208
+
209
+ Returns:
210
+ Default configuration dictionary
211
+ """
212
+ return {
213
+ "postgres": {
214
+ "connection_string": "postgresql://rem:rem@localhost:5432/rem",
215
+ "pool_min_size": 5,
216
+ "pool_max_size": 20,
217
+ },
218
+ "llm": {
219
+ "default_model": "anthropic:claude-sonnet-4-5-20250929",
220
+ "default_temperature": 0.5,
221
+ # API keys will be prompted for in wizard
222
+ # "openai_api_key": "",
223
+ # "anthropic_api_key": "",
224
+ },
225
+ "s3": {
226
+ "bucket_name": "rem-storage",
227
+ "region": "us-east-1",
228
+ # Optional fields
229
+ # "endpoint_url": "http://localhost:9000", # For MinIO
230
+ # "access_key_id": "",
231
+ # "secret_access_key": "",
232
+ },
233
+ "env": {
234
+ # Custom environment variables
235
+ # "MY_VAR": "value",
236
+ },
237
+ }