remdb 0.2.6__py3-none-any.whl → 0.3.103__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 +7 -5
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/providers/phoenix.py +32 -43
- rem/agentic/providers/pydantic_ai.py +84 -10
- rem/api/README.md +238 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +70 -22
- rem/api/mcp_router/server.py +8 -1
- rem/api/mcp_router/tools.py +80 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +277 -0
- rem/api/routers/auth.py +124 -0
- rem/api/routers/chat/completions.py +123 -14
- rem/api/routers/chat/models.py +7 -3
- rem/api/routers/chat/sse_events.py +526 -0
- rem/api/routers/chat/streaming.py +468 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +455 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/ask.py +15 -11
- rem/cli/commands/configure.py +169 -94
- rem/cli/commands/db.py +53 -7
- rem/cli/commands/experiments.py +278 -96
- rem/cli/commands/process.py +8 -7
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +9 -9
- rem/cli/main.py +10 -0
- rem/config.py +2 -2
- rem/models/core/core_model.py +7 -1
- 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 +206 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +367 -0
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/providers.py +94 -140
- rem/services/content/service.py +85 -16
- rem/services/dreaming/affinity_service.py +2 -16
- rem/services/dreaming/moment_service.py +2 -15
- rem/services/embeddings/api.py +20 -13
- rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
- rem/services/phoenix/client.py +252 -19
- rem/services/postgres/README.md +29 -10
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +86 -5
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/session/compression.py +17 -1
- rem/services/user_service.py +98 -0
- rem/settings.py +115 -17
- rem/sql/background_indexes.sql +10 -0
- rem/sql/migrations/001_install.sql +152 -2
- rem/sql/migrations/002_install_models.sql +580 -231
- rem/sql/migrations/003_seed_default_user.sql +48 -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 +273 -14
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/db_maintainer.py +74 -0
- {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/METADATA +486 -132
- {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/RECORD +80 -57
- {remdb-0.2.6.dist-info → remdb-0.3.103.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.103.dist-info}/entry_points.txt +0 -0
rem/utils/schema_loader.py
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
@@ -89,7 +110,88 @@ _fs_schema_cache: dict[str, dict[str, Any]] = {}
|
|
|
89
110
|
# _db_schema_ttl: int = 300 # 5 minutes in seconds
|
|
90
111
|
|
|
91
112
|
|
|
92
|
-
def
|
|
113
|
+
def _load_schema_from_database(schema_name: str, user_id: str) -> dict[str, Any] | None:
|
|
114
|
+
"""
|
|
115
|
+
Load schema from database using LOOKUP query.
|
|
116
|
+
|
|
117
|
+
This function is synchronous but calls async database operations.
|
|
118
|
+
It's designed to be called from load_agent_schema() which is sync.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
schema_name: Schema name to lookup
|
|
122
|
+
user_id: User ID for data scoping
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Schema spec (dict) if found, None otherwise
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
RuntimeError: If database connection fails
|
|
129
|
+
"""
|
|
130
|
+
import asyncio
|
|
131
|
+
|
|
132
|
+
# Check if we're already in an async context
|
|
133
|
+
try:
|
|
134
|
+
loop = asyncio.get_running_loop()
|
|
135
|
+
# We're in an async context - can't use asyncio.run()
|
|
136
|
+
# This shouldn't happen in normal usage since load_agent_schema is called from sync contexts
|
|
137
|
+
logger.warning(
|
|
138
|
+
"Database schema lookup called from async context. "
|
|
139
|
+
"This may cause issues. Consider using async version of load_agent_schema."
|
|
140
|
+
)
|
|
141
|
+
return None
|
|
142
|
+
except RuntimeError:
|
|
143
|
+
# Not in async context - safe to use asyncio.run()
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
async def _async_lookup():
|
|
147
|
+
"""Async helper to query database."""
|
|
148
|
+
from rem.services.postgres import get_postgres_service
|
|
149
|
+
|
|
150
|
+
db = get_postgres_service()
|
|
151
|
+
if not db:
|
|
152
|
+
logger.debug("PostgreSQL service not available for schema lookup")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
await db.connect()
|
|
157
|
+
|
|
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}")
|
|
167
|
+
|
|
168
|
+
row = await db.fetchrow(query, schema_name, user_id)
|
|
169
|
+
|
|
170
|
+
if row:
|
|
171
|
+
spec = row.get("spec")
|
|
172
|
+
if spec and isinstance(spec, dict):
|
|
173
|
+
logger.debug(f"Found schema in database: {schema_name}")
|
|
174
|
+
return spec
|
|
175
|
+
|
|
176
|
+
logger.debug(f"Schema not found in database: {schema_name}")
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.debug(f"Database schema lookup error: {e}")
|
|
181
|
+
return None
|
|
182
|
+
finally:
|
|
183
|
+
await db.disconnect()
|
|
184
|
+
|
|
185
|
+
# Run async lookup in new event loop
|
|
186
|
+
return asyncio.run(_async_lookup())
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def load_agent_schema(
|
|
190
|
+
schema_name_or_path: str,
|
|
191
|
+
use_cache: bool = True,
|
|
192
|
+
user_id: str | None = None,
|
|
193
|
+
enable_db_fallback: bool = True,
|
|
194
|
+
) -> dict[str, Any]:
|
|
93
195
|
"""
|
|
94
196
|
Load agent schema from YAML file with unified search logic and caching.
|
|
95
197
|
|
|
@@ -107,22 +209,26 @@ def load_agent_schema(schema_name_or_path: str, use_cache: bool = True) -> dict[
|
|
|
107
209
|
Search Order:
|
|
108
210
|
1. Check cache (if use_cache=True and schema found in FS cache)
|
|
109
211
|
2. Exact path if it exists (absolute or relative)
|
|
110
|
-
3.
|
|
111
|
-
4. Package resources: schemas/agents/
|
|
112
|
-
5. Package resources: schemas/agents/
|
|
113
|
-
6. Package resources: schemas/
|
|
114
|
-
7. Package resources: schemas/{name}.yaml
|
|
212
|
+
3. Custom paths from rem.register_schema_path() and SCHEMA__PATHS env var
|
|
213
|
+
4. Package resources: schemas/agents/{name}.yaml (top-level)
|
|
214
|
+
5. Package resources: schemas/agents/core/{name}.yaml
|
|
215
|
+
6. Package resources: schemas/agents/examples/{name}.yaml
|
|
216
|
+
7. Package resources: schemas/evaluators/{name}.yaml
|
|
217
|
+
8. Package resources: schemas/{name}.yaml
|
|
218
|
+
9. Database LOOKUP: schemas table (if enable_db_fallback=True and user_id provided)
|
|
115
219
|
|
|
116
220
|
Args:
|
|
117
221
|
schema_name_or_path: Schema name or file path
|
|
118
222
|
Examples: "rem-query-agent", "contract-analyzer", "./my-schema.yaml"
|
|
119
223
|
use_cache: If True, uses in-memory cache for filesystem schemas
|
|
224
|
+
user_id: User ID for database schema lookup (required for DB fallback)
|
|
225
|
+
enable_db_fallback: If True, falls back to database LOOKUP when file not found
|
|
120
226
|
|
|
121
227
|
Returns:
|
|
122
228
|
Agent schema as dictionary
|
|
123
229
|
|
|
124
230
|
Raises:
|
|
125
|
-
FileNotFoundError: If schema not found in any search location
|
|
231
|
+
FileNotFoundError: If schema not found in any search location (filesystem + database)
|
|
126
232
|
yaml.YAMLError: If schema file is invalid YAML
|
|
127
233
|
|
|
128
234
|
Examples:
|
|
@@ -134,6 +240,9 @@ def load_agent_schema(schema_name_or_path: str, use_cache: bool = True) -> dict[
|
|
|
134
240
|
>>>
|
|
135
241
|
>>> # Load evaluator schema (cached)
|
|
136
242
|
>>> schema = load_agent_schema("rem-lookup-correctness")
|
|
243
|
+
>>>
|
|
244
|
+
>>> # Load custom user schema from database
|
|
245
|
+
>>> schema = load_agent_schema("my-custom-agent", user_id="user-123")
|
|
137
246
|
"""
|
|
138
247
|
# Normalize the name for cache key
|
|
139
248
|
cache_key = str(schema_name_or_path).replace('agents/', '').replace('schemas/', '').replace('evaluators/', '').replace('core/', '').replace('examples/', '')
|
|
@@ -160,7 +269,28 @@ def load_agent_schema(schema_name_or_path: str, use_cache: bool = True) -> dict[
|
|
|
160
269
|
# 2. Normalize name for package resource search
|
|
161
270
|
base_name = cache_key
|
|
162
271
|
|
|
163
|
-
# 3. Try
|
|
272
|
+
# 3. Try custom schema paths (from registry + SCHEMA__PATHS env var)
|
|
273
|
+
from ..registry import get_schema_paths
|
|
274
|
+
|
|
275
|
+
custom_paths = get_schema_paths()
|
|
276
|
+
for custom_dir in custom_paths:
|
|
277
|
+
# Try various patterns within each custom directory
|
|
278
|
+
for pattern in [
|
|
279
|
+
f"{base_name}.yaml",
|
|
280
|
+
f"{base_name}.yml",
|
|
281
|
+
f"agents/{base_name}.yaml",
|
|
282
|
+
f"evaluators/{base_name}.yaml",
|
|
283
|
+
]:
|
|
284
|
+
custom_path = Path(custom_dir) / pattern
|
|
285
|
+
if custom_path.exists():
|
|
286
|
+
logger.debug(f"Loading schema from custom path: {custom_path}")
|
|
287
|
+
with open(custom_path, "r") as f:
|
|
288
|
+
schema = yaml.safe_load(f)
|
|
289
|
+
logger.debug(f"Loaded schema with keys: {list(schema.keys())}")
|
|
290
|
+
# Don't cache custom paths (they may change during development)
|
|
291
|
+
return cast(dict[str, Any], schema)
|
|
292
|
+
|
|
293
|
+
# 4. Try package resources with standard search paths
|
|
164
294
|
for search_pattern in SCHEMA_SEARCH_PATHS:
|
|
165
295
|
search_path = search_pattern.format(name=base_name)
|
|
166
296
|
|
|
@@ -185,16 +315,145 @@ def load_agent_schema(schema_name_or_path: str, use_cache: bool = True) -> dict[
|
|
|
185
315
|
logger.debug(f"Could not load from {search_path}: {e}")
|
|
186
316
|
continue
|
|
187
317
|
|
|
188
|
-
#
|
|
318
|
+
# 5. Try database LOOKUP fallback (if enabled and user_id provided)
|
|
319
|
+
if enable_db_fallback and user_id:
|
|
320
|
+
try:
|
|
321
|
+
logger.debug(f"Attempting database LOOKUP for schema: {base_name} (user_id={user_id})")
|
|
322
|
+
db_schema = _load_schema_from_database(base_name, user_id)
|
|
323
|
+
if db_schema:
|
|
324
|
+
logger.info(f"✅ Loaded schema from database: {base_name} (user_id={user_id})")
|
|
325
|
+
return db_schema
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.debug(f"Database schema lookup failed: {e}")
|
|
328
|
+
# Fall through to error below
|
|
329
|
+
|
|
330
|
+
# 6. Schema not found in any location
|
|
189
331
|
searched_paths = [pattern.format(name=base_name) for pattern in SCHEMA_SEARCH_PATHS]
|
|
332
|
+
|
|
333
|
+
custom_paths_note = ""
|
|
334
|
+
if custom_paths:
|
|
335
|
+
custom_paths_note = f"\n - Custom paths: {', '.join(custom_paths)}"
|
|
336
|
+
|
|
337
|
+
db_search_note = ""
|
|
338
|
+
if enable_db_fallback:
|
|
339
|
+
if user_id:
|
|
340
|
+
db_search_note = f"\n - Database: LOOKUP '{base_name}' FROM schemas WHERE user_id='{user_id}' (no match)"
|
|
341
|
+
else:
|
|
342
|
+
db_search_note = "\n - Database: (skipped - no user_id provided)"
|
|
343
|
+
|
|
190
344
|
raise FileNotFoundError(
|
|
191
345
|
f"Schema not found: {schema_name_or_path}\n"
|
|
192
346
|
f"Searched locations:\n"
|
|
193
|
-
f" - Exact path: {path}
|
|
347
|
+
f" - Exact path: {path}"
|
|
348
|
+
f"{custom_paths_note}\n"
|
|
194
349
|
f" - Package resources: {', '.join(searched_paths)}"
|
|
350
|
+
f"{db_search_note}"
|
|
195
351
|
)
|
|
196
352
|
|
|
197
353
|
|
|
354
|
+
async def load_agent_schema_async(
|
|
355
|
+
schema_name_or_path: str,
|
|
356
|
+
user_id: str | None = None,
|
|
357
|
+
db=None,
|
|
358
|
+
) -> dict[str, Any]:
|
|
359
|
+
"""
|
|
360
|
+
Async version of load_agent_schema for use in async contexts.
|
|
361
|
+
|
|
362
|
+
This version accepts an existing database connection to avoid creating new connections.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
schema_name_or_path: Schema name or file path
|
|
366
|
+
user_id: User ID for database schema lookup
|
|
367
|
+
db: Optional existing PostgresService connection (if None, will create one)
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Agent schema as dictionary
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
FileNotFoundError: If schema not found
|
|
374
|
+
"""
|
|
375
|
+
# First try filesystem search (sync operations are fine)
|
|
376
|
+
path = Path(schema_name_or_path)
|
|
377
|
+
|
|
378
|
+
# Normalize the name for cache key
|
|
379
|
+
cache_key = str(schema_name_or_path).replace('agents/', '').replace('schemas/', '').replace('evaluators/', '').replace('core/', '').replace('examples/', '')
|
|
380
|
+
if cache_key.endswith('.yaml') or cache_key.endswith('.yml'):
|
|
381
|
+
cache_key = cache_key.rsplit('.', 1)[0]
|
|
382
|
+
|
|
383
|
+
is_custom_path = path.exists() or '/' in str(schema_name_or_path) or '\\' in str(schema_name_or_path)
|
|
384
|
+
|
|
385
|
+
# Check cache
|
|
386
|
+
if not is_custom_path and cache_key in _fs_schema_cache:
|
|
387
|
+
logger.debug(f"Loading schema from cache: {cache_key}")
|
|
388
|
+
return _fs_schema_cache[cache_key]
|
|
389
|
+
|
|
390
|
+
# Try exact path
|
|
391
|
+
if path.exists():
|
|
392
|
+
logger.debug(f"Loading schema from exact path: {path}")
|
|
393
|
+
with open(path, "r") as f:
|
|
394
|
+
schema = yaml.safe_load(f)
|
|
395
|
+
return cast(dict[str, Any], schema)
|
|
396
|
+
|
|
397
|
+
base_name = cache_key
|
|
398
|
+
|
|
399
|
+
# Try custom schema paths
|
|
400
|
+
from ..registry import get_schema_paths
|
|
401
|
+
custom_paths = get_schema_paths()
|
|
402
|
+
for custom_dir in custom_paths:
|
|
403
|
+
for pattern in [f"{base_name}.yaml", f"{base_name}.yml", f"agents/{base_name}.yaml"]:
|
|
404
|
+
custom_path = Path(custom_dir) / pattern
|
|
405
|
+
if custom_path.exists():
|
|
406
|
+
with open(custom_path, "r") as f:
|
|
407
|
+
schema = yaml.safe_load(f)
|
|
408
|
+
return cast(dict[str, Any], schema)
|
|
409
|
+
|
|
410
|
+
# Try package resources
|
|
411
|
+
for search_pattern in SCHEMA_SEARCH_PATHS:
|
|
412
|
+
search_path = search_pattern.format(name=base_name)
|
|
413
|
+
try:
|
|
414
|
+
schema_ref = importlib.resources.files("rem") / search_path
|
|
415
|
+
schema_path = Path(str(schema_ref))
|
|
416
|
+
if schema_path.exists():
|
|
417
|
+
with open(schema_path, "r") as f:
|
|
418
|
+
schema = yaml.safe_load(f)
|
|
419
|
+
_fs_schema_cache[cache_key] = schema
|
|
420
|
+
return cast(dict[str, Any], schema)
|
|
421
|
+
except Exception:
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
# Try database lookup
|
|
425
|
+
if user_id:
|
|
426
|
+
from rem.services.postgres import get_postgres_service
|
|
427
|
+
|
|
428
|
+
should_disconnect = False
|
|
429
|
+
if db is None:
|
|
430
|
+
db = get_postgres_service()
|
|
431
|
+
if db:
|
|
432
|
+
await db.connect()
|
|
433
|
+
should_disconnect = True
|
|
434
|
+
|
|
435
|
+
if db:
|
|
436
|
+
try:
|
|
437
|
+
query = """
|
|
438
|
+
SELECT spec FROM schemas
|
|
439
|
+
WHERE LOWER(name) = LOWER($1)
|
|
440
|
+
AND (user_id = $2 OR user_id = 'system')
|
|
441
|
+
LIMIT 1
|
|
442
|
+
"""
|
|
443
|
+
row = await db.fetchrow(query, base_name, user_id)
|
|
444
|
+
if row:
|
|
445
|
+
spec = row.get("spec")
|
|
446
|
+
if spec and isinstance(spec, dict):
|
|
447
|
+
logger.info(f"✅ Loaded schema from database: {base_name}")
|
|
448
|
+
return spec
|
|
449
|
+
finally:
|
|
450
|
+
if should_disconnect:
|
|
451
|
+
await db.disconnect()
|
|
452
|
+
|
|
453
|
+
# Not found
|
|
454
|
+
raise FileNotFoundError(f"Schema not found: {schema_name_or_path}")
|
|
455
|
+
|
|
456
|
+
|
|
198
457
|
def validate_agent_schema(schema: dict[str, Any]) -> bool:
|
|
199
458
|
"""
|
|
200
459
|
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
|
-
|
|
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":
|
|
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=
|
|
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=
|
|
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":
|
|
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=
|
|
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
|
|
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
|
|
238
|
+
rem-dreaming moments
|
|
239
239
|
|
|
240
240
|
# Custom lookback
|
|
241
|
-
rem-dreaming moments
|
|
241
|
+
rem-dreaming moments --lookback-hours=48
|
|
242
242
|
|
|
243
243
|
# Limit resources processed
|
|
244
|
-
rem-dreaming moments
|
|
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
|
|
286
|
+
rem-dreaming affinity
|
|
287
287
|
|
|
288
288
|
# LLM mode (intelligent, expensive)
|
|
289
|
-
rem-dreaming affinity
|
|
289
|
+
rem-dreaming affinity --use-llm --limit=100
|
|
290
290
|
|
|
291
291
|
# Custom lookback
|
|
292
|
-
rem-dreaming affinity
|
|
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
|
|
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
|
|
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
|
|
458
|
+
python -m rem.cli.dreaming user-model
|
|
459
459
|
|
|
460
460
|
# Run moment construction
|
|
461
|
-
python -m rem.cli.dreaming moments
|
|
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
|
|
464
|
+
python -m rem.cli.dreaming affinity
|
|
465
465
|
|
|
466
466
|
# Run full workflow
|
|
467
|
-
python -m rem.cli.dreaming full
|
|
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
|
|
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())
|