remdb 0.3.7__py3-none-any.whl → 0.3.14__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.
- rem/__init__.py +129 -2
- rem/agentic/context.py +7 -5
- rem/agentic/providers/phoenix.py +32 -43
- rem/api/README.md +23 -0
- rem/api/main.py +27 -2
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/auth.py +54 -0
- rem/api/routers/chat/completions.py +1 -1
- rem/cli/commands/ask.py +13 -10
- rem/cli/commands/configure.py +4 -3
- rem/cli/commands/db.py +17 -3
- rem/cli/commands/experiments.py +76 -72
- rem/cli/commands/process.py +8 -7
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/main.py +2 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +367 -0
- rem/services/content/providers.py +92 -133
- 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 +148 -14
- rem/services/postgres/schema_generator.py +86 -5
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/user_service.py +98 -0
- rem/settings.py +79 -10
- rem/sql/install_models.sql +13 -0
- 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/schema_loader.py +63 -14
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/db_maintainer.py +74 -0
- {remdb-0.3.7.dist-info → remdb-0.3.14.dist-info}/METADATA +169 -121
- {remdb-0.3.7.dist-info → remdb-0.3.14.dist-info}/RECORD +43 -32
- {remdb-0.3.7.dist-info → remdb-0.3.14.dist-info}/WHEEL +0 -0
- {remdb-0.3.7.dist-info → remdb-0.3.14.dist-info}/entry_points.txt +0 -0
rem/utils/files.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File utilities for consistent file handling throughout REM.
|
|
3
|
+
|
|
4
|
+
Provides context managers and helpers for temporary file operations,
|
|
5
|
+
ensuring proper cleanup and consistent patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import tempfile
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Generator, Optional
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def temp_file_from_bytes(
|
|
18
|
+
content: bytes,
|
|
19
|
+
suffix: str = "",
|
|
20
|
+
prefix: str = "rem_",
|
|
21
|
+
dir: Optional[str] = None,
|
|
22
|
+
) -> Generator[Path, None, None]:
|
|
23
|
+
"""
|
|
24
|
+
Create a temporary file from bytes, yield path, cleanup automatically.
|
|
25
|
+
|
|
26
|
+
This context manager ensures proper cleanup of temporary files even
|
|
27
|
+
if an exception occurs during processing.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
content: Bytes to write to the temporary file
|
|
31
|
+
suffix: File extension (e.g., ".pdf", ".wav")
|
|
32
|
+
prefix: Prefix for the temp file name
|
|
33
|
+
dir: Directory for temp file (uses system temp if None)
|
|
34
|
+
|
|
35
|
+
Yields:
|
|
36
|
+
Path to the temporary file
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> with temp_file_from_bytes(pdf_bytes, suffix=".pdf") as tmp_path:
|
|
40
|
+
... result = process_pdf(tmp_path)
|
|
41
|
+
# File is automatically cleaned up after the block
|
|
42
|
+
|
|
43
|
+
Note:
|
|
44
|
+
The file is created with delete=False so we control cleanup.
|
|
45
|
+
This allows the file to be read by external processes.
|
|
46
|
+
"""
|
|
47
|
+
tmp_path: Optional[Path] = None
|
|
48
|
+
try:
|
|
49
|
+
with tempfile.NamedTemporaryFile(
|
|
50
|
+
suffix=suffix,
|
|
51
|
+
prefix=prefix,
|
|
52
|
+
dir=dir,
|
|
53
|
+
delete=False,
|
|
54
|
+
) as tmp:
|
|
55
|
+
tmp.write(content)
|
|
56
|
+
tmp_path = Path(tmp.name)
|
|
57
|
+
|
|
58
|
+
yield tmp_path
|
|
59
|
+
|
|
60
|
+
finally:
|
|
61
|
+
if tmp_path is not None:
|
|
62
|
+
try:
|
|
63
|
+
tmp_path.unlink(missing_ok=True)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.warning(f"Failed to cleanup temp file {tmp_path}: {e}")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@contextmanager
|
|
69
|
+
def temp_file_empty(
|
|
70
|
+
suffix: str = "",
|
|
71
|
+
prefix: str = "rem_",
|
|
72
|
+
dir: Optional[str] = None,
|
|
73
|
+
) -> Generator[Path, None, None]:
|
|
74
|
+
"""
|
|
75
|
+
Create an empty temporary file, yield path, cleanup automatically.
|
|
76
|
+
|
|
77
|
+
Useful when you need to write to a file after creation or when
|
|
78
|
+
an external process will write to the file.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
suffix: File extension
|
|
82
|
+
prefix: Prefix for the temp file name
|
|
83
|
+
dir: Directory for temp file
|
|
84
|
+
|
|
85
|
+
Yields:
|
|
86
|
+
Path to the empty temporary file
|
|
87
|
+
"""
|
|
88
|
+
tmp_path: Optional[Path] = None
|
|
89
|
+
try:
|
|
90
|
+
with tempfile.NamedTemporaryFile(
|
|
91
|
+
suffix=suffix,
|
|
92
|
+
prefix=prefix,
|
|
93
|
+
dir=dir,
|
|
94
|
+
delete=False,
|
|
95
|
+
) as tmp:
|
|
96
|
+
tmp_path = Path(tmp.name)
|
|
97
|
+
|
|
98
|
+
yield tmp_path
|
|
99
|
+
|
|
100
|
+
finally:
|
|
101
|
+
if tmp_path is not None:
|
|
102
|
+
try:
|
|
103
|
+
tmp_path.unlink(missing_ok=True)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.warning(f"Failed to cleanup temp file {tmp_path}: {e}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@contextmanager
|
|
109
|
+
def temp_directory(
|
|
110
|
+
prefix: str = "rem_",
|
|
111
|
+
dir: Optional[str] = None,
|
|
112
|
+
) -> Generator[Path, None, None]:
|
|
113
|
+
"""
|
|
114
|
+
Create a temporary directory, yield path, cleanup automatically.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
prefix: Prefix for the temp directory name
|
|
118
|
+
dir: Parent directory for temp directory
|
|
119
|
+
|
|
120
|
+
Yields:
|
|
121
|
+
Path to the temporary directory
|
|
122
|
+
"""
|
|
123
|
+
import shutil
|
|
124
|
+
|
|
125
|
+
tmp_dir: Optional[Path] = None
|
|
126
|
+
try:
|
|
127
|
+
tmp_dir = Path(tempfile.mkdtemp(prefix=prefix, dir=dir))
|
|
128
|
+
yield tmp_dir
|
|
129
|
+
|
|
130
|
+
finally:
|
|
131
|
+
if tmp_dir is not None:
|
|
132
|
+
try:
|
|
133
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.warning(f"Failed to cleanup temp directory {tmp_dir}: {e}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def ensure_parent_exists(path: Path) -> Path:
|
|
139
|
+
"""
|
|
140
|
+
Ensure parent directory exists, creating if necessary.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
path: File path whose parent should exist
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The original path (for chaining)
|
|
147
|
+
"""
|
|
148
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
return path
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def safe_delete(path: Path) -> bool:
|
|
153
|
+
"""
|
|
154
|
+
Safely delete a file, returning success status.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: Path to delete
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
True if deleted or didn't exist, False on error
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
path.unlink(missing_ok=True)
|
|
164
|
+
return True
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.warning(f"Failed to delete {path}: {e}")
|
|
167
|
+
return False
|
rem/utils/mime_types.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized MIME type mappings for file format detection.
|
|
3
|
+
|
|
4
|
+
Provides bidirectional mappings between file extensions and MIME types.
|
|
5
|
+
Use these constants throughout the codebase instead of inline dictionaries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Extension to MIME type mapping (extension includes leading dot)
|
|
9
|
+
EXTENSION_TO_MIME: dict[str, str] = {
|
|
10
|
+
# Images
|
|
11
|
+
".png": "image/png",
|
|
12
|
+
".jpg": "image/jpeg",
|
|
13
|
+
".jpeg": "image/jpeg",
|
|
14
|
+
".gif": "image/gif",
|
|
15
|
+
".webp": "image/webp",
|
|
16
|
+
".bmp": "image/bmp",
|
|
17
|
+
".tiff": "image/tiff",
|
|
18
|
+
".svg": "image/svg+xml",
|
|
19
|
+
# Documents
|
|
20
|
+
".pdf": "application/pdf",
|
|
21
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
22
|
+
".doc": "application/msword",
|
|
23
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
24
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
25
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
26
|
+
".xls": "application/vnd.ms-excel",
|
|
27
|
+
# Audio
|
|
28
|
+
".wav": "audio/wav",
|
|
29
|
+
".mp3": "audio/mpeg",
|
|
30
|
+
".m4a": "audio/x-m4a",
|
|
31
|
+
".flac": "audio/flac",
|
|
32
|
+
".ogg": "audio/ogg",
|
|
33
|
+
".aac": "audio/aac",
|
|
34
|
+
# Video
|
|
35
|
+
".mp4": "video/mp4",
|
|
36
|
+
".webm": "video/webm",
|
|
37
|
+
".avi": "video/x-msvideo",
|
|
38
|
+
".mov": "video/quicktime",
|
|
39
|
+
# Text/Code
|
|
40
|
+
".txt": "text/plain",
|
|
41
|
+
".md": "text/markdown",
|
|
42
|
+
".markdown": "text/markdown",
|
|
43
|
+
".json": "application/json",
|
|
44
|
+
".yaml": "application/x-yaml",
|
|
45
|
+
".yml": "application/x-yaml",
|
|
46
|
+
".xml": "application/xml",
|
|
47
|
+
".html": "text/html",
|
|
48
|
+
".css": "text/css",
|
|
49
|
+
".js": "application/javascript",
|
|
50
|
+
".py": "text/x-python",
|
|
51
|
+
".ts": "application/typescript",
|
|
52
|
+
".csv": "text/csv",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# MIME type to extension mapping (reverse of above, preferring shorter extensions)
|
|
56
|
+
MIME_TO_EXTENSION: dict[str, str] = {
|
|
57
|
+
# Images
|
|
58
|
+
"image/png": ".png",
|
|
59
|
+
"image/jpeg": ".jpg",
|
|
60
|
+
"image/gif": ".gif",
|
|
61
|
+
"image/webp": ".webp",
|
|
62
|
+
"image/bmp": ".bmp",
|
|
63
|
+
"image/tiff": ".tiff",
|
|
64
|
+
"image/svg+xml": ".svg",
|
|
65
|
+
# Documents
|
|
66
|
+
"application/pdf": ".pdf",
|
|
67
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
68
|
+
"application/msword": ".doc",
|
|
69
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
70
|
+
"application/vnd.ms-powerpoint": ".ppt",
|
|
71
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
72
|
+
"application/vnd.ms-excel": ".xls",
|
|
73
|
+
# Audio
|
|
74
|
+
"audio/wav": ".wav",
|
|
75
|
+
"audio/mpeg": ".mp3",
|
|
76
|
+
"audio/x-m4a": ".m4a",
|
|
77
|
+
"audio/mp4": ".m4a",
|
|
78
|
+
"audio/flac": ".flac",
|
|
79
|
+
"audio/ogg": ".ogg",
|
|
80
|
+
"audio/aac": ".aac",
|
|
81
|
+
# Video
|
|
82
|
+
"video/mp4": ".mp4",
|
|
83
|
+
"video/webm": ".webm",
|
|
84
|
+
"video/x-msvideo": ".avi",
|
|
85
|
+
"video/quicktime": ".mov",
|
|
86
|
+
# Text/Code
|
|
87
|
+
"text/plain": ".txt",
|
|
88
|
+
"text/markdown": ".md",
|
|
89
|
+
"application/json": ".json",
|
|
90
|
+
"application/x-yaml": ".yaml",
|
|
91
|
+
"application/xml": ".xml",
|
|
92
|
+
"text/html": ".html",
|
|
93
|
+
"text/css": ".css",
|
|
94
|
+
"application/javascript": ".js",
|
|
95
|
+
"text/x-python": ".py",
|
|
96
|
+
"application/typescript": ".ts",
|
|
97
|
+
"text/csv": ".csv",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Grouped by category for convenience
|
|
101
|
+
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".svg"}
|
|
102
|
+
DOCUMENT_EXTENSIONS = {".pdf", ".docx", ".doc", ".pptx", ".ppt", ".xlsx", ".xls"}
|
|
103
|
+
AUDIO_EXTENSIONS = {".wav", ".mp3", ".m4a", ".flac", ".ogg", ".aac"}
|
|
104
|
+
VIDEO_EXTENSIONS = {".mp4", ".webm", ".avi", ".mov"}
|
|
105
|
+
TEXT_EXTENSIONS = {".txt", ".md", ".markdown", ".json", ".yaml", ".yml", ".xml", ".html", ".css", ".js", ".py", ".ts", ".csv"}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_extension(mime_type: str, default: str = ".bin") -> str:
|
|
109
|
+
"""
|
|
110
|
+
Get file extension for a MIME type.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
mime_type: MIME type string (e.g., "image/png")
|
|
114
|
+
default: Default extension if MIME type not found
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
File extension with leading dot (e.g., ".png")
|
|
118
|
+
"""
|
|
119
|
+
return MIME_TO_EXTENSION.get(mime_type, default)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_mime_type(extension: str, default: str = "application/octet-stream") -> str:
|
|
123
|
+
"""
|
|
124
|
+
Get MIME type for a file extension.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
extension: File extension with or without leading dot
|
|
128
|
+
default: Default MIME type if extension not found
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
MIME type string (e.g., "image/png")
|
|
132
|
+
"""
|
|
133
|
+
# Normalize extension to have leading dot
|
|
134
|
+
ext = extension if extension.startswith(".") else f".{extension}"
|
|
135
|
+
return EXTENSION_TO_MIME.get(ext.lower(), default)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def is_image(extension_or_mime: str) -> bool:
|
|
139
|
+
"""Check if extension or MIME type represents an image."""
|
|
140
|
+
if extension_or_mime.startswith("."):
|
|
141
|
+
return extension_or_mime.lower() in IMAGE_EXTENSIONS
|
|
142
|
+
return extension_or_mime.startswith("image/")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_audio(extension_or_mime: str) -> bool:
|
|
146
|
+
"""Check if extension or MIME type represents audio."""
|
|
147
|
+
if extension_or_mime.startswith("."):
|
|
148
|
+
return extension_or_mime.lower() in AUDIO_EXTENSIONS
|
|
149
|
+
return extension_or_mime.startswith("audio/")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_document(extension_or_mime: str) -> bool:
|
|
153
|
+
"""Check if extension or MIME type represents a document."""
|
|
154
|
+
if extension_or_mime.startswith("."):
|
|
155
|
+
return extension_or_mime.lower() in DOCUMENT_EXTENSIONS
|
|
156
|
+
# Check common document MIME types
|
|
157
|
+
doc_mimes = {"application/pdf", "application/msword"}
|
|
158
|
+
return extension_or_mime in doc_mimes or "officedocument" in extension_or_mime
|
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)
|
|
@@ -188,12 +209,13 @@ def load_agent_schema(
|
|
|
188
209
|
Search Order:
|
|
189
210
|
1. Check cache (if use_cache=True and schema found in FS cache)
|
|
190
211
|
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.
|
|
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)
|
|
197
219
|
|
|
198
220
|
Args:
|
|
199
221
|
schema_name_or_path: Schema name or file path
|
|
@@ -247,7 +269,28 @@ def load_agent_schema(
|
|
|
247
269
|
# 2. Normalize name for package resource search
|
|
248
270
|
base_name = cache_key
|
|
249
271
|
|
|
250
|
-
# 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
|
|
251
294
|
for search_pattern in SCHEMA_SEARCH_PATHS:
|
|
252
295
|
search_path = search_pattern.format(name=base_name)
|
|
253
296
|
|
|
@@ -272,7 +315,7 @@ def load_agent_schema(
|
|
|
272
315
|
logger.debug(f"Could not load from {search_path}: {e}")
|
|
273
316
|
continue
|
|
274
317
|
|
|
275
|
-
#
|
|
318
|
+
# 5. Try database LOOKUP fallback (if enabled and user_id provided)
|
|
276
319
|
if enable_db_fallback and user_id:
|
|
277
320
|
try:
|
|
278
321
|
logger.debug(f"Attempting database LOOKUP for schema: {base_name} (user_id={user_id})")
|
|
@@ -284,8 +327,13 @@ def load_agent_schema(
|
|
|
284
327
|
logger.debug(f"Database schema lookup failed: {e}")
|
|
285
328
|
# Fall through to error below
|
|
286
329
|
|
|
287
|
-
#
|
|
330
|
+
# 6. Schema not found in any location
|
|
288
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
|
+
|
|
289
337
|
db_search_note = ""
|
|
290
338
|
if enable_db_fallback:
|
|
291
339
|
if user_id:
|
|
@@ -296,7 +344,8 @@ def load_agent_schema(
|
|
|
296
344
|
raise FileNotFoundError(
|
|
297
345
|
f"Schema not found: {schema_name_or_path}\n"
|
|
298
346
|
f"Searched locations:\n"
|
|
299
|
-
f" - Exact path: {path}
|
|
347
|
+
f" - Exact path: {path}"
|
|
348
|
+
f"{custom_paths_note}\n"
|
|
300
349
|
f" - Package resources: {', '.join(searched_paths)}"
|
|
301
350
|
f"{db_search_note}"
|
|
302
351
|
)
|
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
|