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.

Files changed (82) 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 +7 -5
  7. rem/agentic/llm_provider_models.py +301 -0
  8. rem/agentic/providers/phoenix.py +32 -43
  9. rem/agentic/providers/pydantic_ai.py +84 -10
  10. rem/api/README.md +238 -1
  11. rem/api/deps.py +255 -0
  12. rem/api/main.py +70 -22
  13. rem/api/mcp_router/server.py +8 -1
  14. rem/api/mcp_router/tools.py +80 -0
  15. rem/api/middleware/tracking.py +172 -0
  16. rem/api/routers/admin.py +277 -0
  17. rem/api/routers/auth.py +124 -0
  18. rem/api/routers/chat/completions.py +123 -14
  19. rem/api/routers/chat/models.py +7 -3
  20. rem/api/routers/chat/sse_events.py +526 -0
  21. rem/api/routers/chat/streaming.py +468 -45
  22. rem/api/routers/dev.py +81 -0
  23. rem/api/routers/feedback.py +455 -0
  24. rem/api/routers/messages.py +473 -0
  25. rem/api/routers/models.py +78 -0
  26. rem/api/routers/shared_sessions.py +406 -0
  27. rem/auth/middleware.py +126 -27
  28. rem/cli/commands/ask.py +15 -11
  29. rem/cli/commands/configure.py +169 -94
  30. rem/cli/commands/db.py +53 -7
  31. rem/cli/commands/experiments.py +278 -96
  32. rem/cli/commands/process.py +8 -7
  33. rem/cli/commands/scaffold.py +47 -0
  34. rem/cli/commands/schema.py +9 -9
  35. rem/cli/main.py +10 -0
  36. rem/config.py +2 -2
  37. rem/models/core/core_model.py +7 -1
  38. rem/models/entities/__init__.py +21 -0
  39. rem/models/entities/domain_resource.py +38 -0
  40. rem/models/entities/feedback.py +123 -0
  41. rem/models/entities/message.py +30 -1
  42. rem/models/entities/session.py +83 -0
  43. rem/models/entities/shared_session.py +206 -0
  44. rem/models/entities/user.py +10 -3
  45. rem/registry.py +367 -0
  46. rem/schemas/agents/rem.yaml +7 -3
  47. rem/services/content/providers.py +94 -140
  48. rem/services/content/service.py +85 -16
  49. rem/services/dreaming/affinity_service.py +2 -16
  50. rem/services/dreaming/moment_service.py +2 -15
  51. rem/services/embeddings/api.py +20 -13
  52. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  53. rem/services/phoenix/client.py +252 -19
  54. rem/services/postgres/README.md +29 -10
  55. rem/services/postgres/repository.py +132 -0
  56. rem/services/postgres/schema_generator.py +86 -5
  57. rem/services/rate_limit.py +113 -0
  58. rem/services/rem/README.md +14 -0
  59. rem/services/session/compression.py +17 -1
  60. rem/services/user_service.py +98 -0
  61. rem/settings.py +115 -17
  62. rem/sql/background_indexes.sql +10 -0
  63. rem/sql/migrations/001_install.sql +152 -2
  64. rem/sql/migrations/002_install_models.sql +580 -231
  65. rem/sql/migrations/003_seed_default_user.sql +48 -0
  66. rem/utils/constants.py +97 -0
  67. rem/utils/date_utils.py +228 -0
  68. rem/utils/embeddings.py +17 -4
  69. rem/utils/files.py +167 -0
  70. rem/utils/mime_types.py +158 -0
  71. rem/utils/model_helpers.py +156 -1
  72. rem/utils/schema_loader.py +273 -14
  73. rem/utils/sql_types.py +3 -1
  74. rem/utils/vision.py +9 -14
  75. rem/workers/README.md +14 -14
  76. rem/workers/db_maintainer.py +74 -0
  77. {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/METADATA +486 -132
  78. {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/RECORD +80 -57
  79. {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/WHEEL +1 -1
  80. rem/sql/002_install_models.sql +0 -1068
  81. rem/sql/install_models.sql +0 -1038
  82. {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/entry_points.txt +0 -0
rem/api/README.md CHANGED
@@ -158,9 +158,70 @@ The dreaming worker runs periodically to build user models:
158
158
  - User profile automatically loaded and injected into system message
159
159
  - Simpler for basic chatbots that always need context
160
160
 
161
+ ## Authentication
162
+
163
+ ### Production Authentication
164
+
165
+ When `AUTH__ENABLED=true`, users authenticate via OAuth (Google or Microsoft). The OAuth flow:
166
+
167
+ 1. User visits `/api/auth/google/login` or `/api/auth/microsoft/login`
168
+ 2. User authenticates with provider
169
+ 3. Callback stores user in session cookie
170
+ 4. Subsequent requests use session cookie
171
+
172
+ ### Development Token (Non-Production Only)
173
+
174
+ For local development and testing, you can use a dev token instead of OAuth. This endpoint is available at `/api/dev/token` whenever `ENVIRONMENT != "production"`, regardless of whether auth is enabled.
175
+
176
+ **Get Token:**
177
+ ```bash
178
+ curl http://localhost:8000/api/dev/token
179
+ ```
180
+
181
+ **Response:**
182
+ ```json
183
+ {
184
+ "token": "dev_89737a19376332bfd9a4a06db8b79fd1",
185
+ "type": "Bearer",
186
+ "user": {
187
+ "id": "test-user",
188
+ "email": "test@rem.local",
189
+ "name": "Test User"
190
+ },
191
+ "usage": "curl -H \"Authorization: Bearer dev_...\" http://localhost:8000/api/v1/...",
192
+ "warning": "This token is for development/testing only and will not work in production."
193
+ }
194
+ ```
195
+
196
+ **Use Token:**
197
+ ```bash
198
+ # Get the token
199
+ TOKEN=$(curl -s http://localhost:8000/api/dev/token | jq -r .token)
200
+
201
+ # Use it in requests
202
+ curl -H "Authorization: Bearer $TOKEN" \
203
+ -H "X-Tenant-Id: default" \
204
+ http://localhost:8000/api/v1/shared-with-me
205
+ ```
206
+
207
+ **Security Notes:**
208
+ - Only available when `ENVIRONMENT != "production"`
209
+ - Token is HMAC-signed using session secret
210
+ - Authenticates as `test-user` with `pro` tier and `admin` role
211
+ - Token is deterministic per environment (same secret = same token)
212
+
213
+ ### Anonymous Access
214
+
215
+ When `AUTH__ALLOW_ANONYMOUS=true` (default in development):
216
+ - Requests without authentication are allowed
217
+ - Anonymous users get rate-limited access
218
+ - MCP endpoints still require auth unless `AUTH__MCP_REQUIRES_AUTH=false`
219
+
161
220
  ## Usage Examples
162
221
 
163
- **Note on Authentication**: By default, authentication is disabled (`AUTH__ENABLED=false`) for local development and testing. The examples below work without an `Authorization` header. If authentication is enabled in your environment, add: `-H "Authorization: Bearer your_jwt_token"` to cURL requests or `"Authorization": "Bearer your_jwt_token"` to Python headers.
222
+ **Note on Authentication**: By default, authentication is disabled (`AUTH__ENABLED=false`) for local development and testing. The examples below work without an `Authorization` header. If authentication is enabled, use either:
223
+ - **Dev token**: `-H "Authorization: Bearer $(curl -s http://localhost:8000/api/dev/token | jq -r .token)"`
224
+ - **Session cookie**: Login via OAuth first, then use cookies
164
225
 
165
226
  ### cURL: Simple Chat
166
227
 
@@ -363,6 +424,157 @@ data: {"id":"chatcmpl-abc123","choices":[{"delta":{},"finish_reason":"stop","ind
363
424
  data: [DONE]
364
425
  ```
365
426
 
427
+ ## Extended SSE Event Protocol
428
+
429
+ REM uses OpenAI-compatible format for text content streaming, plus custom named SSE events for rich UI interactions.
430
+
431
+ ### Event Types
432
+
433
+ | Event Type | Format | Purpose | UI Display |
434
+ |------------|--------|---------|------------|
435
+ | (text content) | `data:` (OpenAI format) | Content chunks | Main response area |
436
+ | `reasoning` | `event:` | Model thinking | Collapsible "thinking" section |
437
+ | `progress` | `event:` | Step indicators | Progress bar/stepper |
438
+ | `tool_call` | `event:` | Tool invocations | Tool status panel |
439
+ | `action_request` | `event:` | User input solicitation | Buttons, forms, modals |
440
+ | `metadata` | `event:` | System info | Hidden or badge display |
441
+ | `error` | `event:` | Error notification | Error toast/alert |
442
+ | `done` | `event:` | Stream completion | Cleanup signal |
443
+
444
+ ### Event Format
445
+
446
+ **Text content (OpenAI-compatible `data:` format):**
447
+ ```
448
+ data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1732748123,"model":"gpt-4","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello "},"finish_reason":null}]}
449
+
450
+ data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1732748123,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"world!"},"finish_reason":null}]}
451
+
452
+ data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1732748123,"model":"gpt-4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
453
+
454
+ data: [DONE]
455
+ ```
456
+
457
+ **Named events (use `event:` prefix):**
458
+ ```
459
+ event: reasoning
460
+ data: {"type": "reasoning", "content": "Analyzing the request...", "step": 1}
461
+
462
+ event: progress
463
+ data: {"type": "progress", "step": 1, "total_steps": 3, "label": "Searching", "status": "in_progress"}
464
+
465
+ event: tool_call
466
+ data: {"type": "tool_call", "tool_name": "search_rem", "status": "started", "arguments": {"query": "..."}}
467
+
468
+ event: action_request
469
+ data: {"type": "action_request", "card": {"id": "feedback-1", "prompt": "Was this helpful?", "actions": [...]}}
470
+
471
+ event: metadata
472
+ data: {"type": "metadata", "confidence": 0.95, "sources": ["doc1.md"], "hidden": false}
473
+
474
+ event: done
475
+ data: {"type": "done", "reason": "stop"}
476
+ ```
477
+
478
+ ### Action Request Cards (Adaptive Cards-inspired)
479
+
480
+ Action requests solicit user input using a schema inspired by [Microsoft Adaptive Cards](https://adaptivecards.io/):
481
+
482
+ ```json
483
+ {
484
+ "type": "action_request",
485
+ "card": {
486
+ "id": "confirm-delete-123",
487
+ "prompt": "Are you sure you want to delete this item?",
488
+ "display_style": "modal",
489
+ "actions": [
490
+ {
491
+ "type": "Action.Submit",
492
+ "id": "confirm",
493
+ "title": "Delete",
494
+ "style": "destructive",
495
+ "data": {"action": "delete", "item_id": "123"}
496
+ },
497
+ {
498
+ "type": "Action.Submit",
499
+ "id": "cancel",
500
+ "title": "Cancel",
501
+ "style": "secondary",
502
+ "data": {"action": "cancel"}
503
+ }
504
+ ],
505
+ "inputs": [
506
+ {
507
+ "type": "Input.Text",
508
+ "id": "reason",
509
+ "label": "Reason (optional)",
510
+ "placeholder": "Why are you deleting this?"
511
+ }
512
+ ],
513
+ "timeout_ms": 30000
514
+ }
515
+ }
516
+ ```
517
+
518
+ **Action Types:**
519
+ - `Action.Submit` - Send data to server
520
+ - `Action.OpenUrl` - Navigate to URL
521
+ - `Action.ShowCard` - Reveal nested content
522
+
523
+ **Input Types:**
524
+ - `Input.Text` - Text field (single or multiline)
525
+ - `Input.ChoiceSet` - Dropdown/radio selection
526
+ - `Input.Toggle` - Checkbox/toggle
527
+
528
+ ### SSE Simulator Endpoint
529
+
530
+ For frontend development and testing, use the simulator which generates all event types without LLM costs:
531
+
532
+ ```bash
533
+ curl -X POST http://localhost:8000/api/v1/chat/completions \
534
+ -H "Content-Type: application/json" \
535
+ -H "X-Agent-Schema: simulator" \
536
+ -d '{"messages": [{"role": "user", "content": "demo"}], "stream": true}'
537
+ ```
538
+
539
+ The simulator produces a scripted sequence demonstrating:
540
+ 1. Reasoning events (4 steps)
541
+ 2. Progress indicators
542
+ 3. Simulated tool calls
543
+ 4. Rich markdown content
544
+ 5. Metadata with confidence
545
+ 6. Action request for feedback
546
+
547
+ See `rem/agentic/agents/sse_simulator.py` for implementation details.
548
+
549
+ ### Frontend Integration
550
+
551
+ ```typescript
552
+ // Parse SSE events in React/TypeScript
553
+ const eventSource = new EventSource('/api/v1/chat/completions');
554
+
555
+ eventSource.onmessage = (e) => {
556
+ // Default handler for data-only events (text_delta)
557
+ const event = JSON.parse(e.data);
558
+ if (event.type === 'text_delta') {
559
+ appendContent(event.content);
560
+ }
561
+ };
562
+
563
+ eventSource.addEventListener('reasoning', (e) => {
564
+ const event = JSON.parse(e.data);
565
+ appendReasoning(event.content);
566
+ });
567
+
568
+ eventSource.addEventListener('action_request', (e) => {
569
+ const event = JSON.parse(e.data);
570
+ showActionCard(event.card);
571
+ });
572
+
573
+ eventSource.addEventListener('done', () => {
574
+ eventSource.close();
575
+ });
576
+ ```
577
+
366
578
  ## Architecture
367
579
 
368
580
  ### Middleware Ordering
@@ -392,6 +604,29 @@ Middleware runs in reverse order of addition:
392
604
 
393
605
  ## Error Responses
394
606
 
607
+ ### 429 - Rate Limit Exceeded
608
+
609
+ When a user exceeds their rate limit (based on their tier), the API returns a 429 status code with a structured error body. The frontend should intercept this error to prompt the user to sign in or upgrade.
610
+
611
+ ```json
612
+ {
613
+ "error": {
614
+ "code": "rate_limit_exceeded",
615
+ "message": "You have exceeded your rate limit. Please sign in or upgrade to continue.",
616
+ "details": {
617
+ "limit": 50,
618
+ "tier": "anonymous",
619
+ "retry_after": 60
620
+ }
621
+ }
622
+ }
623
+ ```
624
+
625
+ **Handling Strategy:**
626
+ 1. **Intercept 429s:** API client should listen for `status === 429`.
627
+ 2. **Check Code:** If `error.code === 'rate_limit_exceeded'` AND `error.details.tier === 'anonymous'`, trigger "Login / Sign Up" flow.
628
+ 3. **Authenticated Users:** If `tier !== 'anonymous'`, prompt to upgrade plan.
629
+
395
630
  ### 500 - Agent Schema Not Found
396
631
 
397
632
  ```json
