remdb 0.2.6__py3-none-any.whl → 0.3.103__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/__init__.py +129 -2
- rem/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +16 -2
- rem/agentic/agents/sse_simulator.py +500 -0
- rem/agentic/context.py +7 -5
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/providers/phoenix.py +32 -43
- rem/agentic/providers/pydantic_ai.py +84 -10
- rem/api/README.md +238 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +70 -22
- rem/api/mcp_router/server.py +8 -1
- rem/api/mcp_router/tools.py +80 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +277 -0
- rem/api/routers/auth.py +124 -0
- rem/api/routers/chat/completions.py +123 -14
- rem/api/routers/chat/models.py +7 -3
- rem/api/routers/chat/sse_events.py +526 -0
- rem/api/routers/chat/streaming.py +468 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +455 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/ask.py +15 -11
- rem/cli/commands/configure.py +169 -94
- rem/cli/commands/db.py +53 -7
- rem/cli/commands/experiments.py +278 -96
- rem/cli/commands/process.py +8 -7
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +9 -9
- rem/cli/main.py +10 -0
- rem/config.py +2 -2
- rem/models/core/core_model.py +7 -1
- rem/models/entities/__init__.py +21 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +206 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +367 -0
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/providers.py +94 -140
- rem/services/content/service.py +85 -16
- rem/services/dreaming/affinity_service.py +2 -16
- rem/services/dreaming/moment_service.py +2 -15
- rem/services/embeddings/api.py +20 -13
- rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
- rem/services/phoenix/client.py +252 -19
- rem/services/postgres/README.md +29 -10
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +86 -5
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/session/compression.py +17 -1
- rem/services/user_service.py +98 -0
- rem/settings.py +115 -17
- rem/sql/background_indexes.sql +10 -0
- rem/sql/migrations/001_install.sql +152 -2
- rem/sql/migrations/002_install_models.sql +580 -231
- rem/sql/migrations/003_seed_default_user.sql +48 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/embeddings.py +17 -4
- rem/utils/files.py +167 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +273 -14
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/db_maintainer.py +74 -0
- {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/METADATA +486 -132
- {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/RECORD +80 -57
- {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1038
- {remdb-0.2.6.dist-info → remdb-0.3.103.dist-info}/entry_points.txt +0 -0
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
|
|
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="
|
|
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": "
|
|
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
|
|
rem/api/mcp_router/server.py
CHANGED
|
@@ -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
|
|
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
|