remdb 0.3.0__py3-none-any.whl → 0.3.114__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 (98) 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/otel/setup.py +92 -4
  9. rem/agentic/providers/phoenix.py +32 -43
  10. rem/agentic/providers/pydantic_ai.py +142 -22
  11. rem/agentic/schema.py +358 -21
  12. rem/agentic/tools/rem_tools.py +3 -3
  13. rem/api/README.md +238 -1
  14. rem/api/deps.py +255 -0
  15. rem/api/main.py +151 -37
  16. rem/api/mcp_router/resources.py +1 -1
  17. rem/api/mcp_router/server.py +17 -2
  18. rem/api/mcp_router/tools.py +143 -7
  19. rem/api/middleware/tracking.py +172 -0
  20. rem/api/routers/admin.py +277 -0
  21. rem/api/routers/auth.py +124 -0
  22. rem/api/routers/chat/completions.py +152 -16
  23. rem/api/routers/chat/models.py +7 -3
  24. rem/api/routers/chat/sse_events.py +526 -0
  25. rem/api/routers/chat/streaming.py +608 -45
  26. rem/api/routers/dev.py +81 -0
  27. rem/api/routers/feedback.py +148 -0
  28. rem/api/routers/messages.py +473 -0
  29. rem/api/routers/models.py +78 -0
  30. rem/api/routers/query.py +357 -0
  31. rem/api/routers/shared_sessions.py +406 -0
  32. rem/auth/middleware.py +126 -27
  33. rem/cli/commands/README.md +201 -70
  34. rem/cli/commands/ask.py +13 -10
  35. rem/cli/commands/cluster.py +1359 -0
  36. rem/cli/commands/configure.py +4 -3
  37. rem/cli/commands/db.py +350 -137
  38. rem/cli/commands/experiments.py +76 -72
  39. rem/cli/commands/process.py +22 -15
  40. rem/cli/commands/scaffold.py +47 -0
  41. rem/cli/commands/schema.py +95 -49
  42. rem/cli/main.py +29 -6
  43. rem/config.py +2 -2
  44. rem/models/core/core_model.py +7 -1
  45. rem/models/core/rem_query.py +5 -2
  46. rem/models/entities/__init__.py +21 -0
  47. rem/models/entities/domain_resource.py +38 -0
  48. rem/models/entities/feedback.py +123 -0
  49. rem/models/entities/message.py +30 -1
  50. rem/models/entities/session.py +83 -0
  51. rem/models/entities/shared_session.py +180 -0
  52. rem/models/entities/user.py +10 -3
  53. rem/registry.py +373 -0
  54. rem/schemas/agents/rem.yaml +7 -3
  55. rem/services/content/providers.py +94 -140
  56. rem/services/content/service.py +92 -20
  57. rem/services/dreaming/affinity_service.py +2 -16
  58. rem/services/dreaming/moment_service.py +2 -15
  59. rem/services/embeddings/api.py +24 -17
  60. rem/services/embeddings/worker.py +16 -16
  61. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  62. rem/services/phoenix/client.py +252 -19
  63. rem/services/postgres/README.md +159 -15
  64. rem/services/postgres/__init__.py +2 -1
  65. rem/services/postgres/diff_service.py +426 -0
  66. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  67. rem/services/postgres/repository.py +132 -0
  68. rem/services/postgres/schema_generator.py +86 -5
  69. rem/services/postgres/service.py +6 -6
  70. rem/services/rate_limit.py +113 -0
  71. rem/services/rem/README.md +14 -0
  72. rem/services/rem/parser.py +44 -9
  73. rem/services/rem/service.py +36 -2
  74. rem/services/session/compression.py +17 -1
  75. rem/services/session/reload.py +1 -1
  76. rem/services/user_service.py +98 -0
  77. rem/settings.py +169 -17
  78. rem/sql/background_indexes.sql +21 -16
  79. rem/sql/migrations/001_install.sql +231 -54
  80. rem/sql/migrations/002_install_models.sql +457 -393
  81. rem/sql/migrations/003_optional_extensions.sql +326 -0
  82. rem/utils/constants.py +97 -0
  83. rem/utils/date_utils.py +228 -0
  84. rem/utils/embeddings.py +17 -4
  85. rem/utils/files.py +167 -0
  86. rem/utils/mime_types.py +158 -0
  87. rem/utils/model_helpers.py +156 -1
  88. rem/utils/schema_loader.py +191 -35
  89. rem/utils/sql_types.py +3 -1
  90. rem/utils/vision.py +9 -14
  91. rem/workers/README.md +14 -14
  92. rem/workers/db_maintainer.py +74 -0
  93. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/METADATA +303 -164
  94. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/RECORD +96 -70
  95. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/WHEEL +1 -1
  96. rem/sql/002_install_models.sql +0 -1068
  97. rem/sql/install_models.sql +0 -1038
  98. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/entry_points.txt +0 -0
