remdb 0.2.6__py3-none-any.whl → 0.3.118__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 -2
- rem/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +16 -2
- rem/agentic/agents/sse_simulator.py +500 -0
- rem/agentic/context.py +28 -22
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +29 -3
- rem/agentic/otel/setup.py +92 -4
- rem/agentic/providers/phoenix.py +32 -43
- rem/agentic/providers/pydantic_ai.py +168 -24
- rem/agentic/schema.py +358 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +238 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +154 -37
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +26 -5
- rem/api/mcp_router/tools.py +454 -7
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +124 -0
- rem/api/routers/chat/completions.py +152 -16
- rem/api/routers/chat/models.py +7 -3
- rem/api/routers/chat/sse_events.py +526 -0
- rem/api/routers/chat/streaming.py +608 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +148 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/ask.py +15 -11
- rem/cli/commands/cluster.py +1300 -0
- rem/cli/commands/configure.py +170 -97
- rem/cli/commands/db.py +396 -139
- rem/cli/commands/experiments.py +278 -96
- rem/cli/commands/process.py +22 -15
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +97 -50
- rem/cli/main.py +37 -6
- rem/config.py +2 -2
- rem/models/core/core_model.py +7 -1
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/__init__.py +21 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +373 -0
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/providers.py +94 -140
- rem/services/content/service.py +115 -24
- rem/services/dreaming/affinity_service.py +2 -16
- rem/services/dreaming/moment_service.py +2 -15
- rem/services/embeddings/api.py +24 -17
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
- rem/services/phoenix/client.py +252 -19
- rem/services/postgres/README.md +159 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +531 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +291 -9
- rem/services/postgres/service.py +6 -6
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +17 -1
- rem/services/session/reload.py +1 -1
- rem/services/user_service.py +98 -0
- rem/settings.py +169 -22
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2320 -393
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/embeddings.py +17 -4
- rem/utils/files.py +167 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +284 -21
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/__init__.py +2 -1
- rem/workers/db_maintainer.py +74 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/METADATA +598 -171
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/RECORD +102 -73
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1038
- {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/entry_points.txt +0 -0
rem/services/rem/parser.py
CHANGED
|
@@ -50,9 +50,36 @@ class RemQueryParser:
|
|
|
50
50
|
params: Dict[str, Any] = {}
|
|
51
51
|
positional_args: List[str] = []
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
# For SQL queries, preserve the raw query (keywords like LIMIT are SQL keywords)
|
|
54
|
+
if query_type == QueryType.SQL:
|
|
55
|
+
# Everything after "SQL" is the raw SQL query
|
|
56
|
+
raw_sql = query_string[3:].strip() # Skip "SQL" prefix
|
|
57
|
+
params["raw_query"] = raw_sql
|
|
58
|
+
return query_type, params
|
|
59
|
+
|
|
60
|
+
# Process remaining tokens, handling REM keywords
|
|
61
|
+
i = 1
|
|
62
|
+
while i < len(tokens):
|
|
63
|
+
token = tokens[i]
|
|
64
|
+
token_upper = token.upper()
|
|
65
|
+
|
|
66
|
+
# Handle REM keywords that take a value
|
|
67
|
+
if token_upper in ("LIMIT", "DEPTH", "THRESHOLD", "TYPE", "FROM", "WITH"):
|
|
68
|
+
if i + 1 < len(tokens):
|
|
69
|
+
keyword_map = {
|
|
70
|
+
"LIMIT": "limit",
|
|
71
|
+
"DEPTH": "max_depth",
|
|
72
|
+
"THRESHOLD": "threshold",
|
|
73
|
+
"TYPE": "edge_types",
|
|
74
|
+
"FROM": "initial_query",
|
|
75
|
+
"WITH": "initial_query",
|
|
76
|
+
}
|
|
77
|
+
key = keyword_map[token_upper]
|
|
78
|
+
value = tokens[i + 1]
|
|
79
|
+
params[key] = self._convert_value(key, value)
|
|
80
|
+
i += 2
|
|
81
|
+
continue
|
|
82
|
+
elif "=" in token:
|
|
56
83
|
# It's a keyword argument
|
|
57
84
|
key, value = token.split("=", 1)
|
|
58
85
|
# Handle parameter aliases
|
|
@@ -61,6 +88,7 @@ class RemQueryParser:
|
|
|
61
88
|
else:
|
|
62
89
|
# It's a positional argument part
|
|
63
90
|
positional_args.append(token)
|
|
91
|
+
i += 1
|
|
64
92
|
|
|
65
93
|
# Map positional arguments to specific fields based on QueryType
|
|
66
94
|
self._map_positional_args(query_type, positional_args, params)
|
|
@@ -133,13 +161,20 @@ class RemQueryParser:
|
|
|
133
161
|
params["query_text"] = combined_value
|
|
134
162
|
|
|
135
163
|
elif query_type == QueryType.SEARCH:
|
|
136
|
-
|
|
164
|
+
# SEARCH expects: SEARCH <table> <query_text> [LIMIT n]
|
|
165
|
+
# First positional arg is table name, rest is query text
|
|
166
|
+
if len(positional_args) >= 2:
|
|
167
|
+
params["table_name"] = positional_args[0]
|
|
168
|
+
params["query_text"] = " ".join(positional_args[1:])
|
|
169
|
+
elif len(positional_args) == 1:
|
|
170
|
+
# Could be table name or query text - assume query text if no table
|
|
171
|
+
params["query_text"] = positional_args[0]
|
|
172
|
+
# If no positional args, params stays empty
|
|
137
173
|
|
|
138
174
|
elif query_type == QueryType.TRAVERSE:
|
|
139
175
|
params["initial_query"] = combined_value
|
|
140
176
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# but current service doesn't use them.
|
|
177
|
+
elif query_type == QueryType.SQL:
|
|
178
|
+
# SQL with positional args means "SQL SELECT * FROM ..." form
|
|
179
|
+
# Treat the combined positional args as the raw SQL query
|
|
180
|
+
params["raw_query"] = combined_value
|
rem/services/rem/service.py
CHANGED
|
@@ -13,6 +13,31 @@ Design:
|
|
|
13
13
|
- All queries pushed down to Postgres for performance
|
|
14
14
|
- Model schema inspection for validation only
|
|
15
15
|
- Exceptions for missing fields/embeddings
|
|
16
|
+
|
|
17
|
+
TODO: Staged Plan Execution
|
|
18
|
+
- Implement execute_staged_plan() method for multi-stage query execution
|
|
19
|
+
- Each stage can be:
|
|
20
|
+
1. Static query (query field): Execute REM dialect directly
|
|
21
|
+
2. Dynamic query (intent field): LLM interprets intent + previous results to build query
|
|
22
|
+
- Flow for dynamic stages:
|
|
23
|
+
1. Gather results from depends_on stages (from previous_results or current execution)
|
|
24
|
+
2. Pass intent + previous results to LLM (like ask_rem but with context)
|
|
25
|
+
3. LLM generates REM query based on what it learned from previous stages
|
|
26
|
+
4. Execute generated query
|
|
27
|
+
5. Store results in stage_results for client to use in continuation
|
|
28
|
+
- Multi-turn continuation:
|
|
29
|
+
- Client passes previous_results back from response's stage_results
|
|
30
|
+
- Client sets resume_from_stage to skip already-executed stages
|
|
31
|
+
- Server uses previous_results as context for depends_on lookups
|
|
32
|
+
- Use cases:
|
|
33
|
+
- LOOKUP "Sarah" → intent: "find her team members" (LLM sees Sarah's graph_edges, builds TRAVERSE)
|
|
34
|
+
- SEARCH "API docs" → intent: "get authors" (LLM extracts author refs, builds LOOKUP)
|
|
35
|
+
- Complex graph exploration with LLM-driven navigation
|
|
36
|
+
- API: POST /api/v1/query with:
|
|
37
|
+
- mode="staged-plan"
|
|
38
|
+
- plan=[{stage, query|intent, name, depends_on}]
|
|
39
|
+
- previous_results=[{stage, name, query_executed, results, count}] (for continuation)
|
|
40
|
+
- resume_from_stage=N (to skip completed stages)
|
|
16
41
|
"""
|
|
17
42
|
|
|
18
43
|
from typing import Any
|
|
@@ -309,17 +334,26 @@ class RemService:
|
|
|
309
334
|
)
|
|
310
335
|
|
|
311
336
|
# Execute vector search via rem_search() PostgreSQL function
|
|
337
|
+
min_sim = params.min_similarity if params.min_similarity is not None else 0.3
|
|
338
|
+
limit = params.limit or 10
|
|
312
339
|
query_params = get_search_params(
|
|
313
340
|
query_embedding,
|
|
314
341
|
table_name,
|
|
315
342
|
field_name,
|
|
316
343
|
tenant_id,
|
|
317
344
|
provider,
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
min_sim,
|
|
346
|
+
limit,
|
|
320
347
|
tenant_id, # Use tenant_id (query.user_id) as user_id
|
|
321
348
|
)
|
|
349
|
+
logger.debug(
|
|
350
|
+
f"SEARCH params: table={table_name}, field={field_name}, "
|
|
351
|
+
f"tenant_id={tenant_id}, provider={provider}, "
|
|
352
|
+
f"min_similarity={min_sim}, limit={limit}, "
|
|
353
|
+
f"embedding_dims={len(query_embedding)}"
|
|
354
|
+
)
|
|
322
355
|
results = await self.db.execute(SEARCH_QUERY, query_params)
|
|
356
|
+
logger.debug(f"SEARCH results: {len(results)} rows")
|
|
323
357
|
|
|
324
358
|
return {
|
|
325
359
|
"query_type": "SEARCH",
|
|
@@ -14,6 +14,21 @@ from typing import Any
|
|
|
14
14
|
|
|
15
15
|
from loguru import logger
|
|
16
16
|
|
|
17
|
+
# Max length for entity keys (kv_store.entity_key is varchar(255))
|
|
18
|
+
MAX_ENTITY_KEY_LENGTH = 255
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def truncate_key(key: str, max_length: int = MAX_ENTITY_KEY_LENGTH) -> str:
|
|
22
|
+
"""Truncate a key to max length, preserving useful suffix if possible."""
|
|
23
|
+
if len(key) <= max_length:
|
|
24
|
+
return key
|
|
25
|
+
# Keep first part and add hash suffix for uniqueness
|
|
26
|
+
import hashlib
|
|
27
|
+
hash_suffix = hashlib.md5(key.encode()).hexdigest()[:8]
|
|
28
|
+
truncated = key[:max_length - 9] + "-" + hash_suffix
|
|
29
|
+
logger.warning(f"Truncated key from {len(key)} to {len(truncated)} chars: {key[:50]}...")
|
|
30
|
+
return truncated
|
|
31
|
+
|
|
17
32
|
from rem.models.entities import Message
|
|
18
33
|
from rem.services.postgres import PostgresService, Repository
|
|
19
34
|
from rem.settings import settings
|
|
@@ -151,7 +166,8 @@ class SessionMessageStore:
|
|
|
151
166
|
return f"msg-{message_index}"
|
|
152
167
|
|
|
153
168
|
# Create entity key for REM LOOKUP: session-{session_id}-msg-{index}
|
|
154
|
-
entity_key
|
|
169
|
+
# Truncate to avoid exceeding kv_store.entity_key varchar(255) limit
|
|
170
|
+
entity_key = truncate_key(f"session-{session_id}-msg-{message_index}")
|
|
155
171
|
|
|
156
172
|
# Create Message entity for assistant response
|
|
157
173
|
msg = Message(
|
rem/services/session/reload.py
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Service - User account management.
|
|
3
|
+
|
|
4
|
+
Handles user creation, profile updates, and session linking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from ..models.entities.user import User, UserTier
|
|
13
|
+
from .postgres.repository import Repository
|
|
14
|
+
from .postgres.service import PostgresService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UserService:
|
|
18
|
+
"""
|
|
19
|
+
Service for managing user accounts and sessions.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, db: PostgresService):
|
|
23
|
+
self.db = db
|
|
24
|
+
self.repo = Repository(User, "users", db=db)
|
|
25
|
+
|
|
26
|
+
async def get_or_create_user(
|
|
27
|
+
self,
|
|
28
|
+
email: str,
|
|
29
|
+
tenant_id: str = "default",
|
|
30
|
+
name: str = "New User",
|
|
31
|
+
avatar_url: Optional[str] = None,
|
|
32
|
+
) -> User:
|
|
33
|
+
"""
|
|
34
|
+
Get existing user by email or create a new one.
|
|
35
|
+
"""
|
|
36
|
+
users = await self.repo.find(filters={"email": email}, limit=1)
|
|
37
|
+
|
|
38
|
+
if users:
|
|
39
|
+
user = users[0]
|
|
40
|
+
# Update profile if needed (e.g., name/avatar from OAuth)
|
|
41
|
+
updated = False
|
|
42
|
+
if name and user.name == "New User": # Only update if placeholder
|
|
43
|
+
user.name = name
|
|
44
|
+
updated = True
|
|
45
|
+
|
|
46
|
+
# Store avatar in metadata if provided
|
|
47
|
+
if avatar_url:
|
|
48
|
+
user.metadata = user.metadata or {}
|
|
49
|
+
if user.metadata.get("avatar_url") != avatar_url:
|
|
50
|
+
user.metadata["avatar_url"] = avatar_url
|
|
51
|
+
updated = True
|
|
52
|
+
|
|
53
|
+
if updated:
|
|
54
|
+
user.updated_at = datetime.utcnow()
|
|
55
|
+
await self.repo.upsert(user)
|
|
56
|
+
|
|
57
|
+
return user
|
|
58
|
+
|
|
59
|
+
# Create new user
|
|
60
|
+
user = User(
|
|
61
|
+
tenant_id=tenant_id,
|
|
62
|
+
user_id=email, # Use email as user_id for now? Or UUID?
|
|
63
|
+
# The User model has 'user_id' field but also 'id' UUID.
|
|
64
|
+
# Usually user_id is the external ID or email.
|
|
65
|
+
name=name,
|
|
66
|
+
email=email,
|
|
67
|
+
tier=UserTier.FREE,
|
|
68
|
+
created_at=datetime.utcnow(),
|
|
69
|
+
updated_at=datetime.utcnow(),
|
|
70
|
+
metadata={"avatar_url": avatar_url} if avatar_url else {},
|
|
71
|
+
)
|
|
72
|
+
await self.repo.upsert(user)
|
|
73
|
+
logger.info(f"Created new user: {email}")
|
|
74
|
+
return user
|
|
75
|
+
|
|
76
|
+
async def link_anonymous_session(self, user: User, anon_id: str) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Link an anonymous session ID to a user account.
|
|
79
|
+
|
|
80
|
+
This allows merging history from the anonymous session into the user's profile.
|
|
81
|
+
"""
|
|
82
|
+
if not anon_id:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Check if already linked
|
|
86
|
+
if anon_id in user.anonymous_ids:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Add to list
|
|
90
|
+
user.anonymous_ids.append(anon_id)
|
|
91
|
+
user.updated_at = datetime.utcnow()
|
|
92
|
+
|
|
93
|
+
# Save
|
|
94
|
+
await self.repo.upsert(user)
|
|
95
|
+
logger.info(f"Linked anonymous session {anon_id} to user {user.email}")
|
|
96
|
+
|
|
97
|
+
# TODO: Migrate/Merge actual data (rate limit counts, history) if needed.
|
|
98
|
+
# For now, we just link the IDs so future queries can include data from this anon_id.
|
rem/settings.py
CHANGED
|
@@ -15,7 +15,7 @@ Example .env file:
|
|
|
15
15
|
API__LOG_LEVEL=info
|
|
16
16
|
|
|
17
17
|
# LLM
|
|
18
|
-
LLM__DEFAULT_MODEL=
|
|
18
|
+
LLM__DEFAULT_MODEL=openai:gpt-4.1
|
|
19
19
|
LLM__DEFAULT_TEMPERATURE=0.5
|
|
20
20
|
LLM__MAX_RETRIES=10
|
|
21
21
|
LLM__OPENAI_API_KEY=sk-...
|
|
@@ -57,8 +57,10 @@ Example .env file:
|
|
|
57
57
|
"""
|
|
58
58
|
|
|
59
59
|
import os
|
|
60
|
-
|
|
60
|
+
import hashlib
|
|
61
|
+
from pydantic import Field, field_validator, ValidationInfo
|
|
61
62
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
63
|
+
from loguru import logger
|
|
62
64
|
|
|
63
65
|
|
|
64
66
|
class LLMSettings(BaseSettings):
|
|
@@ -72,7 +74,7 @@ class LLMSettings(BaseSettings):
|
|
|
72
74
|
LLM__EVALUATOR_MODEL or EVALUATOR_MODEL - Model for LLM-as-judge evaluation
|
|
73
75
|
LLM__OPENAI_API_KEY or OPENAI_API_KEY - OpenAI API key
|
|
74
76
|
LLM__ANTHROPIC_API_KEY or ANTHROPIC_API_KEY - Anthropic API key
|
|
75
|
-
LLM__EMBEDDING_PROVIDER or EMBEDDING_PROVIDER - Default embedding provider (openai
|
|
77
|
+
LLM__EMBEDDING_PROVIDER or EMBEDDING_PROVIDER - Default embedding provider (openai)
|
|
76
78
|
LLM__EMBEDDING_MODEL or EMBEDDING_MODEL - Default embedding model name
|
|
77
79
|
"""
|
|
78
80
|
|
|
@@ -84,7 +86,7 @@ class LLMSettings(BaseSettings):
|
|
|
84
86
|
)
|
|
85
87
|
|
|
86
88
|
default_model: str = Field(
|
|
87
|
-
default="
|
|
89
|
+
default="openai:gpt-4.1",
|
|
88
90
|
description="Default LLM model (format: provider:model-id)",
|
|
89
91
|
)
|
|
90
92
|
|
|
@@ -127,7 +129,7 @@ class LLMSettings(BaseSettings):
|
|
|
127
129
|
|
|
128
130
|
embedding_provider: str = Field(
|
|
129
131
|
default="openai",
|
|
130
|
-
description="Default embedding provider (
|
|
132
|
+
description="Default embedding provider (currently only openai supported)",
|
|
131
133
|
)
|
|
132
134
|
|
|
133
135
|
embedding_model: str = Field(
|
|
@@ -359,10 +361,16 @@ class AuthSettings(BaseSettings):
|
|
|
359
361
|
- Custom OIDC provider
|
|
360
362
|
|
|
361
363
|
Environment variables:
|
|
362
|
-
AUTH__ENABLED - Enable authentication (default:
|
|
364
|
+
AUTH__ENABLED - Enable authentication (default: true)
|
|
365
|
+
AUTH__ALLOW_ANONYMOUS - Allow rate-limited anonymous access (default: true)
|
|
363
366
|
AUTH__SESSION_SECRET - Secret for session cookie signing
|
|
364
367
|
AUTH__GOOGLE__* - Google OAuth settings
|
|
365
368
|
AUTH__MICROSOFT__* - Microsoft OAuth settings
|
|
369
|
+
|
|
370
|
+
Access modes:
|
|
371
|
+
- enabled=true, allow_anonymous=true: Auth available, anonymous gets rate-limited access
|
|
372
|
+
- enabled=true, allow_anonymous=false: Auth required for all requests
|
|
373
|
+
- enabled=false: No auth, all requests treated as default user (dev mode)
|
|
366
374
|
"""
|
|
367
375
|
|
|
368
376
|
model_config = SettingsConfigDict(
|
|
@@ -373,8 +381,26 @@ class AuthSettings(BaseSettings):
|
|
|
373
381
|
)
|
|
374
382
|
|
|
375
383
|
enabled: bool = Field(
|
|
376
|
-
default=
|
|
377
|
-
description="Enable authentication (
|
|
384
|
+
default=True,
|
|
385
|
+
description="Enable authentication (OAuth endpoints and middleware)",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
allow_anonymous: bool = Field(
|
|
389
|
+
default=True,
|
|
390
|
+
description=(
|
|
391
|
+
"Allow anonymous (unauthenticated) access with rate limits. "
|
|
392
|
+
"When true, requests without auth get ANONYMOUS tier rate limits. "
|
|
393
|
+
"When false, all requests require authentication."
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
mcp_requires_auth: bool = Field(
|
|
398
|
+
default=True,
|
|
399
|
+
description=(
|
|
400
|
+
"Require authentication for MCP endpoints. "
|
|
401
|
+
"MCP is a protected service and should always require login in production. "
|
|
402
|
+
"Set to false only for local development/testing."
|
|
403
|
+
),
|
|
378
404
|
)
|
|
379
405
|
|
|
380
406
|
session_secret: str = Field(
|
|
@@ -386,6 +412,22 @@ class AuthSettings(BaseSettings):
|
|
|
386
412
|
google: GoogleOAuthSettings = Field(default_factory=GoogleOAuthSettings)
|
|
387
413
|
microsoft: MicrosoftOAuthSettings = Field(default_factory=MicrosoftOAuthSettings)
|
|
388
414
|
|
|
415
|
+
@field_validator("session_secret", mode="before")
|
|
416
|
+
@classmethod
|
|
417
|
+
def generate_dev_secret(cls, v: str | None, info: ValidationInfo) -> str:
|
|
418
|
+
# Only generate if not already set and not in production
|
|
419
|
+
if not v and info.data.get("environment") != "production":
|
|
420
|
+
# Deterministic secret for development
|
|
421
|
+
seed_string = f"{info.data.get('team', 'rem')}-{info.data.get('environment', 'development')}-auth-secret-salt"
|
|
422
|
+
logger.warning(
|
|
423
|
+
"AUTH__SESSION_SECRET not set. Generating deterministic secret for non-production environment. "
|
|
424
|
+
"DO NOT use in production."
|
|
425
|
+
)
|
|
426
|
+
return hashlib.sha256(seed_string.encode()).hexdigest()
|
|
427
|
+
elif not v and info.data.get("environment") == "production":
|
|
428
|
+
raise ValueError("AUTH__SESSION_SECRET must be set in production environment.")
|
|
429
|
+
return v
|
|
430
|
+
|
|
389
431
|
|
|
390
432
|
class PostgresSettings(BaseSettings):
|
|
391
433
|
"""
|
|
@@ -962,6 +1004,107 @@ class APISettings(BaseSettings):
|
|
|
962
1004
|
)
|
|
963
1005
|
|
|
964
1006
|
|
|
1007
|
+
class ModelsSettings(BaseSettings):
|
|
1008
|
+
"""
|
|
1009
|
+
Custom model registration settings for downstream applications.
|
|
1010
|
+
|
|
1011
|
+
Allows downstream apps to specify Python modules containing custom models
|
|
1012
|
+
that should be imported (and thus registered) before schema generation.
|
|
1013
|
+
|
|
1014
|
+
This enables `rem db schema generate` to discover models registered with
|
|
1015
|
+
`@rem.register_model` in downstream applications.
|
|
1016
|
+
|
|
1017
|
+
Environment variables:
|
|
1018
|
+
MODELS__IMPORT_MODULES - Semicolon-separated list of Python modules to import
|
|
1019
|
+
Example: "models;myapp.entities;myapp.custom_models"
|
|
1020
|
+
|
|
1021
|
+
Example:
|
|
1022
|
+
# In downstream app's .env
|
|
1023
|
+
MODELS__IMPORT_MODULES=models
|
|
1024
|
+
|
|
1025
|
+
# In downstream app's models/__init__.py
|
|
1026
|
+
import rem
|
|
1027
|
+
from rem.models.core import CoreModel
|
|
1028
|
+
|
|
1029
|
+
@rem.register_model
|
|
1030
|
+
class MyCustomEntity(CoreModel):
|
|
1031
|
+
name: str
|
|
1032
|
+
|
|
1033
|
+
# Then run schema generation
|
|
1034
|
+
rem db schema generate # Includes MyCustomEntity
|
|
1035
|
+
"""
|
|
1036
|
+
|
|
1037
|
+
model_config = SettingsConfigDict(
|
|
1038
|
+
env_prefix="MODELS__",
|
|
1039
|
+
extra="ignore",
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
import_modules: str = Field(
|
|
1043
|
+
default="",
|
|
1044
|
+
description=(
|
|
1045
|
+
"Semicolon-separated list of Python modules to import for model registration. "
|
|
1046
|
+
"These modules are imported before schema generation to ensure custom models "
|
|
1047
|
+
"decorated with @rem.register_model are discovered. "
|
|
1048
|
+
"Example: 'models;myapp.entities'"
|
|
1049
|
+
),
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
@property
|
|
1053
|
+
def module_list(self) -> list[str]:
|
|
1054
|
+
"""Get modules as a list, filtering empty strings."""
|
|
1055
|
+
if not self.import_modules:
|
|
1056
|
+
return []
|
|
1057
|
+
return [m.strip() for m in self.import_modules.split(";") if m.strip()]
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
class SchemaSettings(BaseSettings):
|
|
1061
|
+
"""
|
|
1062
|
+
Schema search path settings for agent and evaluator schemas.
|
|
1063
|
+
|
|
1064
|
+
Allows extending REM's schema search with custom directories.
|
|
1065
|
+
Custom paths are searched BEFORE built-in package schemas.
|
|
1066
|
+
|
|
1067
|
+
Environment variables:
|
|
1068
|
+
SCHEMA__PATHS - Semicolon-separated list of directories to search
|
|
1069
|
+
Example: "/app/schemas;/shared/agents;./local-schemas"
|
|
1070
|
+
|
|
1071
|
+
Search Order:
|
|
1072
|
+
1. Exact path (if file exists)
|
|
1073
|
+
2. Custom paths from SCHEMA__PATHS (in order)
|
|
1074
|
+
3. Built-in package schemas (schemas/agents/, schemas/evaluators/, etc.)
|
|
1075
|
+
4. Database LOOKUP (if enabled)
|
|
1076
|
+
|
|
1077
|
+
Example:
|
|
1078
|
+
# In .env or environment
|
|
1079
|
+
SCHEMA__PATHS=/app/custom-agents;/shared/evaluators
|
|
1080
|
+
|
|
1081
|
+
# Then in code
|
|
1082
|
+
from rem.utils.schema_loader import load_agent_schema
|
|
1083
|
+
schema = load_agent_schema("my-custom-agent") # Found in /app/custom-agents/
|
|
1084
|
+
"""
|
|
1085
|
+
|
|
1086
|
+
model_config = SettingsConfigDict(
|
|
1087
|
+
env_prefix="SCHEMA__",
|
|
1088
|
+
extra="ignore",
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
paths: str = Field(
|
|
1092
|
+
default="",
|
|
1093
|
+
description=(
|
|
1094
|
+
"Semicolon-separated list of directories to search for schemas. "
|
|
1095
|
+
"These paths are searched BEFORE built-in package schemas. "
|
|
1096
|
+
"Example: '/app/schemas;/shared/agents'"
|
|
1097
|
+
),
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
@property
|
|
1101
|
+
def path_list(self) -> list[str]:
|
|
1102
|
+
"""Get paths as a list, filtering empty strings."""
|
|
1103
|
+
if not self.paths:
|
|
1104
|
+
return []
|
|
1105
|
+
return [p.strip() for p in self.paths.split(";") if p.strip()]
|
|
1106
|
+
|
|
1107
|
+
|
|
965
1108
|
class GitSettings(BaseSettings):
|
|
966
1109
|
"""
|
|
967
1110
|
Git repository provider settings for versioned schema/experiment syncing.
|
|
@@ -1166,6 +1309,11 @@ class Settings(BaseSettings):
|
|
|
1166
1309
|
extra="ignore",
|
|
1167
1310
|
)
|
|
1168
1311
|
|
|
1312
|
+
app_name: str = Field(
|
|
1313
|
+
default="REM",
|
|
1314
|
+
description="Application/API name used in docs, titles, and user-facing text",
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1169
1317
|
team: str = Field(
|
|
1170
1318
|
default="rem",
|
|
1171
1319
|
description="Team or project name for observability",
|
|
@@ -1186,16 +1334,12 @@ class Settings(BaseSettings):
|
|
|
1186
1334
|
description="Root path for reverse proxy (e.g., /rem for ALB routing)",
|
|
1187
1335
|
)
|
|
1188
1336
|
|
|
1189
|
-
sql_dir: str = Field(
|
|
1190
|
-
default="src/rem/sql",
|
|
1191
|
-
description="Directory for SQL files and migrations",
|
|
1192
|
-
)
|
|
1193
|
-
|
|
1194
1337
|
# Nested settings groups
|
|
1195
1338
|
api: APISettings = Field(default_factory=APISettings)
|
|
1196
1339
|
chat: ChatSettings = Field(default_factory=ChatSettings)
|
|
1197
1340
|
llm: LLMSettings = Field(default_factory=LLMSettings)
|
|
1198
1341
|
mcp: MCPSettings = Field(default_factory=MCPSettings)
|
|
1342
|
+
models: ModelsSettings = Field(default_factory=ModelsSettings)
|
|
1199
1343
|
otel: OTELSettings = Field(default_factory=OTELSettings)
|
|
1200
1344
|
phoenix: PhoenixSettings = Field(default_factory=PhoenixSettings)
|
|
1201
1345
|
auth: AuthSettings = Field(default_factory=AuthSettings)
|
|
@@ -1207,20 +1351,23 @@ class Settings(BaseSettings):
|
|
|
1207
1351
|
sqs: SQSSettings = Field(default_factory=SQSSettings)
|
|
1208
1352
|
chunking: ChunkingSettings = Field(default_factory=ChunkingSettings)
|
|
1209
1353
|
content: ContentSettings = Field(default_factory=ContentSettings)
|
|
1354
|
+
schema_search: SchemaSettings = Field(default_factory=SchemaSettings)
|
|
1210
1355
|
test: TestSettings = Field(default_factory=TestSettings)
|
|
1211
1356
|
|
|
1212
1357
|
|
|
1213
1358
|
# Load configuration from ~/.rem/config.yaml before initializing settings
|
|
1214
1359
|
# This allows user configuration to be merged with environment variables
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1360
|
+
# Set REM_SKIP_CONFIG_FILE=true to disable (useful for development with .env)
|
|
1361
|
+
if not os.getenv("REM_SKIP_CONFIG_FILE", "").lower() in ("true", "1", "yes"):
|
|
1362
|
+
try:
|
|
1363
|
+
from rem.config import load_config, merge_config_to_env
|
|
1364
|
+
|
|
1365
|
+
_config = load_config()
|
|
1366
|
+
if _config:
|
|
1367
|
+
merge_config_to_env(_config)
|
|
1368
|
+
except ImportError:
|
|
1369
|
+
# config module not available (e.g., during initial setup)
|
|
1370
|
+
pass
|
|
1224
1371
|
|
|
1225
1372
|
# Global settings singleton
|
|
1226
1373
|
settings = Settings()
|
rem/sql/background_indexes.sql
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
-- Background index creation
|
|
2
2
|
-- Run AFTER initial data load to avoid blocking writes
|
|
3
3
|
|
|
4
|
-
-- HNSW vector index for
|
|
5
|
-
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
6
|
-
ON
|
|
4
|
+
-- HNSW vector index for embeddings_files
|
|
5
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_files_vector_hnsw
|
|
6
|
+
ON embeddings_files
|
|
7
7
|
USING hnsw (embedding vector_cosine_ops);
|
|
8
8
|
|
|
9
9
|
-- HNSW vector index for embeddings_image_resources
|
|
@@ -11,24 +11,14 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_image_resources_vector_hn
|
|
|
11
11
|
ON embeddings_image_resources
|
|
12
12
|
USING hnsw (embedding vector_cosine_ops);
|
|
13
13
|
|
|
14
|
-
-- HNSW vector index for embeddings_moments
|
|
15
|
-
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_moments_vector_hnsw
|
|
16
|
-
ON embeddings_moments
|
|
17
|
-
USING hnsw (embedding vector_cosine_ops);
|
|
18
|
-
|
|
19
|
-
-- HNSW vector index for embeddings_resources
|
|
20
|
-
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_resources_vector_hnsw
|
|
21
|
-
ON embeddings_resources
|
|
22
|
-
USING hnsw (embedding vector_cosine_ops);
|
|
23
|
-
|
|
24
14
|
-- HNSW vector index for embeddings_messages
|
|
25
15
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_messages_vector_hnsw
|
|
26
16
|
ON embeddings_messages
|
|
27
17
|
USING hnsw (embedding vector_cosine_ops);
|
|
28
18
|
|
|
29
|
-
-- HNSW vector index for
|
|
30
|
-
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
|
31
|
-
ON
|
|
19
|
+
-- HNSW vector index for embeddings_moments
|
|
20
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_moments_vector_hnsw
|
|
21
|
+
ON embeddings_moments
|
|
32
22
|
USING hnsw (embedding vector_cosine_ops);
|
|
33
23
|
|
|
34
24
|
-- HNSW vector index for embeddings_ontology_configs
|
|
@@ -36,7 +26,22 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_ontology_configs_vector_h
|
|
|
36
26
|
ON embeddings_ontology_configs
|
|
37
27
|
USING hnsw (embedding vector_cosine_ops);
|
|
38
28
|
|
|
29
|
+
-- HNSW vector index for embeddings_resources
|
|
30
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_resources_vector_hnsw
|
|
31
|
+
ON embeddings_resources
|
|
32
|
+
USING hnsw (embedding vector_cosine_ops);
|
|
33
|
+
|
|
39
34
|
-- HNSW vector index for embeddings_schemas
|
|
40
35
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_schemas_vector_hnsw
|
|
41
36
|
ON embeddings_schemas
|
|
42
37
|
USING hnsw (embedding vector_cosine_ops);
|
|
38
|
+
|
|
39
|
+
-- HNSW vector index for embeddings_sessions
|
|
40
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_sessions_vector_hnsw
|
|
41
|
+
ON embeddings_sessions
|
|
42
|
+
USING hnsw (embedding vector_cosine_ops);
|
|
43
|
+
|
|
44
|
+
-- HNSW vector index for embeddings_users
|
|
45
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_users_vector_hnsw
|
|
46
|
+
ON embeddings_users
|
|
47
|
+
USING hnsw (embedding vector_cosine_ops);
|