remdb 0.3.242__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/__init__.py +129 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- remdb-0.3.242.dist-info/entry_points.txt +2 -0
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
|