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/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
|
|
|
@@ -163,7 +227,22 @@ async def lifespan(app: FastAPI):
|
|
|
163
227
|
|
|
164
228
|
def create_app() -> FastAPI:
|
|
165
229
|
"""
|
|
166
|
-
Create and configure the FastAPI application.
|
|
230
|
+
Create and configure the FastAPI application with MCP server.
|
|
231
|
+
|
|
232
|
+
The returned app exposes `app.mcp_server` (FastMCP instance) for adding
|
|
233
|
+
custom tools, resources, and prompts:
|
|
234
|
+
|
|
235
|
+
app = create_app()
|
|
236
|
+
|
|
237
|
+
@app.mcp_server.tool()
|
|
238
|
+
async def my_tool(query: str) -> dict:
|
|
239
|
+
'''Custom MCP tool.'''
|
|
240
|
+
return {"result": query}
|
|
241
|
+
|
|
242
|
+
@app.mcp_server.resource("custom://data")
|
|
243
|
+
async def my_resource() -> str:
|
|
244
|
+
'''Custom resource.'''
|
|
245
|
+
return '{"data": "value"}'
|
|
167
246
|
|
|
168
247
|
Design Pattern:
|
|
169
248
|
1. Create MCP server
|
|
@@ -174,9 +253,10 @@ def create_app() -> FastAPI:
|
|
|
174
253
|
6. Define health endpoints
|
|
175
254
|
7. Register API routers
|
|
176
255
|
8. Mount MCP app
|
|
256
|
+
9. Expose mcp_server on app for extension
|
|
177
257
|
|
|
178
258
|
Returns:
|
|
179
|
-
Configured FastAPI application
|
|
259
|
+
Configured FastAPI application with .mcp_server attribute
|
|
180
260
|
"""
|
|
181
261
|
# Create MCP server and get HTTP app
|
|
182
262
|
# path="/" creates routes at root, then mount at /api/v1/mcp
|
|
@@ -198,15 +278,42 @@ def create_app() -> FastAPI:
|
|
|
198
278
|
yield
|
|
199
279
|
|
|
200
280
|
app = FastAPI(
|
|
201
|
-
title="
|
|
202
|
-
description="Resources Entities Moments system for agentic AI",
|
|
203
|
-
version=
|
|
281
|
+
title=f"{settings.app_name} API",
|
|
282
|
+
description=f"{settings.app_name} - Resources Entities Moments system for agentic AI",
|
|
283
|
+
version=__version__,
|
|
204
284
|
lifespan=combined_lifespan,
|
|
205
285
|
root_path=settings.root_path if settings.root_path else "",
|
|
206
286
|
redirect_slashes=False, # Don't redirect /mcp/ -> /mcp
|
|
207
287
|
)
|
|
208
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
|
+
|
|
209
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
|
|
210
317
|
session_secret = settings.auth.session_secret or secrets.token_hex(32)
|
|
211
318
|
if not settings.auth.session_secret:
|
|
212
319
|
logger.warning(
|
|
@@ -223,27 +330,12 @@ def create_app() -> FastAPI:
|
|
|
223
330
|
https_only=settings.environment == "production",
|
|
224
331
|
)
|
|
225
332
|
|
|
226
|
-
# Add request logging middleware
|
|
227
|
-
app.add_middleware(RequestLoggingMiddleware)
|
|
228
|
-
|
|
229
|
-
# Add SSE buffering middleware (for MCP SSE transport)
|
|
230
|
-
app.add_middleware(SSEBufferingMiddleware)
|
|
231
|
-
|
|
232
|
-
# Add authentication middleware (if enabled)
|
|
233
|
-
if settings.auth.enabled:
|
|
234
|
-
from ..auth.middleware import AuthMiddleware
|
|
235
|
-
|
|
236
|
-
app.add_middleware(
|
|
237
|
-
AuthMiddleware,
|
|
238
|
-
protected_paths=["/api/v1"],
|
|
239
|
-
excluded_paths=["/api/auth", "/api/v1/mcp/auth"],
|
|
240
|
-
)
|
|
241
|
-
|
|
242
333
|
# Add CORS middleware LAST (runs first in middleware chain)
|
|
243
334
|
# Must expose mcp-session-id header for MCP session management
|
|
244
335
|
CORS_ORIGIN_WHITELIST = [
|
|
245
|
-
"http://localhost:5173", # Local development (Vite)
|
|
246
336
|
"http://localhost:3000", # Local development (React)
|
|
337
|
+
"http://localhost:5000", # Local development (Flask/other)
|
|
338
|
+
"http://localhost:5173", # Local development (Vite)
|
|
247
339
|
]
|
|
248
340
|
|
|
249
341
|
app.add_middleware(
|
|
@@ -261,8 +353,8 @@ def create_app() -> FastAPI:
|
|
|
261
353
|
"""API information endpoint."""
|
|
262
354
|
# TODO: If auth enabled and no user, return 401 with WWW-Authenticate
|
|
263
355
|
return {
|
|
264
|
-
"name": "
|
|
265
|
-
"version":
|
|
356
|
+
"name": f"{settings.app_name} API",
|
|
357
|
+
"version": __version__,
|
|
266
358
|
"mcp_endpoint": "/api/v1/mcp",
|
|
267
359
|
"docs": "/docs",
|
|
268
360
|
}
|
|
@@ -271,12 +363,24 @@ def create_app() -> FastAPI:
|
|
|
271
363
|
@app.get("/health")
|
|
272
364
|
async def health():
|
|
273
365
|
"""Health check endpoint."""
|
|
274
|
-
return {"status": "healthy", "version":
|
|
366
|
+
return {"status": "healthy", "version": __version__}
|
|
275
367
|
|
|
276
368
|
# Register API routers
|
|
277
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
|
|
278
376
|
|
|
279
377
|
app.include_router(chat_router)
|
|
378
|
+
app.include_router(models_router)
|
|
379
|
+
app.include_router(messages_router)
|
|
380
|
+
app.include_router(feedback_router)
|
|
381
|
+
app.include_router(admin_router)
|
|
382
|
+
app.include_router(shared_sessions_router)
|
|
383
|
+
app.include_router(query_router)
|
|
280
384
|
|
|
281
385
|
# Register auth router (if enabled)
|
|
282
386
|
if settings.auth.enabled:
|
|
@@ -284,6 +388,12 @@ def create_app() -> FastAPI:
|
|
|
284
388
|
|
|
285
389
|
app.include_router(auth_router)
|
|
286
390
|
|
|
391
|
+
# Register dev router (non-production only)
|
|
392
|
+
if settings.environment != "production":
|
|
393
|
+
from .routers.dev import router as dev_router
|
|
394
|
+
|
|
395
|
+
app.include_router(dev_router)
|
|
396
|
+
|
|
287
397
|
# TODO: Register additional routers
|
|
288
398
|
# from .routers.query import router as query_router
|
|
289
399
|
# from .routers.resources import router as resources_router
|
|
@@ -305,6 +415,10 @@ def create_app() -> FastAPI:
|
|
|
305
415
|
# Mount MCP app at /api/v1/mcp
|
|
306
416
|
app.mount("/api/v1/mcp", mcp_app)
|
|
307
417
|
|
|
418
|
+
# Expose MCP server on app for extension
|
|
419
|
+
# Users can add tools/resources/prompts via app.mcp_server
|
|
420
|
+
app.mcp_server = mcp_server # type: ignore[attr-defined]
|
|
421
|
+
|
|
308
422
|
return app
|
|
309
423
|
|
|
310
424
|
|
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"
|
|
@@ -165,11 +173,18 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
|
|
|
165
173
|
)
|
|
166
174
|
|
|
167
175
|
# Register REM tools
|
|
168
|
-
from .tools import
|
|
176
|
+
from .tools import (
|
|
177
|
+
ask_rem_agent,
|
|
178
|
+
ingest_into_rem,
|
|
179
|
+
read_resource,
|
|
180
|
+
register_metadata,
|
|
181
|
+
search_rem,
|
|
182
|
+
)
|
|
169
183
|
|
|
170
184
|
mcp.tool()(search_rem)
|
|
171
185
|
mcp.tool()(ask_rem_agent)
|
|
172
186
|
mcp.tool()(read_resource)
|
|
187
|
+
mcp.tool()(register_metadata)
|
|
173
188
|
|
|
174
189
|
# File ingestion tool (with local path support for local servers)
|
|
175
190
|
# Wrap to inject is_local parameter
|
rem/api/mcp_router/tools.py
CHANGED
|
@@ -53,7 +53,7 @@ def init_services(postgres_service: PostgresService, rem_service: RemService):
|
|
|
53
53
|
"""
|
|
54
54
|
_service_cache["postgres"] = postgres_service
|
|
55
55
|
_service_cache["rem"] = rem_service
|
|
56
|
-
logger.
|
|
56
|
+
logger.debug("MCP tools initialized with service instances")
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
async def get_rem_service() -> RemService:
|
|
@@ -79,7 +79,7 @@ async def get_rem_service() -> RemService:
|
|
|
79
79
|
_service_cache["postgres"] = postgres_service
|
|
80
80
|
_service_cache["rem"] = rem_service
|
|
81
81
|
|
|
82
|
-
logger.
|
|
82
|
+
logger.debug("MCP tools: lazy initialized services")
|
|
83
83
|
return rem_service
|
|
84
84
|
|
|
85
85
|
|
|
@@ -399,14 +399,14 @@ async def ask_rem_agent(
|
|
|
399
399
|
)
|
|
400
400
|
|
|
401
401
|
# Run agent (errors handled by decorator)
|
|
402
|
-
logger.
|
|
402
|
+
logger.debug(f"Running ask_rem agent for query: {query[:100]}...")
|
|
403
403
|
result = await agent_runtime.run(query)
|
|
404
404
|
|
|
405
405
|
# Extract output
|
|
406
406
|
from rem.agentic.serialization import serialize_agent_result
|
|
407
407
|
query_output = serialize_agent_result(result.output)
|
|
408
408
|
|
|
409
|
-
logger.
|
|
409
|
+
logger.debug("Agent execution completed successfully")
|
|
410
410
|
|
|
411
411
|
return {
|
|
412
412
|
"response": str(result.output),
|
|
@@ -422,6 +422,7 @@ async def ingest_into_rem(
|
|
|
422
422
|
tags: list[str] | None = None,
|
|
423
423
|
is_local_server: bool = False,
|
|
424
424
|
user_id: str | None = None,
|
|
425
|
+
resource_type: str | None = None,
|
|
425
426
|
) -> dict[str, Any]:
|
|
426
427
|
"""
|
|
427
428
|
Ingest file into REM, creating searchable resources and embeddings.
|
|
@@ -448,6 +449,11 @@ async def ingest_into_rem(
|
|
|
448
449
|
tags: Optional tags for file
|
|
449
450
|
is_local_server: True if running as local/stdio MCP server
|
|
450
451
|
user_id: Optional user identifier (defaults to authenticated user or "default")
|
|
452
|
+
resource_type: Optional resource type for storing chunks (case-insensitive).
|
|
453
|
+
Supports flexible naming:
|
|
454
|
+
- "resource", "resources", "Resource" → Resource (default)
|
|
455
|
+
- "domain-resource", "domain_resource", "DomainResource",
|
|
456
|
+
"domain-resources" → DomainResource (curated internal knowledge)
|
|
451
457
|
|
|
452
458
|
Returns:
|
|
453
459
|
Dict with:
|
|
@@ -478,6 +484,13 @@ async def ingest_into_rem(
|
|
|
478
484
|
file_uri="https://example.com/whitepaper.pdf",
|
|
479
485
|
tags=["research", "whitepaper"]
|
|
480
486
|
)
|
|
487
|
+
|
|
488
|
+
# Ingest as curated domain knowledge
|
|
489
|
+
ingest_into_rem(
|
|
490
|
+
file_uri="s3://bucket/internal/procedures.pdf",
|
|
491
|
+
resource_type="domain-resource",
|
|
492
|
+
category="procedures"
|
|
493
|
+
)
|
|
481
494
|
"""
|
|
482
495
|
from ...services.content import ContentService
|
|
483
496
|
|
|
@@ -493,9 +506,10 @@ async def ingest_into_rem(
|
|
|
493
506
|
category=category,
|
|
494
507
|
tags=tags,
|
|
495
508
|
is_local_server=is_local_server,
|
|
509
|
+
resource_type=resource_type,
|
|
496
510
|
)
|
|
497
511
|
|
|
498
|
-
logger.
|
|
512
|
+
logger.debug(
|
|
499
513
|
f"MCP ingestion complete: {result['file_name']} "
|
|
500
514
|
f"(status: {result['processing_status']}, "
|
|
501
515
|
f"resources: {result['resources_created']})"
|
|
@@ -550,7 +564,7 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
550
564
|
# Check system status
|
|
551
565
|
read_resource(uri="rem://status")
|
|
552
566
|
"""
|
|
553
|
-
logger.
|
|
567
|
+
logger.debug(f"Reading resource: {uri}")
|
|
554
568
|
|
|
555
569
|
# Import here to avoid circular dependency
|
|
556
570
|
from .resources import load_resource
|
|
@@ -558,7 +572,7 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
558
572
|
# Load resource using the existing resource handler (errors handled by decorator)
|
|
559
573
|
result = await load_resource(uri)
|
|
560
574
|
|
|
561
|
-
logger.
|
|
575
|
+
logger.debug(f"Resource loaded successfully: {uri}")
|
|
562
576
|
|
|
563
577
|
# If result is already a dict, return it
|
|
564
578
|
if isinstance(result, dict):
|
|
@@ -582,3 +596,125 @@ async def read_resource(uri: str) -> dict[str, Any]:
|
|
|
582
596
|
"uri": uri,
|
|
583
597
|
"data": {"content": result},
|
|
584
598
|
}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
async def register_metadata(
|
|
602
|
+
confidence: float | None = None,
|
|
603
|
+
references: list[str] | None = None,
|
|
604
|
+
sources: list[str] | None = None,
|
|
605
|
+
flags: list[str] | None = None,
|
|
606
|
+
# Risk assessment fields (used by mental health agents like Siggy)
|
|
607
|
+
risk_level: str | None = None,
|
|
608
|
+
risk_score: int | None = None,
|
|
609
|
+
risk_reasoning: str | None = None,
|
|
610
|
+
recommended_action: str | None = None,
|
|
611
|
+
# Generic extension - any additional key-value pairs
|
|
612
|
+
extra: dict[str, Any] | None = None,
|
|
613
|
+
) -> dict[str, Any]:
|
|
614
|
+
"""
|
|
615
|
+
Register response metadata to be emitted as an SSE MetadataEvent.
|
|
616
|
+
|
|
617
|
+
Call this tool BEFORE generating your final response to provide structured
|
|
618
|
+
metadata that will be sent to the client alongside your natural language output.
|
|
619
|
+
This allows you to stream conversational responses while still providing
|
|
620
|
+
machine-readable confidence scores, references, and other metadata.
|
|
621
|
+
|
|
622
|
+
**Design Pattern**: Agents can call this once before their final response to
|
|
623
|
+
register metadata that the streaming layer will emit as a MetadataEvent.
|
|
624
|
+
This decouples structured metadata from the response format.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
confidence: Confidence score (0.0-1.0) for the response quality.
|
|
628
|
+
- 0.9-1.0: High confidence, answer is well-supported
|
|
629
|
+
- 0.7-0.9: Medium confidence, some uncertainty
|
|
630
|
+
- 0.5-0.7: Low confidence, significant gaps
|
|
631
|
+
- <0.5: Very uncertain, may need clarification
|
|
632
|
+
references: List of reference identifiers (file paths, document IDs,
|
|
633
|
+
entity labels) that support the response.
|
|
634
|
+
sources: List of source descriptions (e.g., "REM database",
|
|
635
|
+
"search results", "user context").
|
|
636
|
+
flags: Optional flags for the response (e.g., "needs_review",
|
|
637
|
+
"uncertain", "incomplete", "crisis_alert").
|
|
638
|
+
|
|
639
|
+
risk_level: Risk level indicator (e.g., "green", "orange", "red").
|
|
640
|
+
Used by mental health agents for C-SSRS style assessment.
|
|
641
|
+
risk_score: Numeric risk score (e.g., 0-6 for C-SSRS).
|
|
642
|
+
risk_reasoning: Brief explanation of risk assessment.
|
|
643
|
+
recommended_action: Suggested next steps based on assessment.
|
|
644
|
+
|
|
645
|
+
extra: Dict of arbitrary additional metadata. Use this for any
|
|
646
|
+
domain-specific fields not covered by the standard parameters.
|
|
647
|
+
Example: {"topics_detected": ["anxiety", "sleep"], "session_count": 5}
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Dict with:
|
|
651
|
+
- status: "success"
|
|
652
|
+
- _metadata_event: True (marker for streaming layer)
|
|
653
|
+
- All provided fields merged into response
|
|
654
|
+
|
|
655
|
+
Examples:
|
|
656
|
+
# High confidence answer with references
|
|
657
|
+
register_metadata(
|
|
658
|
+
confidence=0.95,
|
|
659
|
+
references=["sarah-chen", "q3-report-2024"],
|
|
660
|
+
sources=["REM database lookup"]
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# Mental health risk assessment (Siggy-style)
|
|
664
|
+
register_metadata(
|
|
665
|
+
confidence=0.9,
|
|
666
|
+
risk_level="green",
|
|
667
|
+
risk_score=0,
|
|
668
|
+
risk_reasoning="No risk indicators detected in message",
|
|
669
|
+
sources=["mental_health_resources"]
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
# Orange risk with recommended action
|
|
673
|
+
register_metadata(
|
|
674
|
+
risk_level="orange",
|
|
675
|
+
risk_score=2,
|
|
676
|
+
risk_reasoning="Passive ideation detected - 'feeling hopeless'",
|
|
677
|
+
recommended_action="Schedule care team check-in within 24-48 hours",
|
|
678
|
+
flags=["care_team_alert"]
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Custom domain-specific metadata
|
|
682
|
+
register_metadata(
|
|
683
|
+
confidence=0.8,
|
|
684
|
+
extra={
|
|
685
|
+
"topics_detected": ["medication", "side_effects"],
|
|
686
|
+
"drug_mentioned": "sertraline",
|
|
687
|
+
"sentiment": "concerned"
|
|
688
|
+
}
|
|
689
|
+
)
|
|
690
|
+
"""
|
|
691
|
+
logger.debug(
|
|
692
|
+
f"Registering metadata: confidence={confidence}, "
|
|
693
|
+
f"risk_level={risk_level}, refs={len(references or [])}, "
|
|
694
|
+
f"sources={len(sources or [])}"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
result = {
|
|
698
|
+
"status": "success",
|
|
699
|
+
"_metadata_event": True, # Marker for streaming layer
|
|
700
|
+
"confidence": confidence,
|
|
701
|
+
"references": references,
|
|
702
|
+
"sources": sources,
|
|
703
|
+
"flags": flags,
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
# Add risk assessment fields if provided
|
|
707
|
+
if risk_level is not None:
|
|
708
|
+
result["risk_level"] = risk_level
|
|
709
|
+
if risk_score is not None:
|
|
710
|
+
result["risk_score"] = risk_score
|
|
711
|
+
if risk_reasoning is not None:
|
|
712
|
+
result["risk_reasoning"] = risk_reasoning
|
|
713
|
+
if recommended_action is not None:
|
|
714
|
+
result["recommended_action"] = recommended_action
|
|
715
|
+
|
|
716
|
+
# Merge any extra fields
|
|
717
|
+
if extra:
|
|
718
|
+
result["extra"] = extra
|
|
719
|
+
|
|
720
|
+
return result
|