@@ -9,7 +9,7 @@ Design Pattern:
9
9
  - Support short names: "contract-analyzer" → "schemas/agents/contract-analyzer.yaml"
10
10
  - Support relative/absolute paths
11
11
  - Consistent error messages and logging
12
- i
12
+
13
13
  Usage:
14
14
  # From API
15
15
  schema = load_agent_schema("rem")
@@ -20,6 +20,26 @@ Usage:
20
20
  # From agent factory
21
21
  schema = load_agent_schema("contract-analyzer")
22
22
 
23
+ TODO: Git FS Integration
24
+ The schema loader currently uses importlib.resources for package schemas
25
+ and direct filesystem access for custom paths. The FS abstraction layer
26
+ (rem.services.fs.FS) could be used to abstract storage backends:
27
+
28
+ - Local filesystem (current)
29
+ - Git repositories (GitService)
30
+ - S3 (via FS provider)
31
+
32
+ This would enable loading schemas from versioned Git repos or S3 buckets
33
+ without changing the API. The FS provider pattern already exists and just
34
+ needs integration testing with the schema loader.
35
+
36
+ Example future usage:
37
+ # Load from Git at specific version
38
+ schema = load_agent_schema("git://rem/schemas/agents/rem.yaml?ref=v1.0.0")
39
+
40
+ # Load from S3
41
+ schema = load_agent_schema("s3://rem-schemas/agents/cv-parser.yaml")
42
+
23
43
  Schema Caching Status:
24
44
 
25
45
  ✅ IMPLEMENTED: Filesystem Schema Caching (2025-11-22)
@@ -71,13 +91,14 @@ import yaml
71
91
  from loguru import logger
72
92
 
73
93
 
74
- # Standard search paths for agent schemas (in priority order)
94
+ # Standard search paths for agent/evaluator schemas (in priority order)
75
95
  SCHEMA_SEARCH_PATHS = [
76
96
  "schemas/agents/{name}.yaml", # Top-level agents (e.g., rem.yaml)
77
97
  "schemas/agents/core/{name}.yaml", # Core system agents
78
98
  "schemas/agents/examples/{name}.yaml", # Example agents
79
- "schemas/evaluators/{name}.yaml",
80
- "schemas/{name}.yaml",
99
+ "schemas/evaluators/{name}.yaml", # Nested evaluators (e.g., hello-world/default)
100
+ "schemas/evaluators/rem/{name}.yaml", # REM evaluators (e.g., lookup-correctness)
101
+ "schemas/{name}.yaml", # Generic schemas
81
102
  ]
82
103
 
83
104
  # In-memory cache for filesystem schemas (no TTL - immutable)
@@ -125,7 +146,6 @@ def _load_schema_from_database(schema_name: str, user_id: str) -> dict[str, Any]
125
146
  async def _async_lookup():
126
147
  """Async helper to query database."""
127
148
  from rem.services.postgres import get_postgres_service
128
- from rem.models.entities import Schema
129
149
 
130
150
  db = get_postgres_service()