@@ -414,6 +649,8 @@ Middleware runs in reverse order of addition:
414
649
  ## Related Documentation
415
650
 
416
651
  - [Chat Router](routers/chat/completions.py) - Chat completions implementation
652
+ - [SSE Events](routers/chat/sse_events.py) - SSE event type definitions
653
+ - [SSE Simulator](../../agentic/agents/sse_simulator.py) - Event simulator for testing
417
654
  - [MCP Router](mcp_router/server.py) - MCP server implementation
418
655
  - [Agent Schemas](../../schemas/agents/) - Available agent schemas
419
656
  - [Session Compression](../../services/session/compression.py) - Compression implementation
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
@@ -163,7 +163,22 @@ async def lifespan(app: FastAPI):
163
163
 
164
164
  def create_app() -> FastAPI:
165
165
  """
166
- Create and configure the FastAPI application.
166
+ Create and configure the FastAPI application with MCP server.
167
+
168
+ The returned app exposes `app.mcp_server` (FastMCP instance) for adding
169
+ custom tools, resources, and prompts:
170
+
171
+ app = create_app()
172
+
173
+ @app.mcp_server.tool()
174
+ async def my_tool(query: str) -> dict:
175
+ '''Custom MCP tool.'''
176
+ return {"result": query}
177
+
178
+ @app.mcp_server.resource("custom://data")
179
+ async def my_resource() -> str:
180
+ '''Custom resource.'''
181
+ return '{"data": "value"}'
167
182
 
168
183
  Design Pattern:
169
184
  1. Create MCP server
@@ -174,9 +189,10 @@ def create_app() -> FastAPI:
174
189
  6. Define health endpoints
175
190
  7. Register API routers
176
191
  8. Mount MCP app
192
+ 9. Expose mcp_server on app for extension
177
193
 
178
194
  Returns:
179
- Configured FastAPI application
195
+ Configured FastAPI application with .mcp_server attribute
180
196
  """
