remdb 0.3.180__py3-none-any.whl → 0.3.258__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.
Files changed (70) hide show
  1. rem/agentic/README.md +36 -2
  2. rem/agentic/__init__.py +10 -1
  3. rem/agentic/context.py +185 -1
  4. rem/agentic/context_builder.py +56 -35
  5. rem/agentic/mcp/tool_wrapper.py +2 -2
  6. rem/agentic/providers/pydantic_ai.py +303 -111
  7. rem/agentic/schema.py +2 -2
  8. rem/api/main.py +1 -1
  9. rem/api/mcp_router/resources.py +223 -0
  10. rem/api/mcp_router/server.py +4 -0
  11. rem/api/mcp_router/tools.py +608 -166
  12. rem/api/routers/admin.py +30 -4
  13. rem/api/routers/auth.py +219 -20
  14. rem/api/routers/chat/child_streaming.py +393 -0
  15. rem/api/routers/chat/completions.py +77 -40
  16. rem/api/routers/chat/sse_events.py +7 -3
  17. rem/api/routers/chat/streaming.py +381 -291
  18. rem/api/routers/chat/streaming_utils.py +325 -0
  19. rem/api/routers/common.py +18 -0
  20. rem/api/routers/dev.py +7 -1
  21. rem/api/routers/feedback.py +11 -3
  22. rem/api/routers/messages.py +176 -38
  23. rem/api/routers/models.py +9 -1
  24. rem/api/routers/query.py +17 -15
  25. rem/api/routers/shared_sessions.py +16 -0
  26. rem/auth/jwt.py +19 -4
  27. rem/auth/middleware.py +42 -28
  28. rem/cli/README.md +62 -0
  29. rem/cli/commands/ask.py +205 -114
  30. rem/cli/commands/db.py +55 -31
  31. rem/cli/commands/experiments.py +1 -1
  32. rem/cli/commands/process.py +179 -43
  33. rem/cli/commands/query.py +109 -0
  34. rem/cli/commands/session.py +117 -0
  35. rem/cli/main.py +2 -0
  36. rem/models/core/experiment.py +1 -1
  37. rem/models/entities/ontology.py +18 -20
  38. rem/models/entities/session.py +1 -0
  39. rem/schemas/agents/core/agent-builder.yaml +1 -1
  40. rem/schemas/agents/rem.yaml +1 -1
  41. rem/schemas/agents/test_orchestrator.yaml +42 -0
  42. rem/schemas/agents/test_structured_output.yaml +52 -0
  43. rem/services/content/providers.py +151 -49
  44. rem/services/content/service.py +18 -5
  45. rem/services/embeddings/worker.py +26 -12
  46. rem/services/postgres/__init__.py +28 -3
  47. rem/services/postgres/diff_service.py +57 -5
  48. rem/services/postgres/programmable_diff_service.py +635 -0
  49. rem/services/postgres/pydantic_to_sqlalchemy.py +2 -2
  50. rem/services/postgres/register_type.py +11 -10
  51. rem/services/postgres/repository.py +39 -28
  52. rem/services/postgres/schema_generator.py +5 -5
  53. rem/services/postgres/sql_builder.py +6 -5
  54. rem/services/rem/README.md +4 -3
  55. rem/services/rem/parser.py +7 -10
  56. rem/services/rem/service.py +47 -0
  57. rem/services/session/__init__.py +8 -1
  58. rem/services/session/compression.py +47 -5
  59. rem/services/session/pydantic_messages.py +310 -0
  60. rem/services/session/reload.py +2 -1
  61. rem/settings.py +92 -7
  62. rem/sql/migrations/001_install.sql +125 -7
  63. rem/sql/migrations/002_install_models.sql +159 -149
  64. rem/sql/migrations/004_cache_system.sql +10 -276
  65. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  66. rem/utils/schema_loader.py +180 -120
  67. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/METADATA +7 -6
  68. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/RECORD +70 -61
  69. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/WHEEL +0 -0
  70. {remdb-0.3.180.dist-info → remdb-0.3.258.dist-info}/entry_points.txt +0 -0
rem/api/routers/admin.py CHANGED
@@ -31,6 +31,8 @@ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Background
31
31
  from loguru import logger
32
32
  from pydantic import BaseModel
33
33
 
34
+ from .common import ErrorResponse
35
+
34
36
  from ..deps import require_admin
35
37
  from ...models.entities import Message, Session, SessionMode
36
38
  from ...services.postgres import Repository