131
151
  if not db:
@@ -135,19 +155,20 @@ def _load_schema_from_database(schema_name: str, user_id: str) -> dict[str, Any]
135
155
  try:
136
156
  await db.connect()
137
157
 
138
- # Use REM LOOKUP query to find schema
139
- query = f"LOOKUP '{schema_name}' FROM schemas"
140
- logger.debug(f"Executing: {query} (user_id={user_id})")
158
+ # Query schemas table directly by name
159
+ # Note: Schema name lookup is case-insensitive for user convenience
160
+ query = """
161
+ SELECT spec FROM schemas
162
+ WHERE LOWER(name) = LOWER($1)
163
+ AND (user_id = $2 OR user_id = 'system')
164
+ LIMIT 1
165
+ """
166
+ logger.debug(f"Executing schema lookup: name={schema_name}, user_id={user_id}")
141
167
 
142
- result = await db.execute_rem_query(
143
- query=query,
144
- user_id=user_id,
145
- )
168
+ row = await db.fetchrow(query, schema_name, user_id)
146
169
 
147
- if result and isinstance(result, dict):
148
- # LOOKUP returns single entity or None
149
- # Extract spec field (JSON Schema)
150
- spec = result.get("spec")
170
+ if row:
171
+ spec = row.get("spec")
151
172
  if spec and isinstance(spec, dict):
152
173
  logger.debug(f"Found schema in database: {schema_name}")
153
174
  return spec