181
197
  # Create MCP server and get HTTP app
182
198
  # path="/" creates routes at root, then mount at /api/v1/mcp
@@ -198,15 +214,42 @@ def create_app() -> FastAPI:
198
214
  yield
199
215
 
200
216
  app = FastAPI(
201
- title="REM API",
202
- description="Resources Entities Moments system for agentic AI",
217
+ title=f"{settings.app_name} API",
218
+ description=f"{settings.app_name} - Resources Entities Moments system for agentic AI",
203
219
  version="0.1.0",
204
220
  lifespan=combined_lifespan,
205
221
  root_path=settings.root_path if settings.root_path else "",
206
222
  redirect_slashes=False, # Don't redirect /mcp/ -> /mcp
207
223
  )
208
224
 
225
+ # Add request logging middleware
226
+ app.add_middleware(RequestLoggingMiddleware)
227
+
228
+ # Add SSE buffering middleware (for MCP SSE transport)
229
+ app.add_middleware(SSEBufferingMiddleware)
230
+
231
+ # Add Anonymous Tracking & Rate Limiting (Runs AFTER Auth if Auth is enabled)
232
+ # Must be added BEFORE AuthMiddleware in code to be INNER in the stack
233
+ from .middleware.tracking import AnonymousTrackingMiddleware
234
+ app.add_middleware(AnonymousTrackingMiddleware)
235
+
236
+ # Add authentication middleware
237
+ # Always load middleware for dev token support, but allow anonymous when auth disabled
238
+ from ..auth.middleware import AuthMiddleware
239
+
240
+ app.add_middleware(
241
+ AuthMiddleware,
242
+ protected_paths=["/api/v1"],
243
+ excluded_paths=["/api/auth", "/api/dev", "/api/v1/mcp/auth"],
244
+ # Allow anonymous when auth is disabled, otherwise use setting
245
+ allow_anonymous=(not settings.auth.enabled) or settings.auth.allow_anonymous,
246
+ # MCP requires auth only when auth is fully enabled
247
+ mcp_requires_auth=settings.auth.enabled and settings.auth.mcp_requires_auth,
248
+ )
249
+
209
250
  # Add session middleware for OAuth state management
