remdb 0.3.14__py3-none-any.whl → 0.3.133__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/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +16 -2
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +51 -27
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +112 -17
- rem/agentic/otel/setup.py +93 -4
- rem/agentic/providers/phoenix.py +302 -109
- rem/agentic/providers/pydantic_ai.py +215 -26
- rem/agentic/schema.py +361 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +215 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +132 -40
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +26 -5
- rem/api/mcp_router/tools.py +465 -7
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +70 -0
- rem/api/routers/chat/completions.py +402 -20
- rem/api/routers/chat/models.py +88 -10
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +542 -0
- rem/api/routers/chat/streaming.py +642 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +268 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +1 -3
- rem/cli/commands/db.py +386 -143
- rem/cli/commands/experiments.py +418 -27
- rem/cli/commands/process.py +14 -8
- rem/cli/commands/schema.py +97 -50
- rem/cli/main.py +27 -6
- rem/config.py +10 -3
- rem/models/core/core_model.py +7 -1
- rem/models/core/experiment.py +54 -0
- 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/registry.py +10 -4
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/service.py +92 -20
- rem/services/embeddings/api.py +4 -4
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/client.py +154 -14
- rem/services/postgres/README.md +159 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +531 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/postgres/service.py +6 -6
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +24 -1
- rem/services/session/reload.py +1 -1
- rem/settings.py +324 -23
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2320 -393
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/date_utils.py +2 -2
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +220 -22
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/METADATA +335 -226
- {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/RECORD +86 -66
- {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1051
- rem/sql/migrations/003_seed_default_user.sql +0 -48
- {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/entry_points.txt +0 -0
rem/api/deps.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared FastAPI dependencies for authentication and authorization.
|
|
3
|
+
|
|
4
|
+
Provides dependency injection utilities for:
|
|
5
|
+
- Extracting current user from session
|
|
6
|
+
- Requiring authentication
|
|
7
|
+
- Requiring specific roles (admin, user)
|
|
8
|
+
- User-scoped data filtering with admin override
|
|
9
|
+
|
|
10
|
+
Design Pattern:
|
|
11
|
+
- Use as FastAPI dependencies via Depends()
|
|
12
|
+
- Middleware sets request.state.user and request.state.is_anonymous
|
|
13
|
+
- Dependencies extract and validate from request.state
|
|
14
|
+
- Admin users can access any user's data via filters
|
|
15
|
+
|
|
16
|
+
Roles:
|
|
17
|
+
- "admin": Full access to all data across all users
|
|
18
|
+
- "user": Default role, access limited to own data
|
|
19
|
+
- Anonymous: Rate-limited access, no persistent data
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
from rem.api.deps import require_auth, require_admin, get_user_filter
|
|
23
|
+
|
|
24
|
+
@router.get("/items")
|
|
25
|
+
async def list_items(user: dict = Depends(require_auth)):
|
|
26
|
+
# user is guaranteed to be authenticated
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
@router.post("/admin/action")
|
|
30
|
+
async def admin_action(user: dict = Depends(require_admin)):
|
|
31
|
+
# user is guaranteed to have admin role
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
@router.get("/sessions/{session_id}")
|
|
35
|
+
async def get_session(
|
|
36
|
+
session_id: str,
|
|
37
|
+
filters: dict = Depends(get_user_filter),
|
|
38
|
+
):
|
|
39
|
+
# filters includes user_id constraint (unless admin)
|
|
40
|
+
...
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from typing import Any
|
|
44
|
+
|
|
45
|
+
from fastapi import Depends, HTTPException, Request
|
|
46
|
+
from loguru import logger
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AuthError(HTTPException):
|
|
50
|
+
"""Authentication/Authorization error."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, detail: str, status_code: int = 401):
|
|
53
|
+
super().__init__(status_code=status_code, detail=detail)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_current_user(request: Request) -> dict | None:
|
|
57
|
+
"""
|
|
58
|
+
Get current user from request state (set by AuthMiddleware).
|
|
59
|
+
|
|
60
|
+
Returns None if no user authenticated.
|
|
61
|
+
Use require_auth() if authentication is mandatory.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
request: FastAPI request
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
User dict from session or None
|
|
68
|
+
"""
|
|
69
|
+
return getattr(request.state, "user", None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_is_anonymous(request: Request) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if current request is anonymous.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
request: FastAPI request
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if anonymous, False if authenticated
|
|
81
|
+
"""
|
|
82
|
+
return getattr(request.state, "is_anonymous", True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def require_auth(request: Request) -> dict:
|
|
86
|
+
"""
|
|
87
|
+
Require authenticated user.
|
|
88
|
+
|
|
89
|
+
Use as FastAPI dependency to enforce authentication.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
request: FastAPI request
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
User dict from session
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
HTTPException 401 if not authenticated
|
|
99
|
+
"""
|
|
100
|
+
user = get_current_user(request)
|
|
101
|
+
if not user:
|
|
102
|
+
raise AuthError("Authentication required", status_code=401)
|
|
103
|
+
return user
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def require_admin(request: Request) -> dict:
|
|
107
|
+
"""
|
|
108
|
+
Require authenticated user with admin role.
|
|
109
|
+
|
|
110
|
+
Use as FastAPI dependency to protect admin-only endpoints.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
request: FastAPI request
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
User dict from session
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
HTTPException 401 if not authenticated
|
|
120
|
+
HTTPException 403 if not admin
|
|
121
|
+
"""
|
|
122
|
+
user = require_auth(request)
|
|
123
|
+
roles = user.get("roles", [])
|
|
124
|
+
|
|
125
|
+
if "admin" not in roles:
|
|
126
|
+
logger.warning(f"Admin access denied for user {user.get('email')}")
|
|
127
|
+
raise AuthError("Admin access required", status_code=403)
|
|
128
|
+
|
|
129
|
+
return user
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_admin(user: dict | None) -> bool:
|
|
133
|
+
"""
|
|
134
|
+
Check if user has admin role.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
user: User dict or None
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
True if user is admin
|
|
141
|
+
"""
|
|
142
|
+
if not user:
|
|
143
|
+
return False
|
|
144
|
+
return "admin" in user.get("roles", [])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def get_user_filter(
|
|
148
|
+
request: Request,
|
|
149
|
+
x_user_id: str | None = None,
|
|
150
|
+
x_tenant_id: str = "default",
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
"""
|
|
153
|
+
Get user-scoped filter dict for database queries.
|
|
154
|
+
|
|
155
|
+
For regular users: Always filters by their own user_id.
|
|
156
|
+
For admin users: Can filter by any user_id (or no filter for all users).
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
request: FastAPI request
|
|
160
|
+
x_user_id: Optional user_id filter (admin only for cross-user)
|
|
161
|
+
x_tenant_id: Tenant ID for multi-tenancy
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Filter dict with appropriate user_id constraint
|
|
165
|
+
|
|
166
|
+
Usage:
|
|
167
|
+
@router.get("/items")
|
|
168
|
+
async def list_items(filters: dict = Depends(get_user_filter)):
|
|
169
|
+
return await repo.find(filters)
|
|
170
|
+
"""
|
|
171
|
+
user = get_current_user(request)
|
|
172
|
+
filters: dict[str, Any] = {"tenant_id": x_tenant_id}
|
|
173
|
+
|
|
174
|
+
if is_admin(user):
|
|
175
|
+
# Admin can filter by any user or see all
|
|
176
|
+
if x_user_id:
|
|
177
|
+
filters["user_id"] = x_user_id
|
|
178
|
+
# If no user_id specified, admin sees all (no user_id filter)
|
|
179
|
+
logger.debug(f"Admin access: filters={filters}")
|
|
180
|
+
elif user:
|
|
181
|
+
# Regular authenticated user: always filter by own user_id
|
|
182
|
+
filters["user_id"] = user.get("id")
|
|
183
|
+
if x_user_id and x_user_id != user.get("id"):
|
|
184
|
+
logger.warning(
|
|
185
|
+
f"User {user.get('email')} attempted to filter by user_id={x_user_id}"
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
# Anonymous: could use anonymous tracking ID or restrict access
|
|
189
|
+
# For now, anonymous can't access user-scoped data
|
|
190
|
+
anon_id = getattr(request.state, "anon_id", None)
|
|
191
|
+
if anon_id:
|
|
192
|
+
filters["user_id"] = f"anon:{anon_id}"
|
|
193
|
+
else:
|
|
194
|
+
filters["user_id"] = "anonymous"
|
|
195
|
+
|
|
196
|
+
return filters
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
async def require_owner_or_admin(
|
|
200
|
+
request: Request,
|
|
201
|
+
resource_user_id: str,
|
|
202
|
+
) -> dict:
|
|
203
|
+
"""
|
|
204
|
+
Require that current user owns the resource or is admin.
|
|
205
|
+
|
|
206
|
+
Use for parametric endpoints (GET /resource/{id}) where
|
|
207
|
+
only the owner or admin should access.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
request: FastAPI request
|
|
211
|
+
resource_user_id: The user_id of the resource being accessed
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
User dict from session
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
HTTPException 401 if not authenticated
|
|
218
|
+
HTTPException 403 if not owner and not admin
|
|
219
|
+
"""
|
|
220
|
+
user = require_auth(request)
|
|
221
|
+
|
|
222
|
+
if is_admin(user):
|
|
223
|
+
return user
|
|
224
|
+
|
|
225
|
+
if user.get("id") != resource_user_id:
|
|
226
|
+
logger.warning(
|
|
227
|
+
f"Access denied: user {user.get('email')} tried to access "
|
|
228
|
+
f"resource owned by {resource_user_id}"
|
|
229
|
+
)
|
|
230
|
+
raise AuthError("Access denied: not owner", status_code=403)
|
|
231
|
+
|
|
232
|
+
return user
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def get_user_id_from_request(request: Request) -> str:
|
|
236
|
+
"""
|
|
237
|
+
Get effective user_id for creating resources.
|
|
238
|
+
|
|
239
|
+
Returns authenticated user's ID or anonymous tracking ID.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
request: FastAPI request
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
User ID string
|
|
246
|
+
"""
|
|
247
|
+
user = get_current_user(request)
|
|
248
|
+
if user:
|
|
249
|
+
return user.get("id", "unknown")
|
|
250
|
+
|
|
251
|
+
anon_id = getattr(request.state, "anon_id", None)
|
|
252
|
+
if anon_id:
|
|
253
|
+
return f"anon:{anon_id}"
|
|
254
|
+
|
|
255
|
+
return "anonymous"
|
rem/api/main.py
CHANGED
|
@@ -26,10 +26,10 @@ Endpoints:
|
|
|
26
26
|
- /health : Health check
|
|
27
27
|
- /api/v1/mcp : MCP endpoint (HTTP transport)
|
|
28
28
|
- /api/v1/chat/completions : OpenAI-compatible chat completions (streaming & non-streaming)
|
|
29
|
-
- /api/v1/query : REM query execution (
|
|
29
|
+
- /api/v1/query : REM query execution (rem-dialect or natural-language)
|
|
30
30
|
- /api/v1/resources : Resource CRUD (TODO)
|
|
31
31
|
- /api/v1/moments : Moment CRUD (TODO)
|
|
32
|
-
- /api/auth/* : OAuth/OIDC authentication
|
|
32
|
+
- /api/auth/* : OAuth/OIDC authentication
|
|
33
33
|
- /docs : OpenAPI documentation
|
|
34
34
|
|
|
35
35
|
Headers → AgentContext Mapping:
|
|
@@ -59,8 +59,16 @@ Running:
|
|
|
59
59
|
hypercorn rem.api.main:app --bind 0.0.0.0:8000
|
|
60
60
|
"""
|
|
61
61
|
|
|
62
|
+
import importlib.metadata
|
|
62
63
|
import secrets
|
|
64
|
+
import sys
|
|
63
65
|
import time
|
|
66
|
+
|
|
67
|
+
# Get package version for API responses
|
|
68
|
+
try:
|
|
69
|
+
__version__ = importlib.metadata.version("remdb")
|
|
70
|
+
except importlib.metadata.PackageNotFoundError:
|
|
71
|
+
__version__ = "0.0.0-dev"
|
|
64
72
|
from contextlib import asynccontextmanager
|
|
65
73
|
|
|
66
74
|
from fastapi import FastAPI, Request
|
|
@@ -73,6 +81,23 @@ from starlette.middleware.sessions import SessionMiddleware
|
|
|
73
81
|
from .mcp_router.server import create_mcp_server
|
|
74
82
|
from ..settings import settings
|
|
75
83
|
|
|
84
|
+
# Configure loguru based on settings
|
|
85
|
+
# Remove default handler and add one with configured level
|
|
86
|
+
logger.remove()
|
|
87
|
+
|
|
88
|
+
# Configure level icons - only warnings and errors get visual indicators
|
|
89
|
+
logger.level("DEBUG", icon=" ")
|
|
90
|
+
logger.level("INFO", icon=" ")
|
|
91
|
+
logger.level("WARNING", icon="🟠")
|
|
92
|
+
logger.level("ERROR", icon="🔴")
|
|
93
|
+
logger.level("CRITICAL", icon="🔴")
|
|
94
|
+
|
|
95
|
+
logger.add(
|
|
96
|
+
sys.stderr,
|
|
97
|
+
level=settings.api.log_level.upper(),
|
|
98
|
+
format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | {level.icon} <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
|
99
|
+
)
|
|
100
|
+
|
|
76
101
|
|
|
77
102
|
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
78
103
|
"""
|
|
@@ -82,26 +107,64 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
82
107
|
- Logs request method, path, client, user-agent
|
|
83
108
|
- Logs response status, content-type, duration
|
|
84
109
|
- Essential for debugging OAuth flow and MCP sessions
|
|
110
|
+
- Health checks and 404s logged at DEBUG level to reduce noise
|
|
111
|
+
- Scanner/exploit attempts (common vulnerability probes) logged at DEBUG
|
|
85
112
|
"""
|
|
86
113
|
|
|
114
|
+
# Paths to log at DEBUG level (health checks, probes)
|
|
115
|
+
DEBUG_PATHS = {"/health", "/healthz", "/ready", "/readyz", "/livez"}
|
|
116
|
+
|
|
117
|
+
# Path patterns that indicate vulnerability scanners (log at DEBUG)
|
|
118
|
+
SCANNER_PATTERNS = (
|
|
119
|
+
"/vendor/", # PHP composer exploits
|
|
120
|
+
"/.git/", # Git config exposure
|
|
121
|
+
"/.env", # Environment file exposure
|
|
122
|
+
"/wp-", # WordPress exploits
|
|
123
|
+
"/phpunit/", # PHPUnit RCE
|
|
124
|
+
"/eval-stdin", # PHP eval exploits
|
|
125
|
+
"/console/", # Console exposure
|
|
126
|
+
"/actuator/", # Spring Boot actuator
|
|
127
|
+
"/debug/", # Debug endpoints
|
|
128
|
+
"/admin/", # Admin panel probes (when we don't have one)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _should_log_at_debug(self, path: str, status_code: int) -> bool:
|
|
132
|
+
"""Determine if request should be logged at DEBUG level."""
|
|
133
|
+
# Health checks
|
|
134
|
+
if path in self.DEBUG_PATHS:
|
|
135
|
+
return True
|
|
136
|
+
# 404 responses (not found - includes scanner probes)
|
|
137
|
+
if status_code == 404:
|
|
138
|
+
return True
|
|
139
|
+
# Known scanner patterns
|
|
140
|
+
if any(pattern in path for pattern in self.SCANNER_PATTERNS):
|
|
141
|
+
return True
|
|
142
|
+
return False
|
|
143
|
+
|
|
87
144
|
async def dispatch(self, request: Request, call_next):
|
|
88
145
|
start_time = time.time()
|
|
146
|
+
path = request.url.path
|
|
89
147
|
|
|
90
|
-
# Log incoming request
|
|
148
|
+
# Log incoming request (preliminary - may adjust after response)
|
|
91
149
|
client_host = request.client.host if request.client else "unknown"
|
|
92
|
-
|
|
93
|
-
f"→ REQUEST: {request.method} {request.url.path} | "
|
|
94
|
-
f"Client: {client_host} | "
|
|
95
|
-
f"User-Agent: {request.headers.get('user-agent', 'unknown')[:100]}"
|
|
96
|
-
)
|
|
150
|
+
user_agent = request.headers.get('user-agent', 'unknown')[:100]
|
|
97
151
|
|
|
98
152
|
# Process request
|
|
99
153
|
response = await call_next(request)
|
|
100
154
|
|
|
101
|
-
#
|
|
155
|
+
# Determine log level based on path AND response status
|
|
102
156
|
duration_ms = (time.time() - start_time) * 1000
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
use_debug = self._should_log_at_debug(path, response.status_code)
|
|
158
|
+
log_fn = logger.debug if use_debug else logger.info
|
|
159
|
+
|
|
160
|
+
# Log request and response together
|
|
161
|
+
log_fn(
|
|
162
|
+
f"→ REQUEST: {request.method} {path} | "
|
|
163
|
+
f"Client: {client_host} | "
|
|
164
|
+
f"User-Agent: {user_agent}"
|
|
165
|
+
)
|
|
166
|
+
log_fn(
|
|
167
|
+
f"← RESPONSE: {request.method} {path} | "
|
|
105
168
|
f"Status: {response.status_code} | "
|
|
106
169
|
f"Duration: {duration_ms:.2f}ms"
|
|
107
170
|
)
|
|
@@ -154,7 +217,8 @@ async def lifespan(app: FastAPI):
|
|
|
154
217
|
"and history lookups are unavailable. Enable database with POSTGRES__ENABLED=true"
|
|
155
218
|
)
|
|
156
219
|
else:
|
|
157
|
-
|
|
220
|
+
# Log database host only - never log credentials
|
|
221
|
+
logger.info(f"Database enabled: {settings.postgres.host}:{settings.postgres.port}/{settings.postgres.database}")
|
|
158
222
|
|
|
159
223
|
yield
|
|
160
224
|
|
|
@@ -214,15 +278,42 @@ def create_app() -> FastAPI:
|
|
|
214
278
|
yield
|
|
215
279
|
|
|
216
280
|
app = FastAPI(
|
|
217
|
-
title="
|
|
218
|
-
description="Resources Entities Moments system for agentic AI",
|
|
219
|
-
version=
|
|
281
|
+
title=f"{settings.app_name} API",
|
|
282
|
+
description=f"{settings.app_name} - Resources Entities Moments system for agentic AI",
|
|
283
|
+
version=__version__,
|
|
220
284
|
lifespan=combined_lifespan,
|
|
221
285
|
root_path=settings.root_path if settings.root_path else "",
|
|
222
286
|
redirect_slashes=False, # Don't redirect /mcp/ -> /mcp
|
|
223
287
|
)
|
|
224
288
|
|
|
289
|
+
# Add request logging middleware
|
|
290
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
291
|
+
|
|
292
|
+
# Add SSE buffering middleware (for MCP SSE transport)
|
|
293
|
+
app.add_middleware(SSEBufferingMiddleware)
|
|
294
|
+
|
|
295
|
+
# Add Anonymous Tracking & Rate Limiting (Runs AFTER Auth if Auth is enabled)
|
|
296
|
+
# Must be added BEFORE AuthMiddleware in code to be INNER in the stack
|
|
297
|
+
from .middleware.tracking import AnonymousTrackingMiddleware
|
|
298
|
+
app.add_middleware(AnonymousTrackingMiddleware)
|
|
299
|
+
|
|
300
|
+
# Add authentication middleware
|
|
301
|
+
# Always load middleware for dev token support, but allow anonymous when auth disabled
|
|
302
|
+
from ..auth.middleware import AuthMiddleware
|
|
303
|
+
|
|
304
|
+
app.add_middleware(
|
|
305
|
+
AuthMiddleware,
|
|
306
|
+
protected_paths=["/api/v1"],
|
|
307
|
+
excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth"],
|
|
308
|
+
# Allow anonymous when auth is disabled, otherwise use setting
|
|
309
|
+
allow_anonymous=(not settings.auth.enabled) or settings.auth.allow_anonymous,
|
|
310
|
+
# MCP requires auth only when auth is fully enabled
|
|
311
|
+
mcp_requires_auth=settings.auth.enabled and settings.auth.mcp_requires_auth,
|
|
312
|
+
)
|
|
313
|
+
|
|
225
314
|
# Add session middleware for OAuth state management
|
|
315
|
+
# Must be added AFTER AuthMiddleware in code so it runs BEFORE (middleware runs in reverse)
|
|
316
|
+
# AuthMiddleware needs request.session to be available
|
|
226
317
|
session_secret = settings.auth.session_secret or secrets.token_hex(32)
|
|
227
318
|
if not settings.auth.session_secret:
|
|
228
319
|
logger.warning(
|
|
@@ -239,32 +330,12 @@ def create_app() -> FastAPI:
|
|
|
239
330
|
https_only=settings.environment == "production",
|
|
240
331
|
)
|
|
241
332
|
|
|
242
|
-
# Add request logging middleware
|
|
243
|
-
app.add_middleware(RequestLoggingMiddleware)
|
|
244
|
-
|
|
245
|
-
# Add SSE buffering middleware (for MCP SSE transport)
|
|
246
|
-
app.add_middleware(SSEBufferingMiddleware)
|
|
247
|
-
|
|
248
|
-
# Add Anonymous Tracking & Rate Limiting (Runs AFTER Auth if Auth is enabled)
|
|
249
|
-
# Must be added BEFORE AuthMiddleware in code to be INNER in the stack
|
|
250
|
-
from .middleware.tracking import AnonymousTrackingMiddleware
|
|
251
|
-
app.add_middleware(AnonymousTrackingMiddleware)
|
|
252
|
-
|
|
253
|
-
# Add authentication middleware (if enabled)
|
|
254
|
-
if settings.auth.enabled:
|
|
255
|
-
from ..auth.middleware import AuthMiddleware
|
|
256
|
-
|
|
257
|
-
app.add_middleware(
|
|
258
|
-
AuthMiddleware,
|
|
259
|
-
protected_paths=["/api/v1"],
|
|
260
|
-
excluded_paths=["/api/auth", "/api/v1/mcp/auth"],
|
|
261
|
-
)
|
|
262
|
-
|
|
263
333
|
# Add CORS middleware LAST (runs first in middleware chain)
|
|
264
334
|
# Must expose mcp-session-id header for MCP session management
|
|
265
335
|
CORS_ORIGIN_WHITELIST = [
|
|
266
|
-
"http://localhost:5173", # Local development (Vite)
|
|
267
336
|
"http://localhost:3000", # Local development (React)
|
|
337
|
+
"http://localhost:5000", # Local development (Flask/other)
|
|
338
|
+
"http://localhost:5173", # Local development (Vite)
|
|
268
339
|
]
|
|
269
340
|
|
|
270
341
|
app.add_middleware(
|
|
@@ -282,8 +353,8 @@ def create_app() -> FastAPI:
|
|
|
282
353
|
"""API information endpoint."""
|
|
283
354
|
# TODO: If auth enabled and no user, return 401 with WWW-Authenticate
|
|
284
355
|
return {
|
|
285
|
-
"name": "
|
|
286
|
-
"version":
|
|
356
|
+
"name": f"{settings.app_name} API",
|
|
357
|
+
"version": __version__,
|
|
287
358
|
"mcp_endpoint": "/api/v1/mcp",
|
|
288
359
|
"docs": "/docs",
|
|
289
360
|
}
|
|
@@ -292,12 +363,27 @@ def create_app() -> FastAPI:
|
|
|
292
363
|
@app.get("/health")
|
|
293
364
|
async def health():
|
|
294
365
|
"""Health check endpoint."""
|
|
295
|
-
return {"status": "healthy", "version":
|
|
366
|
+
return {"status": "healthy", "version": __version__}
|
|
296
367
|
|
|
297
368
|
# Register API routers
|
|
298
369
|
from .routers.chat import router as chat_router
|
|
370
|
+
from .routers.models import router as models_router
|
|
371
|
+
from .routers.messages import router as messages_router
|
|
372
|
+
from .routers.feedback import router as feedback_router
|
|
373
|
+
from .routers.admin import router as admin_router
|
|
374
|
+
from .routers.shared_sessions import router as shared_sessions_router
|
|
375
|
+
from .routers.query import router as query_router
|
|
299
376
|
|
|
300
377
|
app.include_router(chat_router)
|
|
378
|
+
app.include_router(models_router)
|
|
379
|
+
# shared_sessions_router MUST be before messages_router
|
|
380
|
+
# because messages_router has /sessions/{session_id} which would match
|
|
381
|
+
# before the more specific /sessions/shared-with-me routes
|
|
382
|
+
app.include_router(shared_sessions_router)
|
|
383
|
+
app.include_router(messages_router)
|
|
384
|
+
app.include_router(feedback_router)
|
|
385
|
+
app.include_router(admin_router)
|
|
386
|
+
app.include_router(query_router)
|
|
301
387
|
|
|
302
388
|
# Register auth router (if enabled)
|
|
303
389
|
if settings.auth.enabled:
|
|
@@ -305,6 +391,12 @@ def create_app() -> FastAPI:
|
|
|
305
391
|
|
|
306
392
|
app.include_router(auth_router)
|
|
307
393
|
|
|
394
|
+
# Register dev router (non-production only)
|
|
395
|
+
if settings.environment != "production":
|
|
396
|
+
from .routers.dev import router as dev_router
|
|
397
|
+
|
|
398
|
+
app.include_router(dev_router)
|
|
399
|
+
|
|
308
400
|
# TODO: Register additional routers
|
|
309
401
|
# from .routers.query import router as query_router
|
|
310
402
|
# from .routers.resources import router as resources_router
|
rem/api/mcp_router/resources.py
CHANGED
|
@@ -181,7 +181,7 @@ Parameters:
|
|
|
181
181
|
- table_name (required): Table to search (resources, moments, etc.)
|
|
182
182
|
- field_name (optional): Field to search (defaults to "content")
|
|
183
183
|
- provider (optional): Embedding provider (default: from LLM__EMBEDDING_PROVIDER setting)
|
|
184
|
-
- min_similarity (optional): Minimum similarity 0.0-1.0 (default: 0.
|
|
184
|
+
- min_similarity (optional): Minimum similarity 0.0-1.0 (default: 0.3)
|
|
185
185
|
- limit (optional): Max results (default: 10)
|
|
186
186
|
- user_id (optional): User scoping
|
|
187
187
|
|
rem/api/mcp_router/server.py
CHANGED
|
@@ -19,10 +19,18 @@ FastMCP Features:
|
|
|
19
19
|
- Built-in auth that can be disabled for testing
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
+
import importlib.metadata
|
|
23
|
+
|
|
22
24
|
from fastmcp import FastMCP
|
|
23
25
|
|
|
24
26
|
from ...settings import settings
|
|
25
27
|
|
|
28
|
+
# Get package version
|
|
29
|
+
try:
|
|
30
|
+
__version__ = importlib.metadata.version("remdb")
|
|
31
|
+
except importlib.metadata.PackageNotFoundError:
|
|
32
|
+
__version__ = "0.0.0-dev"
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
28
36
|
"""
|
|
@@ -52,7 +60,7 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
52
60
|
"""
|
|
53
61
|
mcp = FastMCP(
|
|
54
62
|
name=f"REM MCP Server ({settings.team}/{settings.environment})",
|
|
55
|
-
version=
|
|
63
|
+
version=__version__,
|
|
56
64
|
instructions=(
|
|
57
65
|
"REM (Resource-Entity-Moment) MCP Server - Unified memory infrastructure for agentic systems.\n\n"
|
|
58
66
|
"═══════════════════════════════════════════════════════════════════════════\n"
|
|
@@ -119,10 +127,12 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
119
127
|
"AVAILABLE TOOLS\n"
|
|
120
128
|
"═══════════════════════════════════════════════════════════════════════════\n"
|
|
121
129
|
"\n"
|
|
122
|
-
"•
|
|
123
|
-
"•
|
|
130
|
+
"• search_rem - Execute REM queries (LOOKUP, FUZZY, SEARCH, SQL, TRAVERSE)\n"
|
|
131
|
+
"• ask_rem_agent - Natural language to REM query conversion\n"
|
|
124
132
|
" - plan_mode=True: Hints agent to use TRAVERSE with depth=0 for edge analysis\n"
|
|
125
|
-
"•
|
|
133
|
+
"• ingest_into_rem - Ingest files from local paths (local server only), s3://, or https://\n"
|
|
134
|
+
"• list_schema - List all database schemas (tables) with row counts\n"
|
|
135
|
+
"• get_schema - Get detailed schema for a specific table (columns, types, indexes)\n"
|
|
126
136
|
"\n"
|
|
127
137
|
"═══════════════════════════════════════════════════════════════════════════\n"
|
|
128
138
|
"AVAILABLE RESOURCES (Read-Only)\n"
|
|
@@ -165,11 +175,22 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
165
175
|
)
|
|
166
176
|
|
|
167
177
|
# Register REM tools
|
|
168
|
-
from .tools import
|
|
178
|
+
from .tools import (
|
|
179
|
+
ask_rem_agent,
|
|
180
|
+
get_schema,
|
|
181
|
+
ingest_into_rem,
|
|
182
|
+
list_schema,
|
|
183
|
+
read_resource,
|
|
184
|
+
register_metadata,
|
|
185
|
+
search_rem,
|
|
186
|
+
)
|
|
169
187
|
|
|
170
188
|
mcp.tool()(search_rem)
|
|
171
189
|
mcp.tool()(ask_rem_agent)
|
|
172
190
|
mcp.tool()(read_resource)
|
|
191
|
+
mcp.tool()(register_metadata)
|
|
192
|
+
mcp.tool()(list_schema)
|
|
193
|
+
mcp.tool()(get_schema)
|
|
173
194
|
|
|
174
195
|
# File ingestion tool (with local path support for local servers)
|
|
175
196
|
# Wrap to inject is_local parameter
|