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.
- 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/otel/setup.py +92 -4
- rem/agentic/providers/phoenix.py +32 -43
- rem/agentic/providers/pydantic_ai.py +142 -22
- 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 +151 -37
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +17 -2
- rem/api/mcp_router/tools.py +143 -7
- 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 +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 +357 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/README.md +201 -70
- rem/cli/commands/ask.py +13 -10
- rem/cli/commands/cluster.py +1359 -0
- rem/cli/commands/configure.py +4 -3
- rem/cli/commands/db.py +350 -137
- rem/cli/commands/experiments.py +76 -72
- rem/cli/commands/process.py +22 -15
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +95 -49
- rem/cli/main.py +29 -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 +92 -20
- 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 +426 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +86 -5
- 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 -17
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +231 -54
- rem/sql/migrations/002_install_models.sql +457 -393
- rem/sql/migrations/003_optional_extensions.sql +326 -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 +191 -35
- 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.3.0.dist-info → remdb-0.3.114.dist-info}/METADATA +303 -164
- {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/RECORD +96 -70
- {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1038
- {remdb-0.3.0.dist-info → remdb-0.3.114.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)
|
|
@@ -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
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
query=query,
|
|
144
|
-
user_id=user_id,
|
|
145
|
-
)
|
|
168
|
+
row = await db.fetchrow(query, schema_name, user_id)
|
|
146
169
|
|
|
147
|
-
if
|
|
148
|
-
|
|
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.
|
|
192
|
-
4. Package resources: schemas/agents/
|
|
193
|
-
5. Package resources: schemas/agents/
|
|
194
|
-
6. Package resources: schemas/
|
|
195
|
-
7. Package resources: schemas/{name}.yaml
|
|
196
|
-
8.
|
|
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", "
|
|
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("
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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}
|
|
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
|
-
|
|
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())
|