251
+ # Must be added AFTER AuthMiddleware in code so it runs BEFORE (middleware runs in reverse)
252
+ # AuthMiddleware needs request.session to be available
210
253
  session_secret = settings.auth.session_secret or secrets.token_hex(32)
211
254
  if not settings.auth.session_secret:
212
255
  logger.warning(
@@ -223,27 +266,12 @@ def create_app() -> FastAPI:
223
266
  https_only=settings.environment == "production",
224
267
  )
225
268
 
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
269
  # Add CORS middleware LAST (runs first in middleware chain)
243
270
  # Must expose mcp-session-id header for MCP session management
244
271
  CORS_ORIGIN_WHITELIST = [
245
- "http://localhost:5173", # Local development (Vite)
246
272
  "http://localhost:3000", # Local development (React)
273
+ "http://localhost:5000", # Local development (Flask/other)
274
+ "http://localhost:5173", # Local development (Vite)
247
275
  ]
248
276
 
249
277
  app.add_middleware(
@@ -261,7 +289,7 @@ def create_app() -> FastAPI:
261
289
  """API information endpoint."""
262
290
  # TODO: If auth enabled and no user, return 401 with WWW-Authenticate
263
291
  return {
264
- "name": "REM API",
292
+ "name": f"{settings.app_name} API",
265
293
  "version": "0.1.0",
266
294
  "mcp_endpoint": "/api/v1/mcp",
267
295
  "docs": "/docs",
@@ -275,8 +303,18 @@ def create_app() -> FastAPI:
275
303
 
276
304
  # Register API routers
277
305
  from .routers.chat import router as chat_router
306
+ from .routers.models import router as models_router
307
+ from .routers.messages import router as messages_router
308
+ from .routers.feedback import router as feedback_router
309
+ from .routers.admin import router as admin_router
310
+ from .routers.shared_sessions import router as shared_sessions_router
278
311
 
279
312
  app.include_router(chat_router)
313
+ app.include_router(models_router)
314
+ app.include_router(messages_router)
315
+ app.include_router(feedback_router)
316
+ app.include_router(admin_router)
317
+ app.include_router(shared_sessions_router)
280
318
 
281
319
  # Register auth router (if enabled)
282
320
  if settings.auth.enabled:
@@ -284,6 +322,12 @@ def create_app() -> FastAPI:
284
322
 
285
323
  app.include_router(auth_router)
286
324
 
325
+ # Register dev router (non-production only)
326
+ if settings.environment != "production":
327
+ from .routers.dev import router as dev_router
328
+
329
+ app.include_router(dev_router)
330
+
287
331
  # TODO: Register additional routers
288
332
  # from .routers.query import router as query_router
289
333
  # from .routers.resources import router as resources_router
@@ -305,6 +349,10 @@ def create_app() -> FastAPI:
305
349
  # Mount MCP app at /api/v1/mcp
306
350
  app.mount("/api/v1/mcp", mcp_app)
307
351
 
352
+ # Expose MCP server on app for extension
353
+ # Users can add tools/resources/prompts via app.mcp_server
354
+ app.mcp_server = mcp_server # type: ignore[attr-defined]
355
+
308
356
  return app
309
357
 
310
358
 
@@ -165,11 +165,18 @@ def create_mcp_server(is_local: bool = False) -> FastMCP:
165
165
  )
166
166
 
167
167
  # Register REM tools
168
- from .tools import ask_rem_agent, ingest_into_rem, read_resource, search_rem
168
+ from .tools import (
169
+ ask_rem_agent,
170
+ ingest_into_rem,
171
+ read_resource,
172
+ register_metadata,
173
+ search_rem,
174
+ )
169
175
 
170
176
  mcp.tool()(search_rem)
171
177
  mcp.tool()(ask_rem_agent)
172
178
  mcp.tool()(read_resource)
179
+ mcp.tool()(register_metadata)
173
180
 
174
181
  # File ingestion tool (with local path support for local servers)
175
182
  # Wrap to inject is_local parameter