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.

Files changed (104) hide show
  1. rem/__init__.py +129 -2
  2. rem/agentic/README.md +76 -0
  3. rem/agentic/__init__.py +15 -0
  4. rem/agentic/agents/__init__.py +16 -2
  5. rem/agentic/agents/sse_simulator.py +500 -0
  6. rem/agentic/context.py +28 -22
  7. rem/agentic/llm_provider_models.py +301 -0
  8. rem/agentic/mcp/tool_wrapper.py +29 -3
  9. rem/agentic/otel/setup.py +92 -4
  10. rem/agentic/providers/phoenix.py +32 -43
  11. rem/agentic/providers/pydantic_ai.py +168 -24
  12. rem/agentic/schema.py +358 -21
  13. rem/agentic/tools/rem_tools.py +3 -3
  14. rem/api/README.md +238 -1
  15. rem/api/deps.py +255 -0
  16. rem/api/main.py +154 -37
  17. rem/api/mcp_router/resources.py +1 -1
  18. rem/api/mcp_router/server.py +26 -5
  19. rem/api/mcp_router/tools.py +454 -7
  20. rem/api/middleware/tracking.py +172 -0
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +124 -0
  23. rem/api/routers/chat/completions.py +152 -16
  24. rem/api/routers/chat/models.py +7 -3
  25. rem/api/routers/chat/sse_events.py +526 -0
  26. rem/api/routers/chat/streaming.py +608 -45
  27. rem/api/routers/dev.py +81 -0
  28. rem/api/routers/feedback.py +148 -0
  29. rem/api/routers/messages.py +473 -0
  30. rem/api/routers/models.py +78 -0
  31. rem/api/routers/query.py +360 -0
  32. rem/api/routers/shared_sessions.py +406 -0
  33. rem/auth/middleware.py +126 -27
  34. rem/cli/commands/README.md +237 -64
  35. rem/cli/commands/ask.py +15 -11
  36. rem/cli/commands/cluster.py +1300 -0
  37. rem/cli/commands/configure.py +170 -97
  38. rem/cli/commands/db.py +396 -139
  39. rem/cli/commands/experiments.py +278 -96
  40. rem/cli/commands/process.py +22 -15
  41. rem/cli/commands/scaffold.py +47 -0
  42. rem/cli/commands/schema.py +97 -50
  43. rem/cli/main.py +37 -6
  44. rem/config.py +2 -2
  45. rem/models/core/core_model.py +7 -1
  46. rem/models/core/rem_query.py +5 -2
  47. rem/models/entities/__init__.py +21 -0
  48. rem/models/entities/domain_resource.py +38 -0
  49. rem/models/entities/feedback.py +123 -0
  50. rem/models/entities/message.py +30 -1
  51. rem/models/entities/session.py +83 -0
  52. rem/models/entities/shared_session.py +180 -0
  53. rem/models/entities/user.py +10 -3
  54. rem/registry.py +373 -0
  55. rem/schemas/agents/rem.yaml +7 -3
  56. rem/services/content/providers.py +94 -140
  57. rem/services/content/service.py +115 -24
  58. rem/services/dreaming/affinity_service.py +2 -16
  59. rem/services/dreaming/moment_service.py +2 -15
  60. rem/services/embeddings/api.py +24 -17
  61. rem/services/embeddings/worker.py +16 -16
  62. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  63. rem/services/phoenix/client.py +252 -19
  64. rem/services/postgres/README.md +159 -15
  65. rem/services/postgres/__init__.py +2 -1
  66. rem/services/postgres/diff_service.py +531 -0
  67. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  68. rem/services/postgres/repository.py +132 -0
  69. rem/services/postgres/schema_generator.py +291 -9
  70. rem/services/postgres/service.py +6 -6
  71. rem/services/rate_limit.py +113 -0
  72. rem/services/rem/README.md +14 -0
  73. rem/services/rem/parser.py +44 -9
  74. rem/services/rem/service.py +36 -2
  75. rem/services/session/compression.py +17 -1
  76. rem/services/session/reload.py +1 -1
  77. rem/services/user_service.py +98 -0
  78. rem/settings.py +169 -22
  79. rem/sql/background_indexes.sql +21 -16
  80. rem/sql/migrations/001_install.sql +387 -54
  81. rem/sql/migrations/002_install_models.sql +2320 -393
  82. rem/sql/migrations/003_optional_extensions.sql +326 -0
  83. rem/sql/migrations/004_cache_system.sql +548 -0
  84. rem/utils/__init__.py +18 -0
  85. rem/utils/constants.py +97 -0
  86. rem/utils/date_utils.py +228 -0
  87. rem/utils/embeddings.py +17 -4
  88. rem/utils/files.py +167 -0
  89. rem/utils/mime_types.py +158 -0
  90. rem/utils/model_helpers.py +156 -1
  91. rem/utils/schema_loader.py +284 -21
  92. rem/utils/sql_paths.py +146 -0
  93. rem/utils/sql_types.py +3 -1
  94. rem/utils/vision.py +9 -14
  95. rem/workers/README.md +14 -14
  96. rem/workers/__init__.py +2 -1
  97. rem/workers/db_maintainer.py +74 -0
  98. rem/workers/unlogged_maintainer.py +463 -0
  99. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/METADATA +598 -171
  100. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/RECORD +102 -73
  101. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/WHEEL +1 -1
  102. rem/sql/002_install_models.sql +0 -1068
  103. rem/sql/install_models.sql +0 -1038
  104. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/entry_points.txt +0 -0
@@ -50,9 +50,36 @@ class RemQueryParser:
50
50
  params: Dict[str, Any] = {}
51
51
  positional_args: List[str] = []
52
52
 
53
- # Process remaining tokens
54
- for token in tokens[1:]:
55
- if "=" in token:
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
- params["query_text"] = combined_value
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
- # SQL typically requires named arguments (table=...), but if we supported
142
- # SQL SELECT * FROM ..., we might handle it differently.
143
- # For now, RemService expects table=...
144
- # If there are positional args for SQL, we might ignore or raise,
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
@@ -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
- params.min_similarity or 0.7,
319
- params.limit or 10,
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 = f"session-{session_id}-msg-{message_index}"
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(
@@ -65,7 +65,7 @@ async def reload_session(
65
65
  session_id=session_id, user_id=user_id, decompress=decompress_messages
66
66
  )
67
67
 
68
- logger.info(
68
+ logger.debug(
69
69
  f"Reloaded {len(messages)} messages for session {session_id} "
70
70
  f"(decompressed={decompress_messages})"
71
71
  )
@@ -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=anthropic:claude-sonnet-4-5-20250929
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
- from pydantic import Field, field_validator
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, cohere, jina, etc.)
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="anthropic:claude-sonnet-4-5-20250929",
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 (openai, cohere, jina, etc.)",
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: false)
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=False,
377
- description="Enable authentication (disabled by default for testing)",
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
- try:
1216
- from rem.config import load_config, merge_config_to_env
1217
-
1218
- _config = load_config()
1219
- if _config:
1220
- merge_config_to_env(_config)
1221
- except ImportError:
1222
- # config module not available (e.g., during initial setup)
1223
- pass
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()
@@ -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 embeddings_users
5
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_users_vector_hnsw
6
- ON embeddings_users
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 embeddings_files
30
- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_embeddings_files_vector_hnsw
31
- ON embeddings_files
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);