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.

Files changed (98) hide show
  1. rem/__init__.py +129 -2
  2. rem/agentic/README.md +76 -0
  3. rem/agentic/__init__.py +15 -0
  4. rem/agentic/agents/__init__.py +16 -2
  5. rem/agentic/agents/sse_simulator.py +500 -0
  6. rem/agentic/context.py +28 -22
  7. rem/agentic/llm_provider_models.py +301 -0
  8. rem/agentic/otel/setup.py +92 -4
  9. rem/agentic/providers/phoenix.py +32 -43
  10. rem/agentic/providers/pydantic_ai.py +142 -22
  11. rem/agentic/schema.py +358 -21
  12. rem/agentic/tools/rem_tools.py +3 -3
  13. rem/api/README.md +238 -1
  14. rem/api/deps.py +255 -0
  15. rem/api/main.py +151 -37
  16. rem/api/mcp_router/resources.py +1 -1
  17. rem/api/mcp_router/server.py +17 -2
  18. rem/api/mcp_router/tools.py +143 -7
  19. rem/api/middleware/tracking.py +172 -0
  20. rem/api/routers/admin.py +277 -0
  21. rem/api/routers/auth.py +124 -0
  22. rem/api/routers/chat/completions.py +152 -16
  23. rem/api/routers/chat/models.py +7 -3
  24. rem/api/routers/chat/sse_events.py +526 -0
  25. rem/api/routers/chat/streaming.py +608 -45
  26. rem/api/routers/dev.py +81 -0
  27. rem/api/routers/feedback.py +148 -0
  28. rem/api/routers/messages.py +473 -0
  29. rem/api/routers/models.py +78 -0
  30. rem/api/routers/query.py +357 -0
  31. rem/api/routers/shared_sessions.py +406 -0
  32. rem/auth/middleware.py +126 -27
  33. rem/cli/commands/README.md +201 -70
  34. rem/cli/commands/ask.py +13 -10
  35. rem/cli/commands/cluster.py +1359 -0
  36. rem/cli/commands/configure.py +4 -3
  37. rem/cli/commands/db.py +350 -137
  38. rem/cli/commands/experiments.py +76 -72
  39. rem/cli/commands/process.py +22 -15
  40. rem/cli/commands/scaffold.py +47 -0
  41. rem/cli/commands/schema.py +95 -49
  42. rem/cli/main.py +29 -6
  43. rem/config.py +2 -2
  44. rem/models/core/core_model.py +7 -1
  45. rem/models/core/rem_query.py +5 -2
  46. rem/models/entities/__init__.py +21 -0
  47. rem/models/entities/domain_resource.py +38 -0
  48. rem/models/entities/feedback.py +123 -0
  49. rem/models/entities/message.py +30 -1
  50. rem/models/entities/session.py +83 -0
  51. rem/models/entities/shared_session.py +180 -0
  52. rem/models/entities/user.py +10 -3
  53. rem/registry.py +373 -0
  54. rem/schemas/agents/rem.yaml +7 -3
  55. rem/services/content/providers.py +94 -140
  56. rem/services/content/service.py +92 -20
  57. rem/services/dreaming/affinity_service.py +2 -16
  58. rem/services/dreaming/moment_service.py +2 -15
  59. rem/services/embeddings/api.py +24 -17
  60. rem/services/embeddings/worker.py +16 -16
  61. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  62. rem/services/phoenix/client.py +252 -19
  63. rem/services/postgres/README.md +159 -15
  64. rem/services/postgres/__init__.py +2 -1
  65. rem/services/postgres/diff_service.py +426 -0
  66. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  67. rem/services/postgres/repository.py +132 -0
  68. rem/services/postgres/schema_generator.py +86 -5
  69. rem/services/postgres/service.py +6 -6
  70. rem/services/rate_limit.py +113 -0
  71. rem/services/rem/README.md +14 -0
  72. rem/services/rem/parser.py +44 -9
  73. rem/services/rem/service.py +36 -2
  74. rem/services/session/compression.py +17 -1
  75. rem/services/session/reload.py +1 -1
  76. rem/services/user_service.py +98 -0
  77. rem/settings.py +169 -17
  78. rem/sql/background_indexes.sql +21 -16
  79. rem/sql/migrations/001_install.sql +231 -54
  80. rem/sql/migrations/002_install_models.sql +457 -393
  81. rem/sql/migrations/003_optional_extensions.sql +326 -0
  82. rem/utils/constants.py +97 -0
  83. rem/utils/date_utils.py +228 -0
  84. rem/utils/embeddings.py +17 -4
  85. rem/utils/files.py +167 -0
  86. rem/utils/mime_types.py +158 -0
  87. rem/utils/model_helpers.py +156 -1
  88. rem/utils/schema_loader.py +191 -35
  89. rem/utils/sql_types.py +3 -1
  90. rem/utils/vision.py +9 -14
  91. rem/workers/README.md +14 -14
  92. rem/workers/db_maintainer.py +74 -0
  93. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/METADATA +303 -164
  94. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/RECORD +96 -70
  95. {remdb-0.3.0.dist-info → remdb-0.3.114.dist-info}/WHEEL +1 -1
  96. rem/sql/002_install_models.sql +0 -1068
  97. rem/sql/install_models.sql +0 -1038
  98. {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 (TODO)
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 (TODO)
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
- logger.info(
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
- # Log response
155
+ # Determine log level based on path AND response status
102
156
  duration_ms = (time.time() - start_time) * 1000
103
- logger.info(
104
- f"← RESPONSE: {request.method} {request.url.path} | "
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
- logger.info(f"Database enabled: {settings.postgres.connection_string}")
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="REM API",
202
- description="Resources Entities Moments system for agentic AI",
203
- version="0.1.0",
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": "REM API",
265
- "version": "0.1.0",
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": "0.1.0"}
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
 
@@ -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.7)
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
 
@@ -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="0.1.0",
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 ask_rem_agent, ingest_into_rem, read_resource, search_rem
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
@@ -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.info("MCP tools initialized with service instances")
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.info("MCP tools: lazy initialized services")
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.info(f"Running ask_rem agent for query: {query[:100]}...")
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.info("Agent execution completed successfully")
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.info(
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.info(f"📖 Reading resource: {uri}")
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.info(f"Resource loaded successfully: {uri}")
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