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
rem/settings.py ADDED
@@ -0,0 +1,1877 @@
1
+ """
2
+ REM Settings and Configuration.
3
+
4
+ Pydantic settings with environment variable support:
5
+ - Nested settings with env_prefix for organization
6
+ - Environment variables use double underscore delimiter (ENV__NESTED__VAR)
7
+ - Sensitive defaults (auth disabled, OTEL disabled for local dev)
8
+ - Global settings singleton
9
+
10
+ Example .env file:
11
+ # API Server
12
+ API__HOST=0.0.0.0
13
+ API__PORT=8000
14
+ API__RELOAD=true
15
+ API__LOG_LEVEL=info
16
+
17
+ # LLM
18
+ LLM__DEFAULT_MODEL=openai:gpt-4.1
19
+ LLM__DEFAULT_TEMPERATURE=0.5
20
+ LLM__MAX_RETRIES=10
21
+ LLM__OPENAI_API_KEY=sk-...
22
+ LLM__ANTHROPIC_API_KEY=sk-ant-...
23
+
24
+ # Database (port 5051 for Docker Compose prebuilt, 5050 for local dev)
25
+ POSTGRES__CONNECTION_STRING=postgresql://rem:rem@localhost:5051/rem
26
+ POSTGRES__POOL_MIN_SIZE=5
27
+ POSTGRES__POOL_MAX_SIZE=20
28
+ POSTGRES__STATEMENT_TIMEOUT=30000
29
+
30
+ # Auth (disabled by default)
31
+ AUTH__ENABLED=false
32
+ AUTH__OIDC_ISSUER_URL=https://accounts.google.com
33
+ AUTH__OIDC_CLIENT_ID=your-client-id
34
+ AUTH__SESSION_SECRET=your-secret-key
35
+
36
+ # OpenTelemetry (disabled by default - enable via env var when collector available)
37
+ # Standard OTLP collector ports: 4317 (gRPC), 4318 (HTTP)
38
+ OTEL__ENABLED=false
39
+ OTEL__SERVICE_NAME=rem-api
40
+ OTEL__COLLECTOR_ENDPOINT=http://localhost:4317
41
+ OTEL__PROTOCOL=grpc
42
+
43
+ # Arize Phoenix (enabled by default - can be disabled via env var)
44
+ PHOENIX__ENABLED=true
45
+ PHOENIX__COLLECTOR_ENDPOINT=http://localhost:6006/v1/traces
46
+ PHOENIX__PROJECT_NAME=rem
47
+
48
+ # S3 Storage
49
+ S3__BUCKET_NAME=rem-storage
50
+ S3__REGION=us-east-1
51
+ S3__ENDPOINT_URL=http://localhost:9000 # For MinIO
52
+ S3__ACCESS_KEY_ID=minioadmin
53
+ S3__SECRET_ACCESS_KEY=minioadmin
54
+
55
+ # Environment
56
+ ENVIRONMENT=development
57
+ TEAM=rem
58
+ """
59
+
60
+ import os
61
+ import hashlib
62
+ from pydantic import Field, field_validator, ValidationInfo
63
+ from pydantic_settings import BaseSettings, SettingsConfigDict
64
+ from loguru import logger
65
+
66
+
67
+ class LLMSettings(BaseSettings):
68
+ """
69
+ LLM provider settings for Pydantic AI agents.
70
+
71
+ Environment variables (accepts both prefixed and unprefixed):
72
+ LLM__DEFAULT_MODEL or DEFAULT_MODEL - Default model (format: provider:model-id)
73
+ LLM__DEFAULT_TEMPERATURE or DEFAULT_TEMPERATURE - Temperature for generation
74
+ LLM__MAX_RETRIES or MAX_RETRIES - Max agent request retries
75
+ LLM__EVALUATOR_MODEL or EVALUATOR_MODEL - Model for LLM-as-judge evaluation
76
+ LLM__OPENAI_API_KEY or OPENAI_API_KEY - OpenAI API key
77
+ LLM__ANTHROPIC_API_KEY or ANTHROPIC_API_KEY - Anthropic API key
78
+ LLM__EMBEDDING_PROVIDER or EMBEDDING_PROVIDER - Default embedding provider (openai)
79
+ LLM__EMBEDDING_MODEL or EMBEDDING_MODEL - Default embedding model name
80
+ LLM__DEFAULT_STRUCTURED_OUTPUT - Default structured output mode (False = streaming text)
81
+ """
82
+
83
+ model_config = SettingsConfigDict(
84
+ env_prefix="LLM__",
85
+ env_file=".env",
86
+ env_file_encoding="utf-8",
87
+ extra="ignore",
88
+ )
89
+
90
+ default_model: str = Field(
91
+ default="openai:gpt-4.1",
92
+ description="Default LLM model (format: provider:model-id)",
93
+ )
94
+
95
+ default_temperature: float = Field(
96
+ default=0.5,
97
+ ge=0.0,
98
+ le=1.0,
99
+ description="Default temperature (0.0-0.3: analytical, 0.7-1.0: creative)",
100
+ )
101
+
102
+ max_retries: int = Field(
103
+ default=10,
104
+ description="Maximum agent request retries (prevents infinite loops from tool errors)",
105
+ )
106
+
107
+ default_max_iterations: int = Field(
108
+ default=20,
109
+ description="Default max iterations for agentic calls (limits total LLM requests per agent.run())",
110
+ )
111
+
112
+ evaluator_model: str = Field(
113
+ default="gpt-4.1",
114
+ description="Model for LLM-as-judge evaluators (separate from generation model)",
115
+ )
116
+
117
+ query_agent_model: str = Field(
118
+ default="cerebras:qwen-3-32b",
119
+ description="Model for REM Query Agent (natural language to REM query). Cerebras Qwen 3-32B provides ultra-fast inference (1.2s reasoning, 2400 tok/s). Alternative: cerebras:llama-3.3-70b, gpt-4o-mini, or claude-sonnet-4.5",
120
+ )
121
+
122
+ openai_api_key: str | None = Field(
123
+ default=None,
124
+ description="OpenAI API key for GPT models (reads from LLM__OPENAI_API_KEY or OPENAI_API_KEY)",
125
+ )
126
+
127
+ anthropic_api_key: str | None = Field(
128
+ default=None,
129
+ description="Anthropic API key for Claude models (reads from LLM__ANTHROPIC_API_KEY or ANTHROPIC_API_KEY)",
130
+ )
131
+
132
+ embedding_provider: str = Field(
133
+ default="openai",
134
+ description="Default embedding provider (currently only openai supported)",
135
+ )
136
+
137
+ embedding_model: str = Field(
138
+ default="text-embedding-3-small",
139
+ description="Default embedding model (provider-specific model name)",
140
+ )
141
+
142
+ default_structured_output: bool = Field(
143
+ default=False,
144
+ description="Default structured output mode for agents. False = streaming text (easier), True = JSON schema validation",
145
+ )
146
+
147
+ @field_validator("openai_api_key", mode="before")
148
+ @classmethod
149
+ def validate_openai_api_key(cls, v):
150
+ """Fallback to OPENAI_API_KEY if LLM__OPENAI_API_KEY not set (LLM__ takes precedence)."""
151
+ if v is None:
152
+ return os.getenv("OPENAI_API_KEY")
153
+ return v
154
+
155
+ @field_validator("anthropic_api_key", mode="before")
156
+ @classmethod
157
+ def validate_anthropic_api_key(cls, v):
158
+ """Fallback to ANTHROPIC_API_KEY if LLM__ANTHROPIC_API_KEY not set (LLM__ takes precedence)."""
159
+ if v is None:
160
+ return os.getenv("ANTHROPIC_API_KEY")
161
+ return v
162
+
163
+
164
+ class MCPSettings(BaseSettings):
165
+ """
166
+ MCP server settings.
167
+
168
+ MCP server is mounted at /api/v1/mcp with FastMCP.
169
+ Can be accessed via:
170
+ - HTTP transport (production): /api/v1/mcp
171
+ - SSE transport (compatible with Claude Desktop)
172
+
173
+ Environment variables:
174
+ MCP_SERVER_{NAME} - Server URLs for MCP client connections
175
+ """
176
+
177
+ model_config = SettingsConfigDict(
178
+ env_prefix="MCP__",
179
+ env_file=".env",
180
+ env_file_encoding="utf-8",
181
+ extra="ignore",
182
+ )
183
+
184
+ @staticmethod
185
+ def get_server_url(server_name: str) -> str | None:
186
+ """
187
+ Get MCP server URL from environment variable.
188
+
189
+ Args:
190
+ server_name: Server name (e.g., "test", "prod")
191
+
192
+ Returns:
193
+ Server URL or None if not configured
194
+
195
+ Example:
196
+ MCP_SERVER_TEST=http://localhost:8000/api/v1/mcp
197
+ """
198
+ import os
199
+
200
+ env_key = f"MCP_SERVER_{server_name.upper()}"
201
+ return os.getenv(env_key)
202
+
203
+
204
+ class OTELSettings(BaseSettings):
205
+ """
206
+ OpenTelemetry observability settings.
207
+
208
+ Integrates with OpenTelemetry Collector for distributed tracing.
209
+ Uses OTLP protocol to export to Arize Phoenix or other OTLP backends.
210
+
211
+ Environment variables:
212
+ OTEL__ENABLED - Enable instrumentation (default: false for local dev)
213
+ OTEL__SERVICE_NAME - Service name for traces
214
+ OTEL__COLLECTOR_ENDPOINT - OTLP endpoint (gRPC: 4317, HTTP: 4318)
215
+ OTEL__PROTOCOL - Protocol to use (grpc or http)
216
+ OTEL__EXPORT_TIMEOUT - Export timeout in milliseconds
217
+ """
218
+
219
+ model_config = SettingsConfigDict(
220
+ env_prefix="OTEL__",
221
+ env_file=".env",
222
+ env_file_encoding="utf-8",
223
+ extra="ignore",
224
+ )
225
+
226
+ enabled: bool = Field(
227
+ default=False,
228
+ description="Enable OpenTelemetry instrumentation (disabled by default for local dev)",
229
+ )
230
+
231
+ service_name: str = Field(
232
+ default="rem-api",
233
+ description="Service name for traces",
234
+ )
235
+
236
+ collector_endpoint: str = Field(
237
+ default="http://localhost:4318",
238
+ description="OTLP collector endpoint (HTTP: 4318, gRPC: 4317)",
239
+ )
240
+
241
+ protocol: str = Field(
242
+ default="http",
243
+ description="OTLP protocol (http or grpc)",
244
+ )
245
+
246
+ export_timeout: int = Field(
247
+ default=10000,
248
+ description="Export timeout in milliseconds",
249
+ )
250
+
251
+ insecure: bool = Field(
252
+ default=True,
253
+ description="Use insecure (non-TLS) gRPC connection (default: True for local dev)",
254
+ )
255
+
256
+
257
+ class PhoenixSettings(BaseSettings):
258
+ """
259
+ Arize Phoenix settings for LLM observability and evaluation.
260
+
261
+ Phoenix provides:
262
+ - OpenTelemetry-based LLM tracing (OpenInference conventions)
263
+ - Experiment tracking
264
+ - Evaluation feedback
265
+
266
+ Environment variables:
267
+ PHOENIX__ENABLED - Enable Phoenix integration
268
+ PHOENIX__BASE_URL - Phoenix base URL (for client connections)
269
+ PHOENIX__API_KEY - Phoenix API key (cloud instances)
270
+ PHOENIX__COLLECTOR_ENDPOINT - Phoenix OTLP endpoint
271
+ PHOENIX__PROJECT_NAME - Phoenix project name for trace organization
272
+ """
273
+
274
+ model_config = SettingsConfigDict(
275
+ env_prefix="PHOENIX__",
276
+ env_file=".env",
277
+ env_file_encoding="utf-8",
278
+ extra="ignore",
279
+ )
280
+
281
+ enabled: bool = Field(
282
+ default=True,
283
+ description="Enable Phoenix integration (enabled by default)",
284
+ )
285
+
286
+ base_url: str = Field(
287
+ default="http://localhost:6006",
288
+ description="Phoenix base URL for client connections (default local Phoenix port)",
289
+ )
290
+
291
+ api_key: str | None = Field(
292
+ default=None,
293
+ description="Arize Phoenix API key for cloud instances",
294
+ )
295
+
296
+ collector_endpoint: str = Field(
297
+ default="http://localhost:6006/v1/traces",
298
+ description="Phoenix OTLP endpoint for traces (default local Phoenix port)",
299
+ )
300
+
301
+ project_name: str = Field(
302
+ default="rem",
303
+ description="Phoenix project name for trace organization",
304
+ )
305
+
306
+
307
+ class GoogleOAuthSettings(BaseSettings):
308
+ """
309
+ Google OAuth settings.
310
+
311
+ Environment variables:
312
+ AUTH__GOOGLE__CLIENT_ID - Google OAuth client ID
313
+ AUTH__GOOGLE__CLIENT_SECRET - Google OAuth client secret
314
+ AUTH__GOOGLE__REDIRECT_URI - OAuth callback URL
315
+ AUTH__GOOGLE__HOSTED_DOMAIN - Restrict to Google Workspace domain
316
+ """
317
+
318
+ model_config = SettingsConfigDict(
319
+ env_prefix="AUTH__GOOGLE__",
320
+ env_file=".env",
321
+ env_file_encoding="utf-8",
322
+ extra="ignore",
323
+ )
324
+
325
+ client_id: str = Field(default="", description="Google OAuth client ID")
326
+ client_secret: str = Field(default="", description="Google OAuth client secret")
327
+ redirect_uri: str = Field(
328
+ default="http://localhost:8000/api/auth/google/callback",
329
+ description="OAuth redirect URI",
330
+ )
331
+ hosted_domain: str | None = Field(
332
+ default=None, description="Restrict to Google Workspace domain (e.g., example.com)"
333
+ )
334
+
335
+
336
+ class MicrosoftOAuthSettings(BaseSettings):
337
+ """
338
+ Microsoft Entra ID OAuth settings.
339
+
340
+ Environment variables:
341
+ AUTH__MICROSOFT__CLIENT_ID - Application (client) ID
342
+ AUTH__MICROSOFT__CLIENT_SECRET - Client secret
343
+ AUTH__MICROSOFT__REDIRECT_URI - OAuth callback URL
344
+ AUTH__MICROSOFT__TENANT - Tenant ID or common/organizations/consumers
345
+ """
346
+
347
+ model_config = SettingsConfigDict(
348
+ env_prefix="AUTH__MICROSOFT__",
349
+ env_file=".env",
350
+ env_file_encoding="utf-8",
351
+ extra="ignore",
352
+ )
353
+
354
+ client_id: str = Field(default="", description="Microsoft Application ID")
355
+ client_secret: str = Field(default="", description="Microsoft client secret")
356
+ redirect_uri: str = Field(
357
+ default="http://localhost:8000/api/auth/microsoft/callback",
358
+ description="OAuth redirect URI",
359
+ )
360
+ tenant: str = Field(
361
+ default="common",
362
+ description="Tenant ID or common/organizations/consumers",
363
+ )
364
+
365
+
366
+ class AuthSettings(BaseSettings):
367
+ """
368
+ Authentication settings for OAuth 2.1 / OIDC.
369
+
370
+ Supports multiple providers:
371
+ - Google OAuth
372
+ - Microsoft Entra ID
373
+ - Custom OIDC provider
374
+
375
+ Environment variables:
376
+ AUTH__ENABLED - Enable authentication (default: true)
377
+ AUTH__ALLOW_ANONYMOUS - Allow rate-limited anonymous access (default: true)
378
+ AUTH__SESSION_SECRET - Secret for session cookie signing
379
+ AUTH__GOOGLE__* - Google OAuth settings
380
+ AUTH__MICROSOFT__* - Microsoft OAuth settings
381
+
382
+ Access modes:
383
+ - enabled=true, allow_anonymous=true: Auth available, anonymous gets rate-limited access
384
+ - enabled=true, allow_anonymous=false: Auth required for all requests
385
+ - enabled=false: No auth, all requests treated as default user (dev mode)
386
+ """
387
+
388
+ model_config = SettingsConfigDict(
389
+ env_prefix="AUTH__",
390
+ env_file=".env",
391
+ env_file_encoding="utf-8",
392
+ extra="ignore",
393
+ )
394
+
395
+ enabled: bool = Field(
396
+ default=True,
397
+ description="Enable authentication (OAuth endpoints and middleware)",
398
+ )
399
+
400
+ allow_anonymous: bool = Field(
401
+ default=True,
402
+ description=(
403
+ "Allow anonymous (unauthenticated) access with rate limits. "
404
+ "When true, requests without auth get ANONYMOUS tier rate limits. "
405
+ "When false, all requests require authentication."
406
+ ),
407
+ )
408
+
409
+ mcp_requires_auth: bool = Field(
410
+ default=True,
411
+ description=(
412
+ "Require authentication for MCP endpoints. "
413
+ "MCP is a protected service and should always require login in production. "
414
+ "Set to false only for local development/testing."
415
+ ),
416
+ )
417
+
418
+ session_secret: str = Field(
419
+ default="",
420
+ description="Secret key for session cookie signing (generate with secrets.token_hex(32))",
421
+ )
422
+
423
+ # OAuth provider settings
424
+ google: GoogleOAuthSettings = Field(default_factory=GoogleOAuthSettings)
425
+ microsoft: MicrosoftOAuthSettings = Field(default_factory=MicrosoftOAuthSettings)
426
+
427
+ # Pre-approved login codes (bypass email verification)
428
+ # Format: comma-separated codes with prefix A=admin, B=normal user
429
+ # Example: "A12345,A67890,B11111,B22222"
430
+ preapproved_codes: str = Field(
431
+ default="",
432
+ description=(
433
+ "Comma-separated list of pre-approved login codes. "
434
+ "Prefix A = admin user, B = normal user. "
435
+ "Example: 'A12345,A67890,B11111'. "
436
+ "Users can login with these codes without email verification."
437
+ ),
438
+ )
439
+
440
+ def check_preapproved_code(self, code: str) -> dict | None:
441
+ """
442
+ Check if a code is in the pre-approved list.
443
+
444
+ Args:
445
+ code: The code to check (including prefix)
446
+
447
+ Returns:
448
+ Dict with 'role' key if valid, None if not found.
449
+ - A prefix -> role='admin'
450
+ - B prefix -> role='user'
451
+ """
452
+ if not self.preapproved_codes:
453
+ return None
454
+
455
+ codes = [c.strip().upper() for c in self.preapproved_codes.split(",") if c.strip()]
456
+ code_upper = code.strip().upper()
457
+
458
+ if code_upper not in codes:
459
+ return None
460
+
461
+ # Parse prefix to determine role
462
+ if code_upper.startswith("A"):
463
+ return {"role": "admin", "code": code_upper}
464
+ elif code_upper.startswith("B"):
465
+ return {"role": "user", "code": code_upper}
466
+ else:
467
+ # Unknown prefix, treat as user
468
+ return {"role": "user", "code": code_upper}
469
+
470
+ @field_validator("session_secret", mode="before")
471
+ @classmethod
472
+ def generate_dev_secret(cls, v: str | None, info: ValidationInfo) -> str:
473
+ # Only generate if not already set and not in production
474
+ if not v and info.data.get("environment") != "production":
475
+ # Deterministic secret for development
476
+ seed_string = f"{info.data.get('team', 'rem')}-{info.data.get('environment', 'development')}-auth-secret-salt"
477
+ logger.warning(
478
+ "AUTH__SESSION_SECRET not set. Generating deterministic secret for non-production environment. "
479
+ "DO NOT use in production."
480
+ )
481
+ return hashlib.sha256(seed_string.encode()).hexdigest()
482
+ elif not v and info.data.get("environment") == "production":
483
+ raise ValueError("AUTH__SESSION_SECRET must be set in production environment.")
484
+ return v
485
+
486
+
487
+ class PostgresSettings(BaseSettings):
488
+ """
489
+ PostgreSQL settings for CloudNativePG.
490
+
491
+ Connects to PostgreSQL 18 with pgvector extension running on CloudNativePG.
492
+
493
+ Environment variables:
494
+ POSTGRES__ENABLED - Enable database connection (default: true)
495
+ POSTGRES__CONNECTION_STRING - PostgreSQL connection string
496
+ POSTGRES__POOL_SIZE - Connection pool size
497
+ POSTGRES__POOL_MIN_SIZE - Minimum pool size
498
+ POSTGRES__POOL_MAX_SIZE - Maximum pool size
499
+ POSTGRES__POOL_TIMEOUT - Connection timeout in seconds
500
+ POSTGRES__STATEMENT_TIMEOUT - Statement timeout in milliseconds
501
+ """
502
+
503
+ model_config = SettingsConfigDict(
504
+ env_prefix="POSTGRES__",
505
+ env_file=".env",
506
+ env_file_encoding="utf-8",
507
+ extra="ignore",
508
+ )
509
+
510
+ enabled: bool = Field(
511
+ default=True,
512
+ description="Enable database connection (set to false for testing without DB)",
513
+ )
514
+
515
+ connection_string: str = Field(
516
+ default="postgresql://rem:rem@localhost:5051/rem",
517
+ description="PostgreSQL connection string (default uses Docker Compose prebuilt port 5051)",
518
+ )
519
+
520
+
521
+ pool_size: int = Field(
522
+ default=10,
523
+ description="Connection pool size (deprecated, use pool_min_size/pool_max_size)",
524
+ )
525
+
526
+ pool_min_size: int = Field(
527
+ default=5,
528
+ description="Minimum number of connections in pool",
529
+ )
530
+
531
+ pool_max_size: int = Field(
532
+ default=20,
533
+ description="Maximum number of connections in pool",
534
+ )
535
+
536
+ pool_timeout: int = Field(
537
+ default=30,
538
+ description="Connection timeout in seconds",
539
+ )
540
+
541
+ statement_timeout: int = Field(
542
+ default=30000,
543
+ description="Statement timeout in milliseconds (30 seconds default)",
544
+ )
545
+
546
+ @property
547
+ def user(self) -> str:
548
+ from urllib.parse import urlparse
549
+ return urlparse(self.connection_string).username or "postgres"
550
+
551
+ @property
552
+ def password(self) -> str | None:
553
+ from urllib.parse import urlparse
554
+ return urlparse(self.connection_string).password
555
+
556
+ @property
557
+ def database(self) -> str:
558
+ from urllib.parse import urlparse
559
+ return urlparse(self.connection_string).path.lstrip("/")
560
+
561
+ @property
562
+ def host(self) -> str:
563
+ from urllib.parse import urlparse
564
+ return urlparse(self.connection_string).hostname or "localhost"
565
+
566
+ @property
567
+ def port(self) -> int:
568
+ from urllib.parse import urlparse
569
+ return urlparse(self.connection_string).port or 5432
570
+
571
+
572
+ class MigrationSettings(BaseSettings):
573
+ """
574
+ Migration settings.
575
+
576
+ Environment variables:
577
+ MIGRATION__AUTO_UPGRADE - Automatically run migrations on startup
578
+ MIGRATION__MODE - Migration safety mode (permissive, additive, strict)
579
+ MIGRATION__ALLOW_DROP_COLUMNS - Allow DROP COLUMN operations
580
+ MIGRATION__ALLOW_DROP_TABLES - Allow DROP TABLE operations
581
+ MIGRATION__ALLOW_ALTER_COLUMNS - Allow ALTER COLUMN TYPE operations
582
+ MIGRATION__ALLOW_RENAME_COLUMNS - Allow RENAME COLUMN operations
583
+ MIGRATION__ALLOW_RENAME_TABLES - Allow RENAME TABLE operations
584
+ MIGRATION__UNSAFE_ALTER_WARNING - Warn on unsafe ALTER operations
585
+ """
586
+
587
+ model_config = SettingsConfigDict(
588
+ env_prefix="MIGRATION__",
589
+ env_file=".env",
590
+ env_file_encoding="utf-8",
591
+ extra="ignore",
592
+ )
593
+
594
+ auto_upgrade: bool = Field(
595
+ default=True,
596
+ description="Automatically run database migrations on startup",
597
+ )
598
+
599
+ mode: str = Field(
600
+ default="permissive",
601
+ description="Migration safety mode: permissive, additive, strict",
602
+ )
603
+
604
+ allow_drop_columns: bool = Field(
605
+ default=False,
606
+ description="Allow DROP COLUMN operations (unsafe)",
607
+ )
608
+
609
+ allow_drop_tables: bool = Field(
610
+ default=False,
611
+ description="Allow DROP TABLE operations (unsafe)",
612
+ )
613
+
614
+ allow_alter_columns: bool = Field(
615
+ default=True,
616
+ description="Allow ALTER COLUMN TYPE operations (can be unsafe)",
617
+ )
618
+
619
+ allow_rename_columns: bool = Field(
620
+ default=True,
621
+ description="Allow RENAME COLUMN operations (can be unsafe)",
622
+ )
623
+
624
+ allow_rename_tables: bool = Field(
625
+ default=True,
626
+ description="Allow RENAME TABLE operations (can be unsafe)",
627
+ )
628
+
629
+ unsafe_alter_warning: bool = Field(
630
+ default=True,
631
+ description="Emit warning on potentially unsafe ALTER operations",
632
+ )
633
+
634
+
635
+ class StorageSettings(BaseSettings):
636
+ """
637
+ Storage provider settings.
638
+
639
+ Controls which storage backend to use for file uploads and artifacts.
640
+
641
+ Environment variables:
642
+ STORAGE__PROVIDER - Storage provider (local or s3, default: local)
643
+ STORAGE__BASE_PATH - Base path for local filesystem storage (default: ~/.rem/fs)
644
+ """
645
+
646
+ model_config = SettingsConfigDict(
647
+ env_prefix="STORAGE__",
648
+ env_file=".env",
649
+ env_file_encoding="utf-8",
650
+ extra="ignore",
651
+ )
652
+
653
+ provider: str = Field(
654
+ default="local",
655
+ description="Storage provider: 'local' for filesystem, 's3' for AWS S3",
656
+ )
657
+
658
+ base_path: str = Field(
659
+ default="~/.rem/fs",
660
+ description="Base path for local filesystem storage (only used when provider='local')",
661
+ )
662
+
663
+
664
+ class S3Settings(BaseSettings):
665
+ """
666
+ S3 storage settings for file uploads and artifacts.
667
+
668
+ Uses IRSA (IAM Roles for Service Accounts) for AWS permissions in EKS.
669
+ For local development, can use MinIO or provide access keys.
670
+
671
+ Bucket Naming Convention:
672
+ - Default: rem-io-{environment} (e.g., rem-io-development, rem-io-staging, rem-io-production)
673
+ - Matches Kubernetes manifest convention for consistency
674
+ - Override with S3__BUCKET_NAME environment variable
675
+
676
+ Path Convention:
677
+ Uploads: s3://{bucket}/{version}/uploads/{user_id}/{yyyy}/{mm}/{dd}/{filename}
678
+ Parsed: s3://{bucket}/{version}/parsed/{user_id}/{yyyy}/{mm}/{dd}/{filename}/{resource}
679
+
680
+ Environment variables:
681
+ S3__BUCKET_NAME - S3 bucket name (default: rem-io-development)
682
+ S3__VERSION - API version for paths (default: v1)
683
+ S3__UPLOADS_PREFIX - Uploads directory prefix (default: uploads)
684
+ S3__PARSED_PREFIX - Parsed content directory prefix (default: parsed)
685
+ S3__REGION - AWS region
686
+ S3__ENDPOINT_URL - Custom endpoint (for MinIO, LocalStack)
687
+ S3__ACCESS_KEY_ID - AWS access key (not needed with IRSA)
688
+ S3__SECRET_ACCESS_KEY - AWS secret key (not needed with IRSA)
689
+ S3__USE_SSL - Use SSL for connections (default: true)
690
+ """
691
+
692
+ model_config = SettingsConfigDict(
693
+ env_prefix="S3__",
694
+ env_file=".env",
695
+ env_file_encoding="utf-8",
696
+ extra="ignore",
697
+ )
698
+
699
+ bucket_name: str = Field(
700
+ default="rem-io-development",
701
+ description="S3 bucket name (convention: rem-io-{environment})",
702
+ )
703
+
704
+ version: str = Field(
705
+ default="v1",
706
+ description="API version for S3 path structure",
707
+ )
708
+
709
+ uploads_prefix: str = Field(
710
+ default="uploads",
711
+ description="Prefix for uploaded files (e.g., 'uploads' -> bucket/v1/uploads/...)",
712
+ )
713
+
714
+ parsed_prefix: str = Field(
715
+ default="parsed",
716
+ description="Prefix for parsed content (e.g., 'parsed' -> bucket/v1/parsed/...)",
717
+ )
718
+
719
+ region: str = Field(
720
+ default="us-east-1",
721
+ description="AWS region",
722
+ )
723
+
724
+ endpoint_url: str | None = Field(
725
+ default=None,
726
+ description="Custom S3 endpoint (for MinIO, LocalStack)",
727
+ )
728
+
729
+ access_key_id: str | None = Field(
730
+ default=None,
731
+ description="AWS access key ID (not needed with IRSA in EKS)",
732
+ )
733
+
734
+ secret_access_key: str | None = Field(
735
+ default=None,
736
+ description="AWS secret access key (not needed with IRSA in EKS)",
737
+ )
738
+
739
+ use_ssl: bool = Field(
740
+ default=True,
741
+ description="Use SSL for S3 connections",
742
+ )
743
+
744
+
745
+ class DataLakeSettings(BaseSettings):
746
+ """
747
+ Data lake settings for experiment and dataset storage.
748
+
749
+ Data Lake Convention:
750
+ The data lake provides a standardized structure for storing datasets,
751
+ experiments, and calibration data in S3. Users bring their own bucket
752
+ and the version is pinned by default to v0 in the path.
753
+
754
+ S3 Path Structure:
755
+ s3://{bucket}/{version}/datasets/
756
+ ├── raw/ # Raw source data + transformers
757
+ │ └── {dataset_name}/ # e.g., cns_drugs, codes, care
758
+ ├── tables/ # Database table data (JSONL)
759
+ │ ├── resources/ # → resources table
760
+ │ │ ├── drugs/{category}/ # Psychotropic drugs
761
+ │ │ ├── care/stages/ # Treatment stages
762
+ │ │ └── crisis/ # Crisis resources
763
+ │ └── codes/ # → codes table
764
+ │ ├── icd10/{category}/ # ICD-10 codes
765
+ │ └── cpt/ # CPT codes
766
+ └── calibration/ # Agent calibration
767
+ ├── experiments/ # Experiment configs + results
768
+ │ └── {agent}/{task}/ # e.g., siggy/risk-assessment
769
+ └── datasets/ # Shared evaluation datasets
770
+
771
+ Experiment Storage:
772
+ - Local: experiments/{agent}/{task}/experiment.yaml
773
+ - S3: s3://{bucket}/{version}/datasets/calibration/experiments/{agent}/{task}/
774
+
775
+ Environment variables:
776
+ DATA_LAKE__BUCKET_NAME - S3 bucket for data lake (required)
777
+ DATA_LAKE__VERSION - Path version prefix (default: v0)
778
+ DATA_LAKE__DATASETS_PREFIX - Datasets directory (default: datasets)
779
+ DATA_LAKE__EXPERIMENTS_PREFIX - Experiments subdirectory (default: experiments)
780
+ """
781
+
782
+ model_config = SettingsConfigDict(
783
+ env_prefix="DATA_LAKE__",
784
+ env_file=".env",
785
+ env_file_encoding="utf-8",
786
+ extra="ignore",
787
+ )
788
+
789
+ bucket_name: str | None = Field(
790
+ default=None,
791
+ description="S3 bucket for data lake storage (user-provided)",
792
+ )
793
+
794
+ version: str = Field(
795
+ default="v0",
796
+ description="API version for data lake paths",
797
+ )
798
+
799
+ datasets_prefix: str = Field(
800
+ default="datasets",
801
+ description="Root directory for datasets in the bucket",
802
+ )
803
+
804
+ experiments_prefix: str = Field(
805
+ default="experiments",
806
+ description="Subdirectory within calibration for experiments",
807
+ )
808
+
809
+ def get_base_uri(self) -> str | None:
810
+ """Get the base S3 URI for the data lake."""
811
+ if not self.bucket_name:
812
+ return None
813
+ return f"s3://{self.bucket_name}/{self.version}/{self.datasets_prefix}"
814
+
815
+ def get_experiment_uri(self, agent: str, task: str = "general") -> str | None:
816
+ """Get the S3 URI for an experiment."""
817
+ base = self.get_base_uri()
818
+ if not base:
819
+ return None
820
+ return f"{base}/calibration/{self.experiments_prefix}/{agent}/{task}"
821
+
822
+ def get_tables_uri(self, table: str = "resources") -> str | None:
823
+ """Get the S3 URI for a table directory."""
824
+ base = self.get_base_uri()
825
+ if not base:
826
+ return None
827
+ return f"{base}/tables/{table}"
828
+
829
+
830
+ class ChunkingSettings(BaseSettings):
831
+ """
832
+ Document chunking settings for semantic text splitting.
833
+
834
+ Uses semchunk for semantic-aware text chunking that respects document structure.
835
+ Generous chunk sizes (couple paragraphs) with reasonable overlaps for context.
836
+
837
+ Environment variables:
838
+ CHUNKING__CHUNK_SIZE - Target chunk size in characters
839
+ CHUNKING__OVERLAP - Overlap between chunks in characters
840
+ CHUNKING__MIN_CHUNK_SIZE - Minimum chunk size (avoid tiny chunks)
841
+ CHUNKING__MAX_CHUNK_SIZE - Maximum chunk size (hard limit)
842
+ """
843
+
844
+ model_config = SettingsConfigDict(
845
+ env_prefix="CHUNKING__",
846
+ env_file=".env",
847
+ env_file_encoding="utf-8",
848
+ extra="ignore",
849
+ )
850
+
851
+ chunk_size: int = Field(
852
+ default=1500,
853
+ description="Target chunk size in characters (couple paragraphs, ~300-400 words)",
854
+ )
855
+
856
+ overlap: int = Field(
857
+ default=200,
858
+ description="Overlap between chunks in characters for context preservation",
859
+ )
860
+
861
+ min_chunk_size: int = Field(
862
+ default=100,
863
+ description="Minimum chunk size to avoid tiny fragments",
864
+ )
865
+
866
+ max_chunk_size: int = Field(
867
+ default=2500,
868
+ description="Maximum chunk size (hard limit, prevents oversized chunks)",
869
+ )
870
+
871
+
872
+ class ContentSettings(BaseSettings):
873
+ """
874
+ Content provider settings for file processing.
875
+
876
+ Defines supported file types for each provider type.
877
+ Allows override of specific extensions via register_provider().
878
+
879
+ Environment variables:
880
+ CONTENT__SUPPORTED_TEXT_TYPES - Comma-separated text extensions
881
+ CONTENT__SUPPORTED_DOC_TYPES - Comma-separated document extensions
882
+ CONTENT__SUPPORTED_AUDIO_TYPES - Comma-separated audio extensions
883
+ CONTENT__SUPPORTED_IMAGE_TYPES - Comma-separated image extensions
884
+ CONTENT__IMAGE_VLLM_SAMPLE_RATE - Sampling rate for vision LLM analysis (0.0-1.0)
885
+ CONTENT__IMAGE_VLLM_PROVIDER - Vision provider (anthropic, gemini, openai)
886
+ CONTENT__IMAGE_VLLM_MODEL - Vision model name (provider default if not set)
887
+ CONTENT__CLIP_PROVIDER - CLIP embedding provider (jina, self-hosted)
888
+ CONTENT__CLIP_MODEL - CLIP model name (jina-clip-v1, jina-clip-v2)
889
+ CONTENT__JINA_API_KEY - Jina AI API key for CLIP embeddings
890
+ """
891
+
892
+ model_config = SettingsConfigDict(
893
+ env_prefix="CONTENT__",
894
+ env_file=".env",
895
+ env_file_encoding="utf-8",
896
+ extra="ignore",
897
+ )
898
+
899
+ supported_text_types: list[str] = Field(
900
+ default_factory=lambda: [
901
+ # Plain text
902
+ ".txt",
903
+ ".md",
904
+ ".markdown",
905
+ # Data formats
906
+ ".json",
907
+ ".yaml",
908
+ ".yml",
909
+ ".csv",
910
+ ".tsv",
911
+ ".log",
912
+ # Code files
913
+ ".py",
914
+ ".js",
915
+ ".ts",
916
+ ".tsx",
917
+ ".jsx",
918
+ ".java",
919
+ ".c",
920
+ ".cpp",
921
+ ".h",
922
+ ".rs",
923
+ ".go",
924
+ ".rb",
925
+ ".php",
926
+ ".sh",
927
+ ".bash",
928
+ ".sql",
929
+ # Web files
930
+ ".html",
931
+ ".css",
932
+ ".xml",
933
+ ],
934
+ description="File extensions handled by TextProvider (plain text, code, data files)",
935
+ )
936
+
937
+ supported_doc_types: list[str] = Field(
938
+ default_factory=lambda: [
939
+ # Documents
940
+ ".pdf",
941
+ ".docx",
942
+ ".pptx",
943
+ ".xlsx",
944
+ # Images (OCR text extraction)
945
+ ".png",
946
+ ".jpg",
947
+ ".jpeg",
948
+ ],
949
+ description="File extensions handled by DocProvider (Kreuzberg: PDFs, Office docs, images with OCR)",
950
+ )
951
+
952
+ supported_audio_types: list[str] = Field(
953
+ default_factory=lambda: [
954
+ ".wav",
955
+ ".mp3",
956
+ ".m4a",
957
+ ".flac",
958
+ ".ogg",
959
+ ],
960
+ description="File extensions handled by AudioProvider (Whisper API transcription)",
961
+ )
962
+
963
+ supported_image_types: list[str] = Field(
964
+ default_factory=lambda: [
965
+ ".png",
966
+ ".jpg",
967
+ ".jpeg",
968
+ ".gif",
969
+ ".webp",
970
+ ],
971
+ description="File extensions handled by ImageProvider (vision LLM + CLIP embeddings)",
972
+ )
973
+
974
+ image_vllm_sample_rate: float = Field(
975
+ default=0.0,
976
+ ge=0.0,
977
+ le=1.0,
978
+ description="Sampling rate for vision LLM analysis (0.0 = never, 1.0 = always, 0.1 = 10% of images). Gold tier users always get vision analysis.",
979
+ )
980
+
981
+ image_vllm_provider: str = Field(
982
+ default="anthropic",
983
+ description="Vision LLM provider: anthropic, gemini, or openai",
984
+ )
985
+
986
+ image_vllm_model: str | None = Field(
987
+ default=None,
988
+ description="Vision model name (uses provider default if None)",
989
+ )
990
+
991
+ clip_provider: str = Field(
992
+ default="jina",
993
+ description="CLIP embedding provider (jina for API, self-hosted for future KEDA pods)",
994
+ )
995
+
996
+ clip_model: str = Field(
997
+ default="jina-clip-v2",
998
+ description="CLIP model for image embeddings (jina-clip-v1, jina-clip-v2, or custom)",
999
+ )
1000
+
1001
+ jina_api_key: str | None = Field(
1002
+ default=None,
1003
+ description="Jina AI API key for CLIP embeddings (https://jina.ai/embeddings/)",
1004
+ )
1005
+
1006
+
1007
+ class SQSSettings(BaseSettings):
1008
+ """
1009
+ SQS queue settings for file processing.
1010
+
1011
+ Uses IRSA (IAM Roles for Service Accounts) for AWS permissions in EKS.
1012
+ For local development, can use access keys.
1013
+
1014
+ Environment variables:
1015
+ SQS__QUEUE_URL - SQS queue URL (from Pulumi output)
1016
+ SQS__REGION - AWS region
1017
+ SQS__MAX_MESSAGES - Max messages per receive (1-10)
1018
+ SQS__WAIT_TIME_SECONDS - Long polling wait time
1019
+ SQS__VISIBILITY_TIMEOUT - Message visibility timeout
1020
+ """
1021
+
1022
+ model_config = SettingsConfigDict(
1023
+ env_prefix="SQS__",
1024
+ env_file=".env",
1025
+ env_file_encoding="utf-8",
1026
+ extra="ignore",
1027
+ )
1028
+
1029
+ queue_url: str = Field(
1030
+ default="",
1031
+ description="SQS queue URL for file processing events",
1032
+ )
1033
+
1034
+ region: str = Field(
1035
+ default="us-east-1",
1036
+ description="AWS region",
1037
+ )
1038
+
1039
+ max_messages: int = Field(
1040
+ default=10,
1041
+ ge=1,
1042
+ le=10,
1043
+ description="Maximum messages to receive per batch (1-10)",
1044
+ )
1045
+
1046
+ wait_time_seconds: int = Field(
1047
+ default=20,
1048
+ ge=0,
1049
+ le=20,
1050
+ description="Long polling wait time in seconds (0-20, 20 recommended)",
1051
+ )
1052
+
1053
+ visibility_timeout: int = Field(
1054
+ default=300,
1055
+ description="Visibility timeout in seconds (should match processing time)",
1056
+ )
1057
+
1058
+
1059
+ class ChatSettings(BaseSettings):
1060
+ """
1061
+ Chat and session context settings.
1062
+
1063
+ Environment variables:
1064
+ CHAT__AUTO_INJECT_USER_CONTEXT - Automatically inject user profile into every request (default: false)
1065
+
1066
+ Design Philosophy:
1067
+ - Session history is ALWAYS loaded (required for multi-turn conversations)
1068
+ - Compression with REM LOOKUP hints keeps session history efficient
1069
+ - User context is on-demand by default (agents receive REM LOOKUP hints)
1070
+ - When auto_inject_user_context enabled, user profile is loaded and injected
1071
+
1072
+ Session History (always loaded with compression):
1073
+ - Each chat request is a single message, so history MUST be recovered
1074
+ - Long assistant responses stored as separate Message entities
1075
+ - Compressed versions include REM LOOKUP hints: "... [REM LOOKUP session-{id}-msg-{index}] ..."
1076
+ - Agent can retrieve full content on-demand using REM LOOKUP
1077
+ - Prevents context window bloat while maintaining conversation continuity
1078
+
1079
+ User Context (on-demand by default):
1080
+ - Agent system prompt includes: "User: {email}. To load user profile: Use REM LOOKUP \"{email}\""
1081
+ - Agent decides whether to load profile based on query
1082
+ - More efficient for queries that don't need personalization
1083
+
1084
+ User Context (auto-inject when enabled):
1085
+ - Set CHAT__AUTO_INJECT_USER_CONTEXT=true
1086
+ - User profile automatically loaded and injected into system message
1087
+ - Simpler for basic chatbots that always need context
1088
+ """
1089
+
1090
+ model_config = SettingsConfigDict(
1091
+ env_prefix="CHAT__",
1092
+ env_file=".env",
1093
+ env_file_encoding="utf-8",
1094
+ extra="ignore",
1095
+ )
1096
+
1097
+ auto_inject_user_context: bool = Field(
1098
+ default=False,
1099
+ description="Automatically inject user profile into every request (default: false, use REM LOOKUP hint instead)",
1100
+ )
1101
+
1102
+
1103
+ class APISettings(BaseSettings):
1104
+ """
1105
+ API server settings.
1106
+
1107
+ Environment variables:
1108
+ API__HOST - Host to bind to (0.0.0.0 for Docker, 127.0.0.1 for local)
1109
+ API__PORT - Port to listen on
1110
+ API__RELOAD - Enable auto-reload for development
1111
+ API__WORKERS - Number of worker processes (production)
1112
+ API__LOG_LEVEL - Logging level (debug, info, warning, error)
1113
+ API__API_KEY_ENABLED - Enable X-API-Key header authentication
1114
+ API__API_KEY - API key for X-API-Key authentication
1115
+ """
1116
+
1117
+ model_config = SettingsConfigDict(
1118
+ env_prefix="API__",
1119
+ env_file=".env",
1120
+ env_file_encoding="utf-8",
1121
+ extra="ignore",
1122
+ )
1123
+
1124
+ host: str = Field(
1125
+ default="0.0.0.0",
1126
+ description="Host to bind to (0.0.0.0 for Docker, 127.0.0.1 for local only)",
1127
+ )
1128
+
1129
+ port: int = Field(
1130
+ default=8000,
1131
+ description="Port to listen on",
1132
+ )
1133
+
1134
+ reload: bool = Field(
1135
+ default=True,
1136
+ description="Enable auto-reload for development (disable in production)",
1137
+ )
1138
+
1139
+ workers: int = Field(
1140
+ default=1,
1141
+ description="Number of worker processes (use >1 in production)",
1142
+ )
1143
+
1144
+ log_level: str = Field(
1145
+ default="info",
1146
+ description="Logging level (debug, info, warning, error, critical)",
1147
+ )
1148
+
1149
+ api_key_enabled: bool = Field(
1150
+ default=False,
1151
+ description=(
1152
+ "Enable X-API-Key header authentication for API endpoints. "
1153
+ "When enabled, requests must include X-API-Key header with valid key. "
1154
+ "This provides simple API key auth independent of OAuth."
1155
+ ),
1156
+ )
1157
+
1158
+ api_key: str | None = Field(
1159
+ default=None,
1160
+ description=(
1161
+ "API key for X-API-Key authentication. Required when api_key_enabled=true. "
1162
+ "Generate with: python -c \"import secrets; print(secrets.token_urlsafe(32))\""
1163
+ ),
1164
+ )
1165
+
1166
+ rate_limit_enabled: bool = Field(
1167
+ default=True,
1168
+ description=(
1169
+ "Enable rate limiting for API endpoints. "
1170
+ "Set to false to disable rate limiting entirely (useful for development)."
1171
+ ),
1172
+ )
1173
+
1174
+
1175
+ class ModelsSettings(BaseSettings):
1176
+ """
1177
+ Custom model registration settings for downstream applications.
1178
+
1179
+ Allows downstream apps to specify Python modules containing custom models
1180
+ that should be imported (and thus registered) before schema generation.
1181
+
1182
+ This enables `rem db schema generate` to discover models registered with
1183
+ `@rem.register_model` in downstream applications.
1184
+
1185
+ Environment variables:
1186
+ MODELS__IMPORT_MODULES - Semicolon-separated list of Python modules to import
1187
+ Example: "models;myapp.entities;myapp.custom_models"
1188
+
1189
+ Example:
1190
+ # In downstream app's .env
1191
+ MODELS__IMPORT_MODULES=models
1192
+
1193
+ # In downstream app's models/__init__.py
1194
+ import rem
1195
+ from rem.models.core import CoreModel
1196
+
1197
+ @rem.register_model
1198
+ class MyCustomEntity(CoreModel):
1199
+ name: str
1200
+
1201
+ # Then run schema generation
1202
+ rem db schema generate # Includes MyCustomEntity
1203
+ """
1204
+
1205
+ model_config = SettingsConfigDict(
1206
+ env_prefix="MODELS__",
1207
+ extra="ignore",
1208
+ )
1209
+
1210
+ import_modules: str = Field(
1211
+ default="",
1212
+ description=(
1213
+ "Semicolon-separated list of Python modules to import for model registration. "
1214
+ "These modules are imported before schema generation to ensure custom models "
1215
+ "decorated with @rem.register_model are discovered. "
1216
+ "Example: 'models;myapp.entities'"
1217
+ ),
1218
+ )
1219
+
1220
+ @property
1221
+ def module_list(self) -> list[str]:
1222
+ """
1223
+ Get modules as a list, filtering empty strings.
1224
+
1225
+ Auto-detects ./models folder if it exists and is importable.
1226
+ """
1227
+ modules = []
1228
+ if self.import_modules:
1229
+ modules = [m.strip() for m in self.import_modules.split(";") if m.strip()]
1230
+
1231
+ # Auto-detect ./models if it exists and is a Python package (convention over configuration)
1232
+ from pathlib import Path
1233
+
1234
+ models_path = Path("./models")
1235
+ if models_path.exists() and models_path.is_dir():
1236
+ # Check if it's a Python package (has __init__.py)
1237
+ if (models_path / "__init__.py").exists():
1238
+ if "models" not in modules:
1239
+ modules.insert(0, "models")
1240
+
1241
+ return modules
1242
+
1243
+
1244
+ class SchemaSettings(BaseSettings):
1245
+ """
1246
+ Schema search path settings for agent and evaluator schemas.
1247
+
1248
+ Allows extending REM's schema search with custom directories.
1249
+ Custom paths are searched BEFORE built-in package schemas.
1250
+
1251
+ Environment variables:
1252
+ SCHEMA__PATHS - Semicolon-separated list of directories to search
1253
+ Example: "/app/schemas;/shared/agents;./local-schemas"
1254
+
1255
+ Search Order:
1256
+ 1. Exact path (if file exists)
1257
+ 2. Custom paths from SCHEMA__PATHS (in order)
1258
+ 3. Built-in package schemas (schemas/agents/, schemas/evaluators/, etc.)
1259
+ 4. Database LOOKUP (if enabled)
1260
+
1261
+ Example:
1262
+ # In .env or environment
1263
+ SCHEMA__PATHS=/app/custom-agents;/shared/evaluators
1264
+
1265
+ # Then in code
1266
+ from rem.utils.schema_loader import load_agent_schema
1267
+ schema = load_agent_schema("my-custom-agent") # Found in /app/custom-agents/
1268
+ """
1269
+
1270
+ model_config = SettingsConfigDict(
1271
+ env_prefix="SCHEMA__",
1272
+ extra="ignore",
1273
+ )
1274
+
1275
+ paths: str = Field(
1276
+ default="",
1277
+ description=(
1278
+ "Semicolon-separated list of directories to search for schemas. "
1279
+ "These paths are searched BEFORE built-in package schemas. "
1280
+ "Example: '/app/schemas;/shared/agents'"
1281
+ ),
1282
+ )
1283
+
1284
+ @property
1285
+ def path_list(self) -> list[str]:
1286
+ """Get paths as a list, filtering empty strings."""
1287
+ if not self.paths:
1288
+ return []
1289
+ return [p.strip() for p in self.paths.split(";") if p.strip()]
1290
+
1291
+
1292
+ class GitSettings(BaseSettings):
1293
+ """
1294
+ Git repository provider settings for versioned schema/experiment syncing.
1295
+
1296
+ Enables syncing of agent schemas, evaluators, and experiments from Git repositories
1297
+ using either SSH or HTTPS authentication. Designed for cluster environments where
1298
+ secrets are provided via Kubernetes Secrets or similar mechanisms.
1299
+
1300
+ **Use Cases**:
1301
+ - Sync agent schemas from versioned repos (repo/schemas/)
1302
+ - Sync experiments and evaluation datasets (repo/experiments/)
1303
+ - Clone specific tags/releases for reproducible evaluations
1304
+ - Support multi-tenancy with per-tenant repo configurations
1305
+
1306
+ **Authentication Methods**:
1307
+ 1. **SSH** (recommended for production):
1308
+ - Uses SSH keys from filesystem or Kubernetes Secrets
1309
+ - Path specified via GIT__SSH_KEY_PATH or mounted at /etc/git-secret/ssh
1310
+ - Known hosts file at /etc/git-secret/known_hosts
1311
+
1312
+ 2. **HTTPS with Personal Access Token** (PAT):
1313
+ - GitHub: 5,000 API requests/hour per authenticated user
1314
+ - GitLab: Similar rate limits
1315
+ - Store PAT in GIT__PERSONAL_ACCESS_TOKEN environment variable
1316
+
1317
+ **Kubernetes Deployment Pattern** (git-sync sidecar):
1318
+ ```yaml
1319
+ # Secret creation (one-time setup)
1320
+ kubectl create secret generic git-creds \\
1321
+ --from-file=ssh=$HOME/.ssh/id_rsa \\
1322
+ --from-file=known_hosts=$HOME/.ssh/known_hosts
1323
+
1324
+ # Pod spec with secret mounting
1325
+ volumes:
1326
+ - name: git-secret
1327
+ secret:
1328
+ secretName: git-creds
1329
+ defaultMode: 0400 # Read-only for owner
1330
+ containers:
1331
+ - name: rem-api
1332
+ volumeMounts:
1333
+ - name: git-secret
1334
+ mountPath: /etc/git-secret
1335
+ readOnly: true
1336
+ securityContext:
1337
+ fsGroup: 65533 # Make secrets readable by git user
1338
+ ```
1339
+
1340
+ **Path Conventions**:
1341
+ - Agent schemas: {repo_root}/schemas/
1342
+ - Experiments: {repo_root}/experiments/
1343
+ - Evaluators: {repo_root}/schemas/evaluators/
1344
+
1345
+ **Performance & Caching**:
1346
+ - Clones cached locally in {cache_dir}/{repo_hash}/
1347
+ - Supports shallow clones (--depth=1) for faster syncing
1348
+ - Periodic refresh via cron jobs or git-sync sidecar
1349
+
1350
+ Environment variables:
1351
+ GIT__ENABLED - Enable Git provider (default: False)
1352
+ GIT__DEFAULT_REPO_URL - Default Git repository URL (ssh:// or https://)
1353
+ GIT__DEFAULT_BRANCH - Default branch to clone (default: main)
1354
+ GIT__SSH_KEY_PATH - Path to SSH private key (default: /etc/git-secret/ssh)
1355
+ GIT__KNOWN_HOSTS_PATH - Path to known_hosts file (default: /etc/git-secret/known_hosts)
1356
+ GIT__PERSONAL_ACCESS_TOKEN - GitHub/GitLab PAT for HTTPS auth
1357
+ GIT__CACHE_DIR - Local cache directory for cloned repos
1358
+ GIT__SHALLOW_CLONE - Use shallow clone (--depth=1) for faster sync
1359
+ GIT__VERIFY_SSL - Verify SSL certificates for HTTPS (default: True)
1360
+
1361
+ **Security Best Practices**:
1362
+ - Store SSH keys in Kubernetes Secrets, never in environment variables
1363
+ - Use read-only SSH keys (deploy keys) with minimal permissions
1364
+ - Enable known_hosts verification to prevent MITM attacks
1365
+ - Rotate PATs regularly (90-day expiration recommended)
1366
+ - Use IRSA/Workload Identity for cloud-provider Git services
1367
+ """
1368
+
1369
+ model_config = SettingsConfigDict(
1370
+ env_prefix="GIT__",
1371
+ env_file=".env",
1372
+ env_file_encoding="utf-8",
1373
+ extra="ignore",
1374
+ )
1375
+
1376
+ enabled: bool = Field(
1377
+ default=False,
1378
+ description="Enable Git provider for syncing schemas/experiments from Git repos",
1379
+ )
1380
+
1381
+ default_repo_url: str | None = Field(
1382
+ default=None,
1383
+ description="Default Git repository URL (ssh://git@github.com/org/repo.git or https://github.com/org/repo.git)",
1384
+ )
1385
+
1386
+ default_branch: str = Field(
1387
+ default="main",
1388
+ description="Default branch to clone/checkout (main, master, develop, etc.)",
1389
+ )
1390
+
1391
+ ssh_key_path: str = Field(
1392
+ default="/etc/git-secret/ssh",
1393
+ description="Path to SSH private key (Kubernetes Secret mount point or local path)",
1394
+ )
1395
+
1396
+ known_hosts_path: str = Field(
1397
+ default="/etc/git-secret/known_hosts",
1398
+ description="Path to known_hosts file for SSH host verification",
1399
+ )
1400
+
1401
+ personal_access_token: str | None = Field(
1402
+ default=None,
1403
+ description="Personal Access Token (PAT) for HTTPS authentication (GitHub, GitLab, etc.)",
1404
+ )
1405
+
1406
+ cache_dir: str = Field(
1407
+ default="/tmp/rem-git-cache",
1408
+ description="Local cache directory for cloned repositories",
1409
+ )
1410
+
1411
+ shallow_clone: bool = Field(
1412
+ default=True,
1413
+ description="Use shallow clone (--depth=1) for faster syncing (recommended for large repos)",
1414
+ )
1415
+
1416
+ verify_ssl: bool = Field(
1417
+ default=True,
1418
+ description="Verify SSL certificates for HTTPS connections (disable for self-signed certs)",
1419
+ )
1420
+
1421
+ sync_interval: int = Field(
1422
+ default=300,
1423
+ description="Sync interval in seconds for git-sync sidecar pattern (default: 5 minutes)",
1424
+ )
1425
+
1426
+
1427
+ class DBListenerSettings(BaseSettings):
1428
+ """
1429
+ PostgreSQL LISTEN/NOTIFY database listener settings.
1430
+
1431
+ The DB Listener is a lightweight worker that subscribes to PostgreSQL
1432
+ NOTIFY events and dispatches them to external systems (SQS, REST, custom).
1433
+
1434
+ Architecture:
1435
+ - Single-replica deployment (to avoid duplicate processing)
1436
+ - Dedicated connection for LISTEN (not from connection pool)
1437
+ - Automatic reconnection with exponential backoff
1438
+ - Graceful shutdown on SIGTERM
1439
+
1440
+ Use Cases:
1441
+ - Sync data changes to external systems (Phoenix, webhooks)
1442
+ - Trigger async jobs without polling
1443
+ - Event-driven architectures with PostgreSQL as event source
1444
+
1445
+ Example PostgreSQL trigger:
1446
+ CREATE OR REPLACE FUNCTION notify_feedback_insert()
1447
+ RETURNS TRIGGER AS $$
1448
+ BEGIN
1449
+ PERFORM pg_notify('feedback_sync', json_build_object(
1450
+ 'id', NEW.id,
1451
+ 'table', 'feedbacks',
1452
+ 'action', 'insert'
1453
+ )::text);
1454
+ RETURN NEW;
1455
+ END;
1456
+ $$ LANGUAGE plpgsql;
1457
+
1458
+ Environment variables:
1459
+ DB_LISTENER__ENABLED - Enable the listener worker (default: false)
1460
+ DB_LISTENER__CHANNELS - Comma-separated PostgreSQL channels to listen on
1461
+ DB_LISTENER__HANDLER_TYPE - Handler type: 'sqs', 'rest', or 'custom'
1462
+ DB_LISTENER__SQS_QUEUE_URL - SQS queue URL (for handler_type=sqs)
1463
+ DB_LISTENER__REST_ENDPOINT - REST endpoint URL (for handler_type=rest)
1464
+ DB_LISTENER__RECONNECT_DELAY - Initial reconnect delay in seconds
1465
+ DB_LISTENER__MAX_RECONNECT_DELAY - Maximum reconnect delay in seconds
1466
+
1467
+ References:
1468
+ - PostgreSQL NOTIFY: https://www.postgresql.org/docs/current/sql-notify.html
1469
+ - Brandur's Notifier: https://brandur.org/notifier
1470
+ """
1471
+
1472
+ model_config = SettingsConfigDict(
1473
+ env_prefix="DB_LISTENER__",
1474
+ env_file=".env",
1475
+ env_file_encoding="utf-8",
1476
+ extra="ignore",
1477
+ )
1478
+
1479
+ enabled: bool = Field(
1480
+ default=False,
1481
+ description="Enable the DB Listener worker (disabled by default)",
1482
+ )
1483
+
1484
+ channels: str = Field(
1485
+ default="",
1486
+ description=(
1487
+ "Comma-separated list of PostgreSQL channels to LISTEN on. "
1488
+ "Example: 'feedback_sync,entity_update,user_events'"
1489
+ ),
1490
+ )
1491
+
1492
+ handler_type: str = Field(
1493
+ default="rest",
1494
+ description=(
1495
+ "Handler type for dispatching notifications. Options: "
1496
+ "'sqs' (publish to SQS), 'rest' (POST to endpoint), 'custom' (Python handlers)"
1497
+ ),
1498
+ )
1499
+
1500
+ sqs_queue_url: str = Field(
1501
+ default="",
1502
+ description="SQS queue URL for handler_type='sqs'",
1503
+ )
1504
+
1505
+ rest_endpoint: str = Field(
1506
+ default="http://localhost:8000/api/v1/internal/events",
1507
+ description=(
1508
+ "REST endpoint URL for handler_type='rest'. "
1509
+ "Receives POST with {channel, payload, source} JSON body."
1510
+ ),
1511
+ )
1512
+
1513
+ reconnect_delay: float = Field(
1514
+ default=1.0,
1515
+ description="Initial delay (seconds) between reconnection attempts",
1516
+ )
1517
+
1518
+ max_reconnect_delay: float = Field(
1519
+ default=60.0,
1520
+ description="Maximum delay (seconds) between reconnection attempts (exponential backoff cap)",
1521
+ )
1522
+
1523
+ @property
1524
+ def channel_list(self) -> list[str]:
1525
+ """Get channels as a list, filtering empty strings."""
1526
+ if not self.channels:
1527
+ return []
1528
+ return [c.strip() for c in self.channels.split(",") if c.strip()]
1529
+
1530
+
1531
+ class EmailSettings(BaseSettings):
1532
+ """
1533
+ Email service settings for SMTP.
1534
+
1535
+ Supports passwordless login via email codes and transactional emails.
1536
+ Uses Gmail SMTP with App Passwords by default.
1537
+
1538
+ Generate app password at: https://myaccount.google.com/apppasswords
1539
+
1540
+ Environment variables:
1541
+ EMAIL__ENABLED - Enable email service (default: false)
1542
+ EMAIL__SMTP_HOST - SMTP server host (default: smtp.gmail.com)
1543
+ EMAIL__SMTP_PORT - SMTP server port (default: 587 for TLS)
1544
+ EMAIL__SENDER_EMAIL - Sender email address
1545
+ EMAIL__SENDER_NAME - Sender display name
1546
+ EMAIL__APP_PASSWORD - Gmail app password (from secrets)
1547
+ EMAIL__USE_TLS - Use TLS encryption (default: true)
1548
+ EMAIL__LOGIN_CODE_EXPIRY_MINUTES - Login code expiry (default: 10)
1549
+
1550
+ Branding environment variables (for email templates):
1551
+ EMAIL__APP_NAME - Application name in emails (default: REM)
1552
+ EMAIL__LOGO_URL - Logo URL for email templates (40x40 recommended)
1553
+ EMAIL__TAGLINE - Tagline shown in email footer
1554
+ EMAIL__WEBSITE_URL - Main website URL for email links
1555
+ EMAIL__PRIVACY_URL - Privacy policy URL for email footer
1556
+ EMAIL__TERMS_URL - Terms of service URL for email footer
1557
+ """
1558
+
1559
+ model_config = SettingsConfigDict(
1560
+ env_prefix="EMAIL__",
1561
+ env_file=".env",
1562
+ env_file_encoding="utf-8",
1563
+ extra="ignore",
1564
+ )
1565
+
1566
+ enabled: bool = Field(
1567
+ default=False,
1568
+ description="Enable email service (requires app_password to be set)",
1569
+ )
1570
+
1571
+ smtp_host: str = Field(
1572
+ default="smtp.gmail.com",
1573
+ description="SMTP server host",
1574
+ )
1575
+
1576
+ smtp_port: int = Field(
1577
+ default=587,
1578
+ description="SMTP server port (587 for TLS, 465 for SSL)",
1579
+ )
1580
+
1581
+ sender_email: str = Field(
1582
+ default="",
1583
+ description="Sender email address",
1584
+ )
1585
+
1586
+ sender_name: str = Field(
1587
+ default="REM",
1588
+ description="Sender display name",
1589
+ )
1590
+
1591
+ # Branding settings for email templates
1592
+ app_name: str = Field(
1593
+ default="REM",
1594
+ description="Application name shown in email templates",
1595
+ )
1596
+
1597
+ logo_url: str | None = Field(
1598
+ default=None,
1599
+ description="Logo URL for email templates (40x40 recommended)",
1600
+ )
1601
+
1602
+ tagline: str = Field(
1603
+ default="Your AI-powered platform",
1604
+ description="Tagline shown in email footer",
1605
+ )
1606
+
1607
+ website_url: str = Field(
1608
+ default="https://rem.ai",
1609
+ description="Main website URL for email links",
1610
+ )
1611
+
1612
+ privacy_url: str = Field(
1613
+ default="https://rem.ai/privacy",
1614
+ description="Privacy policy URL for email footer",
1615
+ )
1616
+
1617
+ terms_url: str = Field(
1618
+ default="https://rem.ai/terms",
1619
+ description="Terms of service URL for email footer",
1620
+ )
1621
+
1622
+ app_password: str | None = Field(
1623
+ default=None,
1624
+ description="Gmail app password for SMTP authentication",
1625
+ )
1626
+
1627
+ use_tls: bool = Field(
1628
+ default=True,
1629
+ description="Use TLS encryption for SMTP",
1630
+ )
1631
+
1632
+ login_code_expiry_minutes: int = Field(
1633
+ default=10,
1634
+ description="Login code expiry in minutes",
1635
+ )
1636
+
1637
+ trusted_email_domains: str = Field(
1638
+ default="",
1639
+ description=(
1640
+ "Comma-separated list of trusted email domains for new user registration. "
1641
+ "Existing users can always login regardless of domain. "
1642
+ "New users must have an email from a trusted domain. "
1643
+ "Empty string means all domains are allowed. "
1644
+ "Example: 'siggymd.ai,example.com'"
1645
+ ),
1646
+ )
1647
+
1648
+ @property
1649
+ def trusted_domain_list(self) -> list[str]:
1650
+ """Get trusted domains as a list, filtering empty strings."""
1651
+ if not self.trusted_email_domains:
1652
+ return []
1653
+ return [d.strip().lower() for d in self.trusted_email_domains.split(",") if d.strip()]
1654
+
1655
+ def is_domain_trusted(self, email: str) -> bool:
1656
+ """Check if an email's domain is in the trusted list.
1657
+
1658
+ Args:
1659
+ email: Email address to check
1660
+
1661
+ Returns:
1662
+ True if domain is trusted (or if no trusted domains configured)
1663
+ """
1664
+ domains = self.trusted_domain_list
1665
+ if not domains:
1666
+ # No restrictions configured
1667
+ return True
1668
+
1669
+ email_domain = email.lower().split("@")[-1].strip()
1670
+ return email_domain in domains
1671
+
1672
+ @property
1673
+ def is_configured(self) -> bool:
1674
+ """Check if email service is properly configured."""
1675
+ return bool(self.sender_email and self.app_password)
1676
+
1677
+ @property
1678
+ def template_kwargs(self) -> dict:
1679
+ """
1680
+ Get branding kwargs for email templates.
1681
+
1682
+ Returns a dict that can be passed to template functions:
1683
+ login_code_template(..., **settings.email.template_kwargs)
1684
+ """
1685
+ kwargs = {
1686
+ "app_name": self.app_name,
1687
+ "tagline": self.tagline,
1688
+ "website_url": self.website_url,
1689
+ "privacy_url": self.privacy_url,
1690
+ "terms_url": self.terms_url,
1691
+ }
1692
+ if self.logo_url:
1693
+ kwargs["logo_url"] = self.logo_url
1694
+ return kwargs
1695
+
1696
+
1697
+ class DebugSettings(BaseSettings):
1698
+ """
1699
+ Debug settings for development and troubleshooting.
1700
+
1701
+ Environment variables:
1702
+ DEBUG__AUDIT_SESSION - Dump session history to /tmp/{session_id}.yaml
1703
+ DEBUG__AUDIT_DIR - Directory for session audit files (default: /tmp)
1704
+ """
1705
+
1706
+ model_config = SettingsConfigDict(
1707
+ env_prefix="DEBUG__",
1708
+ env_file=".env",
1709
+ env_file_encoding="utf-8",
1710
+ extra="ignore",
1711
+ )
1712
+
1713
+ audit_session: bool = Field(
1714
+ default=False,
1715
+ description="When true, dump full session history to audit files for debugging",
1716
+ )
1717
+
1718
+ audit_dir: str = Field(
1719
+ default="/tmp",
1720
+ description="Directory for session audit files",
1721
+ )
1722
+
1723
+
1724
+ class TestSettings(BaseSettings):
1725
+ """
1726
+ Test environment settings.
1727
+
1728
+ Environment variables:
1729
+ TEST__USER_EMAIL - Test user email (default: test@rem.ai)
1730
+ TEST__USER_ID - Test user UUID (auto-generated from email if not provided)
1731
+
1732
+ The user_id is a deterministic UUID v5 generated from the email address.
1733
+ This ensures consistent IDs across test runs and allows tests to use both
1734
+ email and UUID interchangeably.
1735
+ """
1736
+
1737
+ model_config = SettingsConfigDict(
1738
+ env_prefix="TEST__",
1739
+ env_file=".env",
1740
+ env_file_encoding="utf-8",
1741
+ extra="ignore",
1742
+ )
1743
+
1744
+ user_email: str = Field(
1745
+ default="test@rem.ai",
1746
+ description="Test user email address",
1747
+ )
1748
+
1749
+ user_id: str | None = Field(
1750
+ default=None,
1751
+ description="Test user UUID (auto-generated from email if not provided)",
1752
+ )
1753
+
1754
+ @property
1755
+ def effective_user_id(self) -> str:
1756
+ """
1757
+ Get the effective user ID (either explicit or generated from email).
1758
+
1759
+ Returns a deterministic UUID v5 based on the email address if user_id
1760
+ is not explicitly set. This ensures consistent test data across runs.
1761
+ """
1762
+ if self.user_id:
1763
+ return self.user_id
1764
+
1765
+ # Generate deterministic UUID v5 from email
1766
+ # Using DNS namespace as the base (standard practice for email-based UUIDs)
1767
+ import uuid
1768
+ return str(uuid.uuid5(uuid.NAMESPACE_DNS, self.user_email))
1769
+
1770
+
1771
+ class Settings(BaseSettings):
1772
+ """
1773
+ Global application settings.
1774
+
1775
+ Aggregates all nested settings groups with environment variable support.
1776
+ Uses double underscore delimiter for nested variables (LLM__DEFAULT_MODEL).
1777
+
1778
+ Environment variables:
1779
+ TEAM - Team/project name for observability
1780
+ ENVIRONMENT - Environment (development, staging, production)
1781
+ DOMAIN - Public domain for OAuth discovery
1782
+ ROOT_PATH - Root path for reverse proxy (e.g., /rem for ALB routing)
1783
+ TEST__USER_ID - Default user ID for integration tests
1784
+ """
1785
+
1786
+ model_config = SettingsConfigDict(
1787
+ env_file=".env",
1788
+ env_file_encoding="utf-8",
1789
+ env_nested_delimiter="__",
1790
+ extra="ignore",
1791
+ )
1792
+
1793
+ app_name: str = Field(
1794
+ default="REM",
1795
+ description="Application/API name used in docs, titles, and user-facing text",
1796
+ )
1797
+
1798
+ team: str = Field(
1799
+ default="rem",
1800
+ description="Team or project name for observability",
1801
+ )
1802
+
1803
+ environment: str = Field(
1804
+ default="development",
1805
+ description="Environment (development, staging, production)",
1806
+ )
1807
+
1808
+ domain: str | None = Field(
1809
+ default=None,
1810
+ description="Public domain for OAuth discovery (e.g., https://api.example.com)",
1811
+ )
1812
+
1813
+ root_path: str = Field(
1814
+ default="",
1815
+ description="Root path for reverse proxy (e.g., /rem for ALB routing)",
1816
+ )
1817
+
1818
+ # Nested settings groups
1819
+ api: APISettings = Field(default_factory=APISettings)
1820
+ chat: ChatSettings = Field(default_factory=ChatSettings)
1821
+ llm: LLMSettings = Field(default_factory=LLMSettings)
1822
+ mcp: MCPSettings = Field(default_factory=MCPSettings)
1823
+ models: ModelsSettings = Field(default_factory=ModelsSettings)
1824
+ otel: OTELSettings = Field(default_factory=OTELSettings)
1825
+ phoenix: PhoenixSettings = Field(default_factory=PhoenixSettings)
1826
+ auth: AuthSettings = Field(default_factory=AuthSettings)
1827
+ postgres: PostgresSettings = Field(default_factory=PostgresSettings)
1828
+ migration: MigrationSettings = Field(default_factory=MigrationSettings)
1829
+ storage: StorageSettings = Field(default_factory=StorageSettings)
1830
+ s3: S3Settings = Field(default_factory=S3Settings)
1831
+ data_lake: DataLakeSettings = Field(default_factory=DataLakeSettings)
1832
+ git: GitSettings = Field(default_factory=GitSettings)
1833
+ sqs: SQSSettings = Field(default_factory=SQSSettings)
1834
+ db_listener: DBListenerSettings = Field(default_factory=DBListenerSettings)
1835
+ chunking: ChunkingSettings = Field(default_factory=ChunkingSettings)
1836
+ content: ContentSettings = Field(default_factory=ContentSettings)
1837
+ schema_search: SchemaSettings = Field(default_factory=SchemaSettings)
1838
+ email: EmailSettings = Field(default_factory=EmailSettings)
1839
+ test: TestSettings = Field(default_factory=TestSettings)
1840
+ debug: DebugSettings = Field(default_factory=DebugSettings)
1841
+
1842
+
1843
+ # Auto-load .env file from current directory if it exists
1844
+ # This happens BEFORE config file loading, so .env takes precedence
1845
+ from pathlib import Path
1846
+ from dotenv import load_dotenv
1847
+
1848
+ _dotenv_path = Path(".env")
1849
+ if _dotenv_path.exists():
1850
+ load_dotenv(_dotenv_path, override=False) # Don't override existing env vars
1851
+ logger.debug(f"Loaded environment from {_dotenv_path.resolve()}")
1852
+
1853
+ # Load configuration from ~/.rem/config.yaml before initializing settings
1854
+ # This allows user configuration to be merged with environment variables
1855
+ # Set REM_SKIP_CONFIG=1 to disable (useful for development with .env)
1856
+ if not os.getenv("REM_SKIP_CONFIG", "").lower() in ("true", "1", "yes"):
1857
+ try:
1858
+ from rem.config import load_config, merge_config_to_env
1859
+
1860
+ _config = load_config()
1861
+ if _config:
1862
+ merge_config_to_env(_config)
1863
+ except ImportError:
1864
+ # config module not available (e.g., during initial setup)
1865
+ pass
1866
+
1867
+ # Global settings singleton
1868
+ settings = Settings()
1869
+
1870
+ # Sync API keys to environment for pydantic-ai
1871
+ # Pydantic AI providers check environment directly, so we need to ensure
1872
+ # API keys from settings (LLM__*_API_KEY) are also available without prefix
1873
+ if settings.llm.openai_api_key and not os.getenv("OPENAI_API_KEY"):
1874
+ os.environ["OPENAI_API_KEY"] = settings.llm.openai_api_key
1875
+
1876
+ if settings.llm.anthropic_api_key and not os.getenv("ANTHROPIC_API_KEY"):
1877
+ os.environ["ANTHROPIC_API_KEY"] = settings.llm.anthropic_api_key