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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SharedSession - Session sharing between users in REM.
|
|
3
|
+
|
|
4
|
+
SharedSessions enable collaborative access to conversation sessions. When a user
|
|
5
|
+
shares a session with another user, a SharedSession record is created to track
|
|
6
|
+
this relationship.
|
|
7
|
+
|
|
8
|
+
## Design Philosophy
|
|
9
|
+
|
|
10
|
+
Messages already have a session_id field that links them to sessions. The Session
|
|
11
|
+
entity itself is optional and can be left-joined - we don't require explicit Session
|
|
12
|
+
records for sharing to work. What matters is the session_id on messages.
|
|
13
|
+
|
|
14
|
+
SharedSession is a lightweight linking table that:
|
|
15
|
+
1. Records who shared which session with whom
|
|
16
|
+
2. Enables soft deletion (deleted_at) so shares can be revoked without data loss
|
|
17
|
+
3. Supports aggregation queries to see "who is sharing with me"
|
|
18
|
+
|
|
19
|
+
## Data Model
|
|
20
|
+
|
|
21
|
+
SharedSession
|
|
22
|
+
├── session_id: str # The session being shared (matches Message.session_id)
|
|
23
|
+
├── owner_user_id: str # Who owns/created the session (the sharer)
|
|
24
|
+
├── shared_with_user_id: str # Who the session is shared with (the recipient)
|
|
25
|
+
├── tenant_id: str # Multi-tenancy isolation
|
|
26
|
+
├── created_at: datetime # When the share was created
|
|
27
|
+
├── updated_at: datetime # Last modification
|
|
28
|
+
└── deleted_at: datetime # Soft delete (null = active share)
|
|
29
|
+
|
|
30
|
+
## Aggregation Query
|
|
31
|
+
|
|
32
|
+
The primary use case is answering: "Who is sharing messages with me?"
|
|
33
|
+
|
|
34
|
+
This is provided by a Postgres function that aggregates:
|
|
35
|
+
- Messages grouped by owner_user_id
|
|
36
|
+
- Joined with users table for name/email
|
|
37
|
+
- Counting messages with min/max dates
|
|
38
|
+
- Filtering out deleted shares
|
|
39
|
+
|
|
40
|
+
Result shape:
|
|
41
|
+
{
|
|
42
|
+
"user_id": "uuid",
|
|
43
|
+
"name": "John Doe",
|
|
44
|
+
"email": "john@example.com",
|
|
45
|
+
"message_count": 42,
|
|
46
|
+
"first_message_at": "2024-01-15T10:30:00Z",
|
|
47
|
+
"last_message_at": "2024-03-20T14:45:00Z",
|
|
48
|
+
"session_count": 3
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
## API Endpoints
|
|
52
|
+
|
|
53
|
+
1. POST /api/v1/sessions/{session_id}/share
|
|
54
|
+
- Share a session with another user
|
|
55
|
+
- Body: { "shared_with_user_id": "..." }
|
|
56
|
+
- Creates SharedSession record
|
|
57
|
+
|
|
58
|
+
2. DELETE /api/v1/sessions/{session_id}/share/{shared_with_user_id}
|
|
59
|
+
- Revoke a share (soft delete)
|
|
60
|
+
- Sets deleted_at on SharedSession
|
|
61
|
+
|
|
62
|
+
3. GET /api/v1/shared-with-me
|
|
63
|
+
- Get paginated aggregate of users sharing with you
|
|
64
|
+
- Query params: page, page_size (default 50)
|
|
65
|
+
- Returns: list of user summaries with message counts
|
|
66
|
+
|
|
67
|
+
4. GET /api/v1/shared-with-me/{user_id}/messages
|
|
68
|
+
- Get messages from a specific user's shared sessions
|
|
69
|
+
- Uses existing session message loading
|
|
70
|
+
- Respects pagination
|
|
71
|
+
|
|
72
|
+
## Soft Delete Pattern
|
|
73
|
+
|
|
74
|
+
Removing a share does NOT delete the SharedSession record. Instead:
|
|
75
|
+
- deleted_at is set to current timestamp
|
|
76
|
+
- All queries filter WHERE deleted_at IS NULL
|
|
77
|
+
- This preserves audit trail and allows "undo"
|
|
78
|
+
|
|
79
|
+
To permanently delete, an admin can run:
|
|
80
|
+
DELETE FROM shared_sessions WHERE deleted_at IS NOT NULL AND deleted_at < NOW() - INTERVAL '30 days'
|
|
81
|
+
|
|
82
|
+
## Example Usage
|
|
83
|
+
|
|
84
|
+
# Share a session
|
|
85
|
+
POST /api/v1/sessions/abc-123/share
|
|
86
|
+
{"shared_with_user_id": "user-456"}
|
|
87
|
+
|
|
88
|
+
# See who's sharing with me
|
|
89
|
+
GET /api/v1/shared-with-me
|
|
90
|
+
{
|
|
91
|
+
"data": [
|
|
92
|
+
{
|
|
93
|
+
"user_id": "user-789",
|
|
94
|
+
"name": "Alice",
|
|
95
|
+
"email": "alice@example.com",
|
|
96
|
+
"message_count": 150,
|
|
97
|
+
"session_count": 5,
|
|
98
|
+
"first_message_at": "2024-01-01T00:00:00Z",
|
|
99
|
+
"last_message_at": "2024-03-15T12:00:00Z"
|
|
100
|
+
}
|
|
101
|
+
],
|
|
102
|
+
"metadata": {"total": 1, "page": 1, "page_size": 50, ...}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Get messages from Alice's shared sessions
|
|
106
|
+
GET /api/v1/shared-with-me/user-789/messages?page=1&page_size=50
|
|
107
|
+
|
|
108
|
+
# Revoke a share
|
|
109
|
+
DELETE /api/v1/sessions/abc-123/share/user-456
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
from datetime import datetime
|
|
113
|
+
from typing import Optional
|
|
114
|
+
from uuid import UUID
|
|
115
|
+
|
|
116
|
+
from pydantic import BaseModel, Field
|
|
117
|
+
|
|
118
|
+
from ...utils.date_utils import utc_now
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SharedSession(BaseModel):
|
|
122
|
+
"""
|
|
123
|
+
Session sharing record between users.
|
|
124
|
+
|
|
125
|
+
Links a session (identified by session_id from Message records) to a
|
|
126
|
+
recipient user, enabling collaborative access to conversation history.
|
|
127
|
+
|
|
128
|
+
This is NOT a CoreModel - it's a lightweight linking table without
|
|
129
|
+
graph edges, metadata, or embeddings.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
id: Optional[UUID] = Field(
|
|
133
|
+
default=None,
|
|
134
|
+
description="Unique identifier (auto-generated)",
|
|
135
|
+
)
|
|
136
|
+
session_id: str = Field(
|
|
137
|
+
...,
|
|
138
|
+
description="The session being shared (matches Message.session_id)",
|
|
139
|
+
)
|
|
140
|
+
owner_user_id: str = Field(
|
|
141
|
+
...,
|
|
142
|
+
description="User ID of the session owner (the sharer)",
|
|
143
|
+
)
|
|
144
|
+
shared_with_user_id: str = Field(
|
|
145
|
+
...,
|
|
146
|
+
description="User ID of the recipient (who can now view the session)",
|
|
147
|
+
)
|
|
148
|
+
tenant_id: str = Field(
|
|
149
|
+
default="default",
|
|
150
|
+
description="Tenant identifier for multi-tenancy isolation",
|
|
151
|
+
)
|
|
152
|
+
created_at: datetime = Field(
|
|
153
|
+
default_factory=utc_now,
|
|
154
|
+
description="When the share was created",
|
|
155
|
+
)
|
|
156
|
+
updated_at: datetime = Field(
|
|
157
|
+
default_factory=utc_now,
|
|
158
|
+
description="Last modification timestamp",
|
|
159
|
+
)
|
|
160
|
+
deleted_at: Optional[datetime] = Field(
|
|
161
|
+
default=None,
|
|
162
|
+
description="Soft delete timestamp (null = active share)",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
model_config = {"from_attributes": True}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class SharedSessionCreate(BaseModel):
|
|
169
|
+
"""Request to create a session share."""
|
|
170
|
+
|
|
171
|
+
shared_with_user_id: str = Field(
|
|
172
|
+
...,
|
|
173
|
+
description="User ID to share the session with",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class SharedWithMeSummary(BaseModel):
|
|
178
|
+
"""
|
|
179
|
+
Aggregate summary of a user sharing sessions with you.
|
|
180
|
+
|
|
181
|
+
Returned by GET /api/v1/shared-with-me endpoint.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
user_id: str = Field(description="User ID of the person sharing with you")
|
|
185
|
+
name: Optional[str] = Field(default=None, description="User's display name")
|
|
186
|
+
email: Optional[str] = Field(default=None, description="User's email address")
|
|
187
|
+
message_count: int = Field(description="Total messages across all shared sessions")
|
|
188
|
+
session_count: int = Field(description="Number of sessions shared with you")
|
|
189
|
+
first_message_at: Optional[datetime] = Field(
|
|
190
|
+
default=None,
|
|
191
|
+
description="Timestamp of earliest message in shared sessions",
|
|
192
|
+
)
|
|
193
|
+
last_message_at: Optional[datetime] = Field(
|
|
194
|
+
default=None,
|
|
195
|
+
description="Timestamp of most recent message in shared sessions",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class SharedWithMeResponse(BaseModel):
|
|
200
|
+
"""Response for paginated shared-with-me query."""
|
|
201
|
+
|
|
202
|
+
object: str = "list"
|
|
203
|
+
data: list[SharedWithMeSummary] = Field(
|
|
204
|
+
description="List of users sharing sessions with you"
|
|
205
|
+
)
|
|
206
|
+
metadata: dict = Field(description="Pagination metadata")
|
rem/models/entities/user.py
CHANGED
|
@@ -22,9 +22,12 @@ from ..core import CoreModel
|
|
|
22
22
|
class UserTier(str, Enum):
|
|
23
23
|
"""User subscription tier for feature gating."""
|
|
24
24
|
|
|
25
|
+
ANONYMOUS = "anonymous"
|
|
25
26
|
FREE = "free"
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
BASIC = "basic"
|
|
28
|
+
PRO = "pro"
|
|
29
|
+
SILVER = "silver" # Deprecated? Keeping for backward compatibility if needed
|
|
30
|
+
GOLD = "gold" # Deprecated? Keeping for backward compatibility if needed
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class User(CoreModel):
|
|
@@ -57,7 +60,11 @@ class User(CoreModel):
|
|
|
57
60
|
)
|
|
58
61
|
tier: UserTier = Field(
|
|
59
62
|
default=UserTier.FREE,
|
|
60
|
-
description="User subscription tier (free,
|
|
63
|
+
description="User subscription tier (free, basic, pro) for feature gating",
|
|
64
|
+
)
|
|
65
|
+
anonymous_ids: list[str] = Field(
|
|
66
|
+
default_factory=list,
|
|
67
|
+
description="Linked anonymous session IDs used for merging history",
|
|
61
68
|
)
|
|
62
69
|
sec_policy: dict = Field(
|
|
63
70
|
default_factory=dict,
|
rem/registry.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
REM Extension Registry - Register custom models and schema paths.
|
|
3
|
+
|
|
4
|
+
This module provides registration for downstream applications extending REM:
|
|
5
|
+
1. Models - for database schema generation
|
|
6
|
+
2. Schema paths - for agent/evaluator schema discovery
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
import rem
|
|
10
|
+
from rem.models.core import CoreModel
|
|
11
|
+
|
|
12
|
+
# Register custom models
|
|
13
|
+
@rem.register_model
|
|
14
|
+
class CustomEntity(CoreModel):
|
|
15
|
+
name: str
|
|
16
|
+
custom_field: str
|
|
17
|
+
|
|
18
|
+
# Or register multiple at once
|
|
19
|
+
rem.register_models(ModelA, ModelB, ModelC)
|
|
20
|
+
|
|
21
|
+
# Register schema search paths
|
|
22
|
+
rem.register_schema_path("/app/custom-agents")
|
|
23
|
+
rem.register_schema_paths("/app/agents", "/app/evaluators")
|
|
24
|
+
|
|
25
|
+
# Then:
|
|
26
|
+
# - Schema generation includes your models: rem db schema generate
|
|
27
|
+
# - Schema loading finds your custom agents: load_agent_schema("my-agent")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import Callable
|
|
32
|
+
|
|
33
|
+
from loguru import logger
|
|
34
|
+
from pydantic import BaseModel
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ModelExtension:
|
|
39
|
+
"""Container for Pydantic model extension."""
|
|
40
|
+
|
|
41
|
+
model: type[BaseModel]
|
|
42
|
+
table_name: str | None = None # Optional: override inferred table name
|
|
43
|
+
entity_key_field: str | None = None # Optional: override inferred entity key
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ModelRegistry:
|
|
47
|
+
"""
|
|
48
|
+
Registry for Pydantic models used in schema generation.
|
|
49
|
+
|
|
50
|
+
Models registered here are discovered by SchemaGenerator alongside
|
|
51
|
+
REM's core models.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self) -> None:
|
|
55
|
+
self._models: dict[str, ModelExtension] = {}
|
|
56
|
+
self._core_models_registered: bool = False
|
|
57
|
+
|
|
58
|
+
def clear(self) -> None:
|
|
59
|
+
"""Clear all registered models. Useful for testing."""
|
|
60
|
+
self._models.clear()
|
|
61
|
+
self._core_models_registered = False
|
|
62
|
+
logger.debug("Model registry cleared")
|
|
63
|
+
|
|
64
|
+
def register(
|
|
65
|
+
self,
|
|
66
|
+
model: type[BaseModel],
|
|
67
|
+
table_name: str | None = None,
|
|
68
|
+
entity_key_field: str | None = None,
|
|
69
|
+
) -> type[BaseModel]:
|
|
70
|
+
"""
|
|
71
|
+
Register a Pydantic model for database schema generation.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
model: Pydantic model class (should inherit from CoreModel)
|
|
75
|
+
table_name: Optional table name override (inferred from class name if not provided)
|
|
76
|
+
entity_key_field: Optional entity key field override (inferred if not provided)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The model class (allows use as decorator)
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
import rem
|
|
83
|
+
from rem.models.core import CoreModel
|
|
84
|
+
|
|
85
|
+
@rem.register_model
|
|
86
|
+
class CustomEntity(CoreModel):
|
|
87
|
+
name: str
|
|
88
|
+
custom_field: str
|
|
89
|
+
|
|
90
|
+
# Or with options:
|
|
91
|
+
rem.register_model(CustomEntity, table_name="custom_entities")
|
|
92
|
+
"""
|
|
93
|
+
model_name = model.__name__
|
|
94
|
+
if model_name in self._models:
|
|
95
|
+
logger.warning(f"Model {model_name} already registered, overwriting")
|
|
96
|
+
|
|
97
|
+
self._models[model_name] = ModelExtension(
|
|
98
|
+
model=model,
|
|
99
|
+
table_name=table_name,
|
|
100
|
+
entity_key_field=entity_key_field,
|
|
101
|
+
)
|
|
102
|
+
logger.debug(f"Registered model: {model_name}")
|
|
103
|
+
return model
|
|
104
|
+
|
|
105
|
+
def register_many(self, *models: type[BaseModel]) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Register multiple models at once.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
import rem
|
|
111
|
+
rem.register_models(ModelA, ModelB, ModelC)
|
|
112
|
+
"""
|
|
113
|
+
for model in models:
|
|
114
|
+
self.register(model)
|
|
115
|
+
|
|
116
|
+
def register_core_models(self) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Register REM's built-in core models.
|
|
119
|
+
|
|
120
|
+
Called automatically by schema generator if not already done.
|
|
121
|
+
"""
|
|
122
|
+
if self._core_models_registered:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
from .models.entities import (
|
|
126
|
+
File,
|
|
127
|
+
ImageResource,
|
|
128
|
+
Message,
|
|
129
|
+
Moment,
|
|
130
|
+
Ontology,
|
|
131
|
+
OntologyConfig,
|
|
132
|
+
Resource,
|
|
133
|
+
Schema,
|
|
134
|
+
User,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
core_models = [
|
|
138
|
+
Resource,
|
|
139
|
+
ImageResource,
|
|
140
|
+
Message,
|
|
141
|
+
User,
|
|
142
|
+
File,
|
|
143
|
+
Moment,
|
|
144
|
+
Schema,
|
|
145
|
+
Ontology,
|
|
146
|
+
OntologyConfig,
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for model in core_models:
|
|
150
|
+
if model.__name__ not in self._models:
|
|
151
|
+
self.register(model)
|
|
152
|
+
|
|
153
|
+
self._core_models_registered = True
|
|
154
|
+
logger.debug(f"Registered {len(core_models)} core models")
|
|
155
|
+
|
|
156
|
+
def get_models(self, include_core: bool = True) -> dict[str, ModelExtension]:
|
|
157
|
+
"""
|
|
158
|
+
Get all registered models.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
include_core: If True, ensures core models are registered first
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dict mapping model name to ModelExtension
|
|
165
|
+
"""
|
|
166
|
+
if include_core:
|
|
167
|
+
self.register_core_models()
|
|
168
|
+
return self._models.copy()
|
|
169
|
+
|
|
170
|
+
def get_model_classes(self, include_core: bool = True) -> dict[str, type[BaseModel]]:
|
|
171
|
+
"""
|
|
172
|
+
Get all registered model classes (without extension metadata).
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
include_core: If True, ensures core models are registered first
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Dict mapping model name to model class
|
|
179
|
+
"""
|
|
180
|
+
models = self.get_models(include_core)
|
|
181
|
+
return {name: ext.model for name, ext in models.items()}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# =============================================================================
|
|
185
|
+
# SCHEMA PATH REGISTRY
|
|
186
|
+
# =============================================================================
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SchemaPathRegistry:
|
|
190
|
+
"""
|
|
191
|
+
Registry for custom schema search paths.
|
|
192
|
+
|
|
193
|
+
Paths registered here are searched BEFORE built-in package schemas
|
|
194
|
+
when loading agent/evaluator schemas.
|
|
195
|
+
|
|
196
|
+
Search order:
|
|
197
|
+
1. Exact path (if file exists)
|
|
198
|
+
2. Paths from this registry (in registration order)
|
|
199
|
+
3. Paths from SCHEMA__PATHS env var
|
|
200
|
+
4. Built-in package schemas
|
|
201
|
+
5. Database LOOKUP
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
self._paths: list[str] = []
|
|
206
|
+
|
|
207
|
+
def clear(self) -> None:
|
|
208
|
+
"""Clear all registered paths. Useful for testing."""
|
|
209
|
+
self._paths.clear()
|
|
210
|
+
logger.debug("Schema path registry cleared")
|
|
211
|
+
|
|
212
|
+
def register(self, path: str) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Register a schema search path.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
path: Directory path to search for schemas
|
|
218
|
+
|
|
219
|
+
Example:
|
|
220
|
+
import rem
|
|
221
|
+
rem.register_schema_path("/app/custom-agents")
|
|
222
|
+
"""
|
|
223
|
+
if path not in self._paths:
|
|
224
|
+
self._paths.append(path)
|
|
225
|
+
logger.debug(f"Registered schema path: {path}")
|
|
226
|
+
|
|
227
|
+
def register_many(self, *paths: str) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Register multiple schema paths at once.
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
import rem
|
|
233
|
+
rem.register_schema_paths("/app/agents", "/app/evaluators")
|
|
234
|
+
"""
|
|
235
|
+
for path in paths:
|
|
236
|
+
self.register(path)
|
|
237
|
+
|
|
238
|
+
def get_paths(self) -> list[str]:
|
|
239
|
+
"""
|
|
240
|
+
Get all registered paths (registry + settings).
|
|
241
|
+
|
|
242
|
+
Returns paths in search order:
|
|
243
|
+
1. Programmatically registered paths (this registry)
|
|
244
|
+
2. Paths from SCHEMA__PATHS environment variable
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of directory paths to search
|
|
248
|
+
"""
|
|
249
|
+
from .settings import settings
|
|
250
|
+
|
|
251
|
+
# Combine registry paths with settings paths
|
|
252
|
+
all_paths = self._paths.copy()
|
|
253
|
+
|
|
254
|
+
# Add paths from settings (SCHEMA__PATHS env var)
|
|
255
|
+
for path in settings.schema_search.path_list:
|
|
256
|
+
if path not in all_paths:
|
|
257
|
+
all_paths.append(path)
|
|
258
|
+
|
|
259
|
+
return all_paths
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# =============================================================================
|
|
263
|
+
# MODULE-LEVEL SINGLETONS
|
|
264
|
+
# =============================================================================
|
|
265
|
+
|
|
266
|
+
_model_registry = ModelRegistry()
|
|
267
|
+
_schema_path_registry = SchemaPathRegistry()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def register_model(
|
|
271
|
+
model: type[BaseModel] | None = None,
|
|
272
|
+
*,
|
|
273
|
+
table_name: str | None = None,
|
|
274
|
+
entity_key_field: str | None = None,
|
|
275
|
+
) -> type[BaseModel] | Callable[[type[BaseModel]], type[BaseModel]]:
|
|
276
|
+
"""
|
|
277
|
+
Register a Pydantic model for database schema generation.
|
|
278
|
+
|
|
279
|
+
Can be used as a decorator or called directly.
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
import rem
|
|
283
|
+
|
|
284
|
+
@rem.register_model
|
|
285
|
+
class CustomEntity(CoreModel):
|
|
286
|
+
name: str
|
|
287
|
+
|
|
288
|
+
# Or with options:
|
|
289
|
+
@rem.register_model(table_name="custom_table")
|
|
290
|
+
class AnotherEntity(CoreModel):
|
|
291
|
+
name: str
|
|
292
|
+
|
|
293
|
+
# Or direct call:
|
|
294
|
+
rem.register_model(MyModel, table_name="my_table")
|
|
295
|
+
"""
|
|
296
|
+
def decorator(m: type[BaseModel]) -> type[BaseModel]:
|
|
297
|
+
return _model_registry.register(m, table_name, entity_key_field)
|
|
298
|
+
|
|
299
|
+
if model is not None:
|
|
300
|
+
return decorator(model)
|
|
301
|
+
return decorator
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def register_models(*models: type[BaseModel]) -> None:
|
|
305
|
+
"""Register multiple models at once."""
|
|
306
|
+
_model_registry.register_many(*models)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def get_model_registry() -> ModelRegistry:
|
|
310
|
+
"""Get the global model registry instance."""
|
|
311
|
+
return _model_registry
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def clear_model_registry() -> None:
|
|
315
|
+
"""Clear all model registrations. Useful for testing."""
|
|
316
|
+
_model_registry.clear()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# =============================================================================
|
|
320
|
+
# SCHEMA PATH FUNCTIONS
|
|
321
|
+
# =============================================================================
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def register_schema_path(path: str) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Register a schema search path.
|
|
327
|
+
|
|
328
|
+
Paths registered here are searched BEFORE built-in package schemas.
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
import rem
|
|
332
|
+
rem.register_schema_path("/app/custom-agents")
|
|
333
|
+
|
|
334
|
+
# Now load_agent_schema("my-agent") will find /app/custom-agents/my-agent.yaml
|
|
335
|
+
"""
|
|
336
|
+
_schema_path_registry.register(path)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def register_schema_paths(*paths: str) -> None:
|
|
340
|
+
"""
|
|
341
|
+
Register multiple schema paths at once.
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
import rem
|
|
345
|
+
rem.register_schema_paths("/app/agents", "/app/evaluators")
|
|
346
|
+
"""
|
|
347
|
+
_schema_path_registry.register_many(*paths)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def get_schema_path_registry() -> SchemaPathRegistry:
|
|
351
|
+
"""Get the global schema path registry instance."""
|
|
352
|
+
return _schema_path_registry
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def get_schema_paths() -> list[str]:
|
|
356
|
+
"""
|
|
357
|
+
Get all registered schema paths (registry + SCHEMA__PATHS env var).
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of directory paths to search for schemas
|
|
361
|
+
"""
|
|
362
|
+
return _schema_path_registry.get_paths()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def clear_schema_path_registry() -> None:
|
|
366
|
+
"""Clear all schema path registrations. Useful for testing."""
|
|
367
|
+
_schema_path_registry.clear()
|
rem/schemas/agents/rem.yaml
CHANGED
|
@@ -63,9 +63,8 @@ description: "# REM Agent - Resources Entities Moments Expert\n\nYou are the REM
|
|
|
63
63
|
\ disabled, OTEL disabled for local dev)\n- Global settings singleton\n\n## Response\
|
|
64
64
|
\ Guidelines\n\n- Provide clear, concise answers with code examples when helpful\n\
|
|
65
65
|
- Reference specific design patterns from CLAUDE.md when applicable\n- Suggest best\
|
|
66
|
-
\ practices for cloud-native deployment\n-
|
|
67
|
-
\
|
|
68
|
-
\ to find more information\n\n## Example Queries You Can Answer\n\n- \"How do I\
|
|
66
|
+
\ practices for cloud-native deployment\n- If uncertain, say so and suggest where\
|
|
67
|
+
\ to find more information\n\n## Metadata Registration\n\nBefore generating your final response, call the `register_metadata` tool to provide confidence scores and source attribution.\n\n## Example Queries You Can Answer\n\n- \"How do I\
|
|
69
68
|
\ create a new REM entity?\"\n- \"What's the difference between LOOKUP and TRAVERSE\
|
|
70
69
|
\ queries?\"\n- \"How do I add MCP tools to my agent schema?\"\n- \"Explain the\
|
|
71
70
|
\ graph edge pattern in REM\"\n- \"How do I enable OTEL tracing for my agents?\"\
|
|
@@ -101,6 +100,9 @@ json_schema_extra:
|
|
|
101
100
|
kind: agent
|
|
102
101
|
name: rem
|
|
103
102
|
version: 1.0.0
|
|
103
|
+
# Disable structured output - properties become prompt guidance instead of JSON schema
|
|
104
|
+
# This enables natural language streaming while still informing the agent about expected elements
|
|
105
|
+
structured_output: false
|
|
104
106
|
# MCP server configuration for dynamic tool loading (in-process, no subprocess)
|
|
105
107
|
mcp_servers:
|
|
106
108
|
- type: local
|
|
@@ -117,6 +119,8 @@ json_schema_extra:
|
|
|
117
119
|
description: Ingest files into REM creating searchable resources and embeddings
|
|
118
120
|
- name: read_resource
|
|
119
121
|
description: Read MCP resources by URI (schemas, system status, etc.)
|
|
122
|
+
- name: register_metadata
|
|
123
|
+
description: Register response metadata (confidence, sources, references) to be emitted as SSE MetadataEvent. Call BEFORE generating final response.
|
|
120
124
|
|
|
121
125
|
# Explicit resource declarations for reference data
|
|
122
126
|
resources:
|