@@ -103,7 +105,13 @@ class SystemStats(BaseModel):
103
105
  # =============================================================================
104
106
 
105
107
 
106
- @router.get("/users", response_model=UserListResponse)
108
+ @router.get(
109
+ "/users",
110
+ response_model=UserListResponse,
111
+ responses={
112
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
113
+ },
114
+ )
107
115
  async def list_all_users(
108
116
  user: dict = Depends(require_admin),
109
117
  limit: int = Query(default=50, ge=1, le=100),
@@ -155,7 +163,13 @@ async def list_all_users(
155
163
  return UserListResponse(data=summaries, total=total, has_more=has_more)
156
164
 
157
165
 
158
- @router.get("/sessions", response_model=SessionListResponse)
166
+ @router.get(
167
+ "/sessions",
168
+ response_model=SessionListResponse,
169
+ responses={
170
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
171
+ },
172
+ )
159
173
  async def list_all_sessions(
160
174
  user: dict = Depends(require_admin),
161
175
  user_id: str | None = Query(default=None, description="Filter by user ID"),
@@ -202,7 +216,13 @@ async def list_all_sessions(
202
216
  return SessionListResponse(data=sessions, total=total, has_more=has_more)
203
217
 
204
218
 
205
- @router.get("/messages", response_model=MessageListResponse)
219
+ @router.get(
220
+ "/messages",
221
+ response_model=MessageListResponse,
222
+ responses={
223
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
224
+ },
225
+ )
206
226
  async def list_all_messages(
207
227
  user: dict = Depends(require_admin),
208
228
  user_id: str | None = Query(default=None, description="Filter by user ID"),
@@ -252,7 +272,13 @@ async def list_all_messages(
252
272
  return MessageListResponse(data=messages, total=total, has_more=has_more)
253
273
 
254
274
 
255
- @router.get("/stats", response_model=SystemStats)
275
+ @router.get(
276
+ "/stats",
277
+ response_model=SystemStats,
278
+ responses={
279
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
280
+ },
281
+ )
256
282
  async def get_system_stats(
257
283
  user: dict = Depends(require_admin),
258
284
  ) -> SystemStats:
rem/api/routers/auth.py CHANGED
@@ -3,11 +3,12 @@ Authentication Router.
3
3
 
4
4
  Supports multiple authentication methods:
5
5
  1. Email (passwordless): POST /api/auth/email/send-code, POST /api/auth/email/verify
6
- 2. OAuth (Google, Microsoft): GET /api/auth/{provider}/login, GET /api/auth/{provider}/callback
6
+ 2. Pre-approved codes: POST /api/auth/email/verify (with pre-approved code, no send-code needed)
7
+ 3. OAuth (Google, Microsoft): GET /api/auth/{provider}/login, GET /api/auth/{provider}/callback
7
8
 
8
9
  Endpoints:
9
10
  - POST /api/auth/email/send-code - Send login code to email
10
- - POST /api/auth/email/verify - Verify code and create session
11
+ - POST /api/auth/email/verify - Verify code and create session (supports pre-approved codes)
11
12
  - GET /api/auth/{provider}/login - Initiate OAuth flow
12
13
  - GET /api/auth/{provider}/callback - OAuth callback
13
14
  - POST /api/auth/logout - Clear session
@@ -15,9 +16,39 @@ Endpoints:
15
16
 
16
17
  Supported providers:
17
18
  - email: Passwordless email login
19
+ - preapproved: Pre-approved codes (bypass email, set via AUTH__PREAPPROVED_CODES)
18
20
  - google: Google OAuth 2.0 / OIDC
19
21
  - microsoft: Microsoft Entra ID OIDC
20
22
 
23
+ =============================================================================
24
+ Pre-Approved Code Authentication
25
+ =============================================================================
26
+
27
+ Pre-approved codes allow login without email verification. Useful for:
28
+ - Demo accounts
29
+ - Testing
30
+ - Beta access codes
31
+ - Admin provisioning
32
+
33
+ Configuration:
34
+ AUTH__PREAPPROVED_CODES=A12345,A67890,B11111,B22222
35
+
36
+ Code prefixes:
37
+ A = Admin role (e.g., A12345, AADMIN1)
38
+ B = Normal user role (e.g., B11111, BUSER1)
39
+
40
+ Flow:
41
+ 1. User enters email + pre-approved code (no send-code step needed)
42
+ 2. POST /api/auth/email/verify with email and code
43
+ 3. System validates code against AUTH__PREAPPROVED_CODES
44
+ 4. Creates user if not exists, sets role based on prefix
45
+ 5. Returns JWT tokens (same as email auth)
46
+
47
+ Example:
48
+ curl -X POST http://localhost:8000/api/auth/email/verify \
49
+ -H "Content-Type: application/json" \
50
+ -d '{"email": "admin@example.com", "code": "A12345"}'
51
+
21
52
  =============================================================================
22
53
  Email Authentication Access Control
23
54
  =============================================================================
@@ -52,7 +83,7 @@ User Tiers (models.entities.UserTier):
52
83
 
53
84
  Configuration:
54
85
  # Allow only specific domains for new signups
55
- EMAIL__TRUSTED_EMAIL_DOMAINS=siggymd.ai,example.com
86
+ EMAIL__TRUSTED_EMAIL_DOMAINS=mycompany.com,example.com
56
87
 
57
88
  # Allow all domains (no restrictions)
58
89
  EMAIL__TRUSTED_EMAIL_DOMAINS=
@@ -101,6 +132,8 @@ from authlib.integrations.starlette_client import OAuth
101
132
  from pydantic import BaseModel, EmailStr
102
133
  from loguru import logger
103
134
 
135
+ from .common import ErrorResponse
136
+
104
137
  from ...settings import settings
105
138
  from ...services.postgres.service import PostgresService
106
139
  from ...services.user_service import UserService
@@ -159,7 +192,14 @@ class EmailVerifyRequest(BaseModel):
159
192
  code: str
160
193
 
161
194
 
162
- @router.post("/email/send-code")
195
+ @router.post(
196
+ "/email/send-code",
197
+ responses={
198
+ 400: {"model": ErrorResponse, "description": "Invalid request or email rejected"},
199
+ 500: {"model": ErrorResponse, "description": "Failed to send login code"},
200
+ 501: {"model": ErrorResponse, "description": "Email auth or database not configured"},
201
+ },
202
+ )
163
203
  async def send_email_code(request: Request, body: EmailSendCodeRequest):
164
204
  """
165
205
  Send a login code to an email address.
@@ -221,11 +261,24 @@ async def send_email_code(request: Request, body: EmailSendCodeRequest):
221
261
  await db.disconnect()
222
262
 
223
263
 
224
- @router.post("/email/verify")
264
+ @router.post(
265
+ "/email/verify",
266
+ responses={
267
+ 400: {"model": ErrorResponse, "description": "Invalid or expired code"},
268
+ 500: {"model": ErrorResponse, "description": "Failed to verify login code"},
269
+ 501: {"model": ErrorResponse, "description": "Email auth or database not configured"},
270
+ },
271
+ )
225
272
  async def verify_email_code(request: Request, body: EmailVerifyRequest):
226
273
  """
227
274
  Verify login code and create session with JWT tokens.
228
275
 
276
+ Supports two authentication methods:
277
+ 1. Pre-approved codes: Codes from AUTH__PREAPPROVED_CODES bypass email verification.
278
+ - A prefix = admin role, B prefix = normal user role
279
+ - Creates user if not exists, logs in directly
280
+ 2. Email verification: Standard 6-digit code sent via email
281
+
229
282
  Args:
230
283
  request: FastAPI request
231
284
  body: EmailVerifyRequest with email and code
@@ -233,12 +286,6 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
233
286
  Returns:
234
287
  Success status with user info and JWT tokens
235
288
  """
236
- if not settings.email.is_configured:
237
- raise HTTPException(
238
- status_code=501,
239
- detail="Email authentication is not configured"
240
- )
241
-
242
289
  if not settings.postgres.enabled:
243
290
  raise HTTPException(
244
291
  status_code=501,
@@ -248,6 +295,79 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
248
295
  db = PostgresService()
249
296
  try:
250
297
  await db.connect()
298
+ user_service = UserService(db)
299
+
300
+ # Check for pre-approved code first
301
+ preapproved = settings.auth.check_preapproved_code(body.code)
302
+ if preapproved:
303
+ logger.info(f"Pre-approved code login attempt for {body.email} (role: {preapproved['role']})")
304
+
305
+ # Get or create user with pre-approved role
306
+ user_id = email_to_user_id(body.email)
307
+ user_entity = await user_service.get_user_by_id(user_id)
308
+
309
+ if not user_entity:
310
+ # Create new user with role from pre-approved code
311
+ user_entity = await user_service.get_or_create_user(
312
+ email=body.email,
313
+ name=body.email.split("@")[0],
314
+ tenant_id="default",
315
+ )
316
+ # Update role based on pre-approved code prefix
317
+ user_entity.role = preapproved["role"]
318
+ from ...services.postgres.repository import Repository
319
+ from ...models.entities.user import User
320
+ user_repo = Repository(User, "users", db=db)
321
+ await user_repo.upsert(user_entity)
322
+ logger.info(f"Created user {body.email} with role={preapproved['role']} via pre-approved code")
323
+ else:
324
+ # Update existing user's role if admin code used
325
+ if preapproved["role"] == "admin" and user_entity.role != "admin":
326
+ user_entity.role = "admin"
327
+ from ...services.postgres.repository import Repository
328
+ from ...models.entities.user import User
329
+ user_repo = Repository(User, "users", db=db)
330
+ await user_repo.upsert(user_entity)
331
+ logger.info(f"Upgraded user {body.email} to admin via pre-approved code")
332
+
333
+ # Build user dict for session/JWT
334
+ user_dict = {
335
+ "id": str(user_entity.id),
336
+ "email": body.email,
337
+ "email_verified": True,
338
+ "name": user_entity.name or body.email.split("@")[0],
339
+ "provider": "preapproved",
340
+ "tenant_id": user_entity.tenant_id or "default",
341
+ "tier": user_entity.tier.value if user_entity.tier else "free",
342
+ "role": user_entity.role or preapproved["role"],
343
+ "roles": [user_entity.role or preapproved["role"]],
344
+ }
345
+
346
+ # Generate JWT tokens
347
+ jwt_service = get_jwt_service()
348
+ tokens = jwt_service.create_tokens(user_dict)
349
+
350
+ # Store user in session
351
+ request.session["user"] = user_dict
352
+
353
+ logger.info(f"User authenticated via pre-approved code: {body.email} (role: {user_dict['role']})")
354
+
355
+ return {
356
+ "success": True,
357
+ "message": "Successfully authenticated with pre-approved code!",
358
+ "user": user_dict,
359
+ "access_token": tokens["access_token"],
360
+ "refresh_token": tokens["refresh_token"],
361
+ "token_type": tokens["token_type"],
362
+ "expires_in": tokens["expires_in"],
363
+ }
364
+
365
+ # Standard email verification flow
366
+ if not settings.email.is_configured:
367
+ raise HTTPException(
368
+ status_code=501,
369
+ detail="Email authentication is not configured"
370
+ )
251
371
 
252
372
  # Initialize email auth provider
253
373
  email_auth = EmailAuthProvider()
@@ -272,7 +392,6 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
272
392
  )
273
393
 
274
394
  # Fetch actual user data from database to get role/tier
275
- user_service = UserService(db)
276
395
  try:
277
396
  user_entity = await user_service.get_user_by_id(result.user_id)
278
397
  if user_entity:
@@ -319,7 +438,13 @@ async def verify_email_code(request: Request, body: EmailVerifyRequest):
319
438
  # =============================================================================
320
439
 
321
440
 
322
- @router.get("/{provider}/login")
441
+ @router.get(
442
+ "/{provider}/login",
443
+ responses={
444
+ 400: {"model": ErrorResponse, "description": "Unknown OAuth provider"},
445
+ 501: {"model": ErrorResponse, "description": "Authentication is disabled"},
446
+ },
447
+ )
323
448
  async def login(provider: str, request: Request):
324
449
  """
325
450
  Initiate OAuth flow with provider.
@@ -361,7 +486,13 @@ async def login(provider: str, request: Request):
361
486
  return await client.authorize_redirect(request, redirect_uri)
362
487
 
363
488
 
364
- @router.get("/{provider}/callback")
489
+ @router.get(
490
+ "/{provider}/callback",
491
+ responses={
492
+ 400: {"model": ErrorResponse, "description": "Authentication failed or unknown provider"},
493
+ 501: {"model": ErrorResponse, "description": "Authentication is disabled"},
494
+ },
495
+ )
365
496
  async def callback(provider: str, request: Request):
366
497
  """
367
498
  OAuth callback endpoint.
@@ -498,7 +629,12 @@ async def logout(request: Request):
498
629
  return {"message": "Logged out successfully"}
499
630
 
500
631
 
501
- @router.get("/me")
632
+ @router.get(
633
+ "/me",
634
+ responses={
635
+ 401: {"model": ErrorResponse, "description": "Not authenticated"},
636
+ },
637
+ )
502
638
  async def me(request: Request):
503
639
  """
504
640
  Get current user information from session or JWT.
@@ -536,11 +672,19 @@ class TokenRefreshRequest(BaseModel):
536
672
  refresh_token: str
537
673
 
538
674
 
539
- @router.post("/token/refresh")
675
+ @router.post(
676
+ "/token/refresh",
677
+ responses={
678
+ 401: {"model": ErrorResponse, "description": "Invalid or expired refresh token"},
679
+ },
680
+ )
540
681
  async def refresh_token(body: TokenRefreshRequest):
541
682
  """
542
683
  Refresh access token using refresh token.
543
684
 
685
+ Fetches the user's current role/tier from the database to ensure
686
+ the new access token reflects their actual permissions.
687
+
544
688
  Args:
545
689
  body: TokenRefreshRequest with refresh_token
546
690
 
@@ -548,7 +692,46 @@ async def refresh_token(body: TokenRefreshRequest):
548
692
  New access token or 401 if refresh token is invalid
549
693
  """
550
694
  jwt_service = get_jwt_service()
551
- result = jwt_service.refresh_access_token(body.refresh_token)
695
+
696
+ # First decode the refresh token to get user_id (without full verification yet)
697
+ payload = jwt_service.decode_without_verification(body.refresh_token)
698
+ if not payload:
699
+ raise HTTPException(
700
+ status_code=401,
701
+ detail="Invalid refresh token format"
702
+ )
703
+
704
+ user_id = payload.get("sub")
705
+ if not user_id:
706
+ raise HTTPException(
707
+ status_code=401,
708
+ detail="Invalid refresh token: missing user ID"
709
+ )
710
+
711
+ # Fetch user from database to get current role/tier
712
+ user_override = None
713
+ if settings.postgres.enabled:
714
+ db = PostgresService()
715
+ try:
716
+ await db.connect()
717
+ user_service = UserService(db)
718
+ user_entity = await user_service.get_user_by_id(user_id)
719
+ if user_entity:
720
+ user_override = {
721
+ "role": user_entity.role or "user",
722
+ "roles": [user_entity.role] if user_entity.role else ["user"],
723
+ "tier": user_entity.tier.value if user_entity.tier else "free",
724
+ "name": user_entity.name,
725
+ }
726
+ logger.debug(f"Refresh token: fetched user {user_id} with role={user_override['role']}, tier={user_override['tier']}")
727
+ except Exception as e:
728
+ logger.warning(f"Could not fetch user for token refresh: {e}")
729
+ # Continue without override - will use defaults
730
+ finally:
731
+ await db.disconnect()
732
+
733
+ # Now do the actual refresh with proper verification
734
+ result = jwt_service.refresh_access_token(body.refresh_token, user_override=user_override)
552
735
 
553
736
  if not result:
554
737
  raise HTTPException(
@@ -559,7 +742,12 @@ async def refresh_token(body: TokenRefreshRequest):
559
742
  return result
560
743
 
561
744
 
562
- @router.post("/token/verify")
745
+ @router.post(
746
+ "/token/verify",
747
+ responses={
748
+ 401: {"model": ErrorResponse, "description": "Missing, invalid, or expired token"},
749
+ },
750
+ )
563
751
  async def verify_token(request: Request):
564
752
  """
565
753
  Verify an access token is valid.
@@ -623,7 +811,12 @@ def verify_dev_token(token: str) -> bool:
623
811
  return token == expected
624
812
 
625
813
 
626
- @router.get("/dev/token")
814
+ @router.get(
815
+ "/dev/token",
816
+ responses={
817
+ 401: {"model": ErrorResponse, "description": "Dev tokens not available in production"},
818
+ },
819
+ )
627
820
  async def get_dev_token(request: Request):
628
821
  """
629
822
  Get a development token for testing (non-production only).
@@ -659,7 +852,13 @@ async def get_dev_token(request: Request):
659
852
  }
660
853
 
661
854
 
662
- @router.get("/dev/mock-code/{email}")
855
+ @router.get(
856
+ "/dev/mock-code/{email}",
857
+ responses={
858
+ 401: {"model": ErrorResponse, "description": "Mock codes not available in production"},
859
+ 404: {"model": ErrorResponse, "description": "No code found for email"},
860
+ },
861
+ )
663
862
  async def get_mock_code(email: str, request: Request):
664
863
  """
665
864
  Get the mock login code for testing (non-production only).