@@ -174,6 +195,8 @@ def load_agent_schema(
174
195
  """
175
196
  Load agent schema from YAML file with unified search logic and caching.
176
197
 
198
+ Schema names are case-invariant - "Siggy", "siggy", "SIGGY" all resolve to the same schema.
199
+
177
200
  Filesystem schemas are cached indefinitely (immutable, versioned with code).
178
201
  Database schemas (future) will be cached with TTL for invalidation.
179
202
 
@@ -188,16 +211,17 @@ def load_agent_schema(
188
211
  Search Order:
189
212
  1. Check cache (if use_cache=True and schema found in FS cache)
190
213
  2. Exact path if it exists (absolute or relative)
191
- 3. Package resources: schemas/agents/{name}.yaml (top-level)
192
- 4. Package resources: schemas/agents/core/{name}.yaml
193
- 5. Package resources: schemas/agents/examples/{name}.yaml
194
- 6. Package resources: schemas/evaluators/{name}.yaml
195
- 7. Package resources: schemas/{name}.yaml
196
- 8. Database LOOKUP: schemas table (if enable_db_fallback=True and user_id provided)
214
+ 3. Custom paths from rem.register_schema_path() and SCHEMA__PATHS env var
215
+ 4. Package resources: schemas/agents/{name}.yaml (top-level)
216
+ 5. Package resources: schemas/agents/core/{name}.yaml
217
+ 6. Package resources: schemas/agents/examples/{name}.yaml
218
+ 7. Package resources: schemas/evaluators/{name}.yaml
219
+ 8. Package resources: schemas/{name}.yaml
220
+ 9. Database LOOKUP: schemas table (if enable_db_fallback=True and user_id provided)
197
221
 
198
222
  Args:
199
- schema_name_or_path: Schema name or file path
200
- Examples: "rem-query-agent", "contract-analyzer", "./my-schema.yaml"
223
+ schema_name_or_path: Schema name or file path (case-invariant for names)
224
+ Examples: "rem-query-agent", "Contract-Analyzer", "./my-schema.yaml"
201
225
  use_cache: If True, uses in-memory cache for filesystem schemas
202
226
  user_id: User ID for database schema lookup (required for DB fallback)
203
227
  enable_db_fallback: If True, falls back to database LOOKUP when file not found
@@ -210,8 +234,8 @@ def load_agent_schema(
210
234
  yaml.YAMLError: If schema file is invalid YAML
211
235
 
212
236
  Examples:
213
- >>> # Load by short name (cached after first load)
214
- >>> schema = load_agent_schema("contract-analyzer")
237
+ >>> # Load by short name (cached after first load) - case invariant
238
+ >>> schema = load_agent_schema("Contract-Analyzer") # same as "contract-analyzer"
215
239
  >>>
216
240
  >>> # Load from custom path (not cached - custom paths may change)
217
241
  >>> schema = load_agent_schema("./my-agent.yaml")
@@ -219,11 +243,11 @@ def load_agent_schema(
219
243
  >>> # Load evaluator schema (cached)
220
244
  >>> schema = load_agent_schema("rem-lookup-correctness")
221
245
  >>>
222
- >>> # Load custom user schema from database
223
- >>> schema = load_agent_schema("my-custom-agent", user_id="user-123")
246
+ >>> # Load custom user schema from database (case invariant)
247
+ >>> schema = load_agent_schema("My-Agent", user_id="user-123") # same as "my-agent"
224
248
  """
225
- # Normalize the name for cache key
226
- cache_key = str(schema_name_or_path).replace('agents/', '').replace('schemas/', '').replace('evaluators/', '').replace('core/', '').replace('examples/', '')
249
+ # Normalize the name for cache key (lowercase for case-invariant lookups)
250
+ cache_key = str(schema_name_or_path).replace('agents/', '').replace('schemas/', '').replace('evaluators/', '').replace('core/', '').replace('examples/', '').lower()
227
251
  if cache_key.endswith('.yaml') or cache_key.endswith('.yml'):
228
252
  cache_key = cache_key.rsplit('.', 1)[0]
229
253
 
@@ -244,10 +268,31 @@ def load_agent_schema(
244
268
  # Don't cache custom paths (they may change)
245
269
  return cast(dict[str, Any], schema)
246
270
 
247
- # 2. Normalize name for package resource search
271
+ # 2. Normalize name for package resource search (lowercase)
248
272
  base_name = cache_key
249
273
 
250
- # 3. Try package resources with standard search paths
274
+ # 3. Try custom schema paths (from registry + SCHEMA__PATHS env var)
275
+ from ..registry import get_schema_paths
276
+
277
+ custom_paths = get_schema_paths()
278
+ for custom_dir in custom_paths:
279
+ # Try various patterns within each custom directory
280
+ for pattern in [
281
+ f"{base_name}.yaml",
282
+ f"{base_name}.yml",
283
+ f"agents/{base_name}.yaml",
284
+ f"evaluators/{base_name}.yaml",
285
+ ]:
286
+ custom_path = Path(custom_dir) / pattern
287
+ if custom_path.exists():
288
+ logger.debug(f"Loading schema from custom path: {custom_path}")
289
+ with open(custom_path, "r") as f:
290
+ schema = yaml.safe_load(f)
291
+ logger.debug(f"Loaded schema with keys: {list(schema.keys())}")
292
+ # Don't cache custom paths (they may change during development)
293
+ return cast(dict[str, Any], schema)
294
+
295
+ # 4. Try package resources with standard search paths
251
296
  for search_pattern in SCHEMA_SEARCH_PATHS:
252
297
  search_path = search_pattern.format(name=base_name)
253
298
 
@@ -272,7 +317,7 @@ def load_agent_schema(
272
317
  logger.debug(f"Could not load from {search_path}: {e}")
273
318
  continue
274
319
 
275
- # 4. Try database LOOKUP fallback (if enabled and user_id provided)
320
+ # 5. Try database LOOKUP fallback (if enabled and user_id provided)
276
321
  if enable_db_fallback and user_id:
277
322
  try:
278
323
  logger.debug(f"Attempting database LOOKUP for schema: {base_name} (user_id={user_id})")
@@ -284,8 +329,13 @@ def load_agent_schema(
284
329
  logger.debug(f"Database schema lookup failed: {e}")
285
330
  # Fall through to error below
286
331
 
287
- # 5. Schema not found in any location
332
+ # 6. Schema not found in any location
288
333
  searched_paths = [pattern.format(name=base_name) for pattern in SCHEMA_SEARCH_PATHS]
334
+
335
+ custom_paths_note = ""
336
+ if custom_paths:
337
+ custom_paths_note = f"\n - Custom paths: {', '.join(custom_paths)}"
338
+
289
339
  db_search_note = ""
290
340
  if enable_db_fallback:
291
341
  if user_id:
@@ -296,12 +346,118 @@ def load_agent_schema(
296
346
  raise FileNotFoundError(
297
347
  f"Schema not found: {schema_name_or_path}\n"
298
348
  f"Searched locations:\n"
299
- f" - Exact path: {path}\n"
349
+ f" - Exact path: {path}"
350
+ f"{custom_paths_note}\n"
300
351
  f" - Package resources: {', '.join(searched_paths)}"
301
352
  f"{db_search_note}"
302
353
  )
303
354
 
304
355
 
356
+ async def load_agent_schema_async(
357
+ schema_name_or_path: str,
358
+ user_id: str | None = None,
359
+ db=None,
360
+ ) -> dict[str, Any]:
361
+ """
362
+ Async version of load_agent_schema for use in async contexts.
363
+
364
+ Schema names are case-invariant - "MyAgent", "myagent", "MYAGENT" all resolve to the same schema.
365
+
366
+ This version accepts an existing database connection to avoid creating new connections.
367
+
368
+ Args:
369
+ schema_name_or_path: Schema name or file path (case-invariant for names)
370
+ user_id: User ID for database schema lookup
371
+ db: Optional existing PostgresService connection (if None, will create one)
372
+
373
+ Returns:
374
+ Agent schema as dictionary
375
+
376
+ Raises:
377
+ FileNotFoundError: If schema not found
378
+ """
379
+ # First try filesystem search (sync operations are fine)
380
+ path = Path(schema_name_or_path)
381
+
382
+ # Normalize the name for cache key (lowercase for case-invariant lookups)
383
+ cache_key = str(schema_name_or_path).replace('agents/', '').replace('schemas/', '').replace('evaluators/', '').replace('core/', '').replace('examples/', '').lower()
384
+ if cache_key.endswith('.yaml') or cache_key.endswith('.yml'):
385
+ cache_key = cache_key.rsplit('.', 1)[0]
386
+
387
+ is_custom_path = path.exists() or '/' in str(schema_name_or_path) or '\\' in str(schema_name_or_path)
388
+
389
+ # Check cache
390
+ if not is_custom_path and cache_key in _fs_schema_cache:
391
+ logger.debug(f"Loading schema from cache: {cache_key}")
392
+ return _fs_schema_cache[cache_key]
393
+
394
+ # Try exact path
395
+ if path.exists():
396
+ logger.debug(f"Loading schema from exact path: {path}")
397
+ with open(path, "r") as f:
398
+ schema = yaml.safe_load(f)
399
+ return cast(dict[str, Any], schema)
400
+
401
+ base_name = cache_key
402
+
403
+ # Try custom schema paths
404
+ from ..registry import get_schema_paths
405
+ custom_paths = get_schema_paths()
406
+ for custom_dir in custom_paths:
407
+ for pattern in [f"{base_name}.yaml", f"{base_name}.yml", f"agents/{base_name}.yaml"]:
408
+ custom_path = Path(custom_dir) / pattern
409
+ if custom_path.exists():
410
+ with open(custom_path, "r") as f:
411
+ schema = yaml.safe_load(f)
412
+ return cast(dict[str, Any], schema)
413
+
414
+ # Try package resources
415
+ for search_pattern in SCHEMA_SEARCH_PATHS:
416
+ search_path = search_pattern.format(name=base_name)
417
+ try:
418
+ schema_ref = importlib.resources.files("rem") / search_path
419
+ schema_path = Path(str(schema_ref))
420
+ if schema_path.exists():
421
+ with open(schema_path, "r") as f:
422
+ schema = yaml.safe_load(f)
423
+ _fs_schema_cache[cache_key] = schema
424
+ return cast(dict[str, Any], schema)
425
+ except Exception:
426
+ continue
427
+
428
+ # Try database lookup
429
+ if user_id:
430
+ from rem.services.postgres import get_postgres_service
431
+
432
+ should_disconnect = False
433
+ if db is None:
434
+ db = get_postgres_service()
435
+ if db:
436
+ await db.connect()
437
+ should_disconnect = True
438
+
439
+ if db:
440
+ try:
441
+ query = """
442
+ SELECT spec FROM schemas
443
+ WHERE LOWER(name) = LOWER($1)
444
+ AND (user_id = $2 OR user_id = 'system' OR user_id IS NULL)
445
+ LIMIT 1
446
+ """
447
+ row = await db.fetchrow(query, base_name, user_id)
448
+ if row:
449
+ spec = row.get("spec")
450
+ if spec and isinstance(spec, dict):
451
+ logger.info(f"✅ Loaded schema from database: {base_name}")
452
+ return spec
453
+ finally:
454
+ if should_disconnect:
455
+ await db.disconnect()
456
+
457
+ # Not found
458
+ raise FileNotFoundError(f"Schema not found: {schema_name_or_path}")
459
+
460
+
305
461
  def validate_agent_schema(schema: dict[str, Any]) -> bool:
306
462
  """
307
463
  Validate agent schema structure.
rem/utils/sql_types.py CHANGED
@@ -16,6 +16,7 @@ Best Practices:
16
16
  - UUID for identifiers in Union types
17
17
  """
18
18
 
19
+ import types
19
20
  from datetime import date, datetime, time
20
21
  from typing import Any, Union, get_args, get_origin
21
22
  from uuid import UUID
@@ -78,8 +79,9 @@ def get_sql_type(field_info: FieldInfo, field_name: str) -> str:
78
79
  return "TEXT"
79
80
 
80
81
  # Handle Union types (including Optional[T] which is Union[T, None])
82
+ # Also handles Python 3.10+ `X | None` syntax which uses types.UnionType
81
83
  origin = get_origin(annotation)
82
- if origin is Union:
84
+ if origin is Union or isinstance(annotation, types.UnionType):
83
85
  args = get_args(annotation)
84
86
  # Filter out NoneType
85
87
  non_none_args = [arg for arg in args if arg is not type(None)]
rem/utils/vision.py CHANGED
@@ -11,7 +11,6 @@ markdown descriptions of images.
11
11
  """
12
12
 
13
13
  import base64
14
- import os
15
14
  from enum import Enum
16
15
  from pathlib import Path
17
16
  from typing import Optional
@@ -19,6 +18,9 @@ from typing import Optional
19
18
  import requests
20
19
  from loguru import logger
21
20
 
21
+ from rem.utils.constants import HTTP_TIMEOUT_LONG, VISION_MAX_TOKENS
22
+ from rem.utils.mime_types import EXTENSION_TO_MIME
23
+
22
24
 
23
25
  class VisionProvider(str, Enum):
24
26
  """Supported vision providers."""
@@ -141,14 +143,7 @@ class ImageAnalyzer:
141
143
 
142
144
  # Detect media type
143
145
  suffix = image_path.suffix.lower()
144
- media_type_map = {
145
- ".png": "image/png",
146
- ".jpg": "image/jpeg",
147
- ".jpeg": "image/jpeg",
148
- ".gif": "image/gif",
149
- ".webp": "image/webp",
150
- }
151
- media_type = media_type_map.get(suffix, "image/png")
146
+ media_type = EXTENSION_TO_MIME.get(suffix, "image/png")
152
147
 
153
148
  logger.info(f"Analyzing {image_path.name} with {self.provider.value} ({self.model})")
154
149
 
@@ -190,7 +185,7 @@ class ImageAnalyzer:
190
185
 
191
186
  body = {
192
187
  "model": self.model,
193
- "max_tokens": 2048,
188
+ "max_tokens": VISION_MAX_TOKENS,
194
189
  "messages": [
195
190
  {
196
191
  "role": "user",
@@ -216,7 +211,7 @@ class ImageAnalyzer:
216
211
  "https://api.anthropic.com/v1/messages",
217
212
  headers=headers,
218
213
  json=body,
219
- timeout=60.0,
214
+ timeout=HTTP_TIMEOUT_LONG,
220
215
  )
221
216
 
222
217
  if response.status_code != 200:
@@ -261,7 +256,7 @@ class ImageAnalyzer:
261
256
  url,
262
257
  params=params,
263
258
  json=body,
264
- timeout=60.0,
259
+ timeout=HTTP_TIMEOUT_LONG,
265
260
  )
266
261
 
267
262
  if response.status_code != 200:
@@ -311,14 +306,14 @@ class ImageAnalyzer:
311
306
  ],
312
307
  }
313
308
  ],
314
- "max_tokens": 2048,
309
+ "max_tokens": VISION_MAX_TOKENS,
315
310
  }
316
311
 
317
312
  response = requests.post(
318
313
  url,
319
314
  headers=headers,
320
315
  json=body,
321
- timeout=60.0,
316
+ timeout=HTTP_TIMEOUT_LONG,
322
317
  )
323
318
 
324
319
  if response.status_code != 200:
rem/workers/README.md CHANGED
@@ -207,7 +207,7 @@ Reads recent activity to generate comprehensive user profiles.
207
207
 
208
208
  **CLI:**
209
209
  ```bash
210
- rem-dreaming user-model --tenant-id=tenant-123
210
+ rem-dreaming user-model
211
211
  ```
212
212
 
213
213
  **Frequency:** Daily (runs as part of full workflow)
@@ -235,13 +235,13 @@ Extracts temporal narratives from resources.
235
235
  **CLI:**
236
236
  ```bash
237
237
  # Process last 24 hours
238
- rem-dreaming moments --tenant-id=tenant-123
238
+ rem-dreaming moments
239
239
 
240
240
  # Custom lookback
241
- rem-dreaming moments --tenant-id=tenant-123 --lookback-hours=48
241
+ rem-dreaming moments --lookback-hours=48
242
242
 
243
243
  # Limit resources processed
244
- rem-dreaming moments --tenant-id=tenant-123 --limit=100
244
+ rem-dreaming moments --limit=100
245
245
  ```
246
246
 
247
247
  **Frequency:** Daily or on-demand
@@ -283,13 +283,13 @@ Builds semantic relationships between resources.
283
283
  **CLI:**
284
284
  ```bash
285
285
  # Semantic mode (fast, cheap)
286
- rem-dreaming affinity --tenant-id=tenant-123
286
+ rem-dreaming affinity
287
287
 
288
288
  # LLM mode (intelligent, expensive)
289
- rem-dreaming affinity --tenant-id=tenant-123 --use-llm --limit=100
289
+ rem-dreaming affinity --use-llm --limit=100
290
290
 
291
291
  # Custom lookback
292
- rem-dreaming affinity --tenant-id=tenant-123 --lookback-hours=168
292
+ rem-dreaming affinity --lookback-hours=168
293
293
  ```
294
294
 
295
295
  **Frequency:**
@@ -308,13 +308,13 @@ Runs all operations in sequence.
308
308
  **CLI:**
309
309
  ```bash
310
310
  # Single tenant
311
- rem-dreaming full --tenant-id=tenant-123
311
+ rem-dreaming full
312
312
 
313
313
  # All active tenants (daily cron)
314
314
  rem-dreaming full --all-tenants
315
315
 
316
316
  # Use LLM affinity mode
317
- rem-dreaming full --tenant-id=tenant-123 --use-llm-affinity
317
+ rem-dreaming full --use-llm-affinity
318
318
  ```
319
319
 
320
320
  **Frequency:** Daily at 3 AM UTC
@@ -455,16 +455,16 @@ export REM_API_URL=http://localhost:8000
455
455
  export OPENAI_API_KEY=sk-...
456
456
 
457
457
  # Run user model update
458
- python -m rem.cli.dreaming user-model --tenant-id=tenant-test
458
+ python -m rem.cli.dreaming user-model
459
459
 
460
460
  # Run moment construction
461
- python -m rem.cli.dreaming moments --tenant-id=tenant-test --lookback-hours=24
461
+ python -m rem.cli.dreaming moments --lookback-hours=24
462
462
 
463
463
  # Run affinity (semantic mode)
464
- python -m rem.cli.dreaming affinity --tenant-id=tenant-test
464
+ python -m rem.cli.dreaming affinity
465
465
 
466
466
  # Run full workflow
467
- python -m rem.cli.dreaming full --tenant-id=tenant-test
467
+ python -m rem.cli.dreaming full
468
468
  ```
469
469
 
470
470
  ### Testing with Docker
@@ -478,7 +478,7 @@ docker run --rm \
478
478
  -e REM_API_URL=http://host.docker.internal:8000 \
479
479
  -e OPENAI_API_KEY=$OPENAI_API_KEY \
480
480
  rem-stack:latest \
481
- python -m rem.cli.dreaming full --tenant-id=tenant-test
481
+ python -m rem.cli.dreaming full
482
482
  ```
483
483
 
484
484
  ## Architecture Decisions
@@ -0,0 +1,74 @@
1
+ """
2
+ Database Maintainer Worker.
3
+
4
+ Handles background maintenance tasks for PostgreSQL:
5
+ 1. Cleaning up expired rate limit counters (UNLOGGED table).
6
+ 2. Refreshing materialized views (if any).
7
+ 3. Vacuuming specific tables (if needed).
8
+
9
+ Usage:
10
+ python -m rem.workers.db_maintainer
11
+
12
+ # Or via docker-compose:
13
+ # command: python -m rem.workers.db_maintainer
14
+ """
15
+
16
+ import asyncio
17
+ import signal
18
+ from loguru import logger
19
+
20
+ from ..services.postgres.service import PostgresService
21
+ from ..services.rate_limit import RateLimitService
22
+
23
+ class DatabaseMaintainer:
24
+ def __init__(self):
25
+ self.running = False
26
+ self.db = PostgresService()
27
+ self.rate_limiter = RateLimitService(self.db)
28
+
29
+ async def start(self):
30
+ """Start maintenance loop."""
31
+ self.running = True
32
+ logger.info("Starting Database Maintainer Worker")
33
+
34
+ await self.db.connect()
35
+
36
+ try:
37
+ while self.running:
38
+ await self._run_maintenance_cycle()
39
+ # Sleep for 5 minutes
40
+ await asyncio.sleep(300)
41
+ finally:
42
+ await self.db.disconnect()
43
+
44
+ async def _run_maintenance_cycle(self):
45
+ """Execute maintenance tasks."""
46
+ logger.debug("Running maintenance cycle...")
47
+
48
+ try:
49
+ # 1. Cleanup Rate Limits
50
+ await self.rate_limiter.cleanup_expired()
51
+
52
+ # 2. (Future) Refresh Views
53
+ # await self.db.execute("REFRESH MATERIALIZED VIEW ...")
54
+
55
+ except Exception as e:
56
+ logger.error(f"Maintenance cycle failed: {e}")
57
+
58
+ def stop(self):
59
+ """Stop worker gracefully."""
60
+ self.running = False
61
+ logger.info("Stopping Database Maintainer Worker...")
62
+
63
+ async def main():
64
+ worker = DatabaseMaintainer()
65
+
66
+ # Handle signals
67
+ loop = asyncio.get_running_loop()
68
+ for sig in (signal.SIGTERM, signal.SIGINT):
69
+ loop.add_signal_handler(sig, worker.stop)
70
+
71
+ await worker.start()
72
+
73
+ if __name__ == "__main__":
74
+ asyncio.run(main())