remdb 0.3.14__py3-none-any.whl → 0.3.157__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 (112) hide show
  1. rem/agentic/README.md +76 -0
  2. rem/agentic/__init__.py +15 -0
  3. rem/agentic/agents/__init__.py +32 -2
  4. rem/agentic/agents/agent_manager.py +310 -0
  5. rem/agentic/agents/sse_simulator.py +502 -0
  6. rem/agentic/context.py +51 -27
  7. rem/agentic/context_builder.py +5 -3
  8. rem/agentic/llm_provider_models.py +301 -0
  9. rem/agentic/mcp/tool_wrapper.py +155 -18
  10. rem/agentic/otel/setup.py +93 -4
  11. rem/agentic/providers/phoenix.py +371 -108
  12. rem/agentic/providers/pydantic_ai.py +280 -57
  13. rem/agentic/schema.py +361 -21
  14. rem/agentic/tools/rem_tools.py +3 -3
  15. rem/api/README.md +215 -1
  16. rem/api/deps.py +255 -0
  17. rem/api/main.py +132 -40
  18. rem/api/mcp_router/resources.py +1 -1
  19. rem/api/mcp_router/server.py +28 -5
  20. rem/api/mcp_router/tools.py +555 -7
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +278 -4
  23. rem/api/routers/chat/completions.py +402 -20
  24. rem/api/routers/chat/models.py +88 -10
  25. rem/api/routers/chat/otel_utils.py +33 -0
  26. rem/api/routers/chat/sse_events.py +542 -0
  27. rem/api/routers/chat/streaming.py +697 -45
  28. rem/api/routers/dev.py +81 -0
  29. rem/api/routers/feedback.py +268 -0
  30. rem/api/routers/messages.py +473 -0
  31. rem/api/routers/models.py +78 -0
  32. rem/api/routers/query.py +360 -0
  33. rem/api/routers/shared_sessions.py +406 -0
  34. rem/auth/__init__.py +13 -3
  35. rem/auth/middleware.py +186 -22
  36. rem/auth/providers/__init__.py +4 -1
  37. rem/auth/providers/email.py +215 -0
  38. rem/cli/commands/README.md +237 -64
  39. rem/cli/commands/cluster.py +1808 -0
  40. rem/cli/commands/configure.py +4 -7
  41. rem/cli/commands/db.py +386 -143
  42. rem/cli/commands/experiments.py +468 -76
  43. rem/cli/commands/process.py +14 -8
  44. rem/cli/commands/schema.py +97 -50
  45. rem/cli/commands/session.py +336 -0
  46. rem/cli/dreaming.py +2 -2
  47. rem/cli/main.py +29 -6
  48. rem/config.py +10 -3
  49. rem/models/core/core_model.py +7 -1
  50. rem/models/core/experiment.py +58 -14
  51. rem/models/core/rem_query.py +5 -2
  52. rem/models/entities/__init__.py +25 -0
  53. rem/models/entities/domain_resource.py +38 -0
  54. rem/models/entities/feedback.py +123 -0
  55. rem/models/entities/message.py +30 -1
  56. rem/models/entities/ontology.py +1 -1
  57. rem/models/entities/ontology_config.py +1 -1
  58. rem/models/entities/session.py +83 -0
  59. rem/models/entities/shared_session.py +180 -0
  60. rem/models/entities/subscriber.py +175 -0
  61. rem/models/entities/user.py +1 -0
  62. rem/registry.py +10 -4
  63. rem/schemas/agents/core/agent-builder.yaml +134 -0
  64. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  65. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  66. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  67. rem/schemas/agents/rem.yaml +7 -3
  68. rem/services/__init__.py +3 -1
  69. rem/services/content/service.py +92 -19
  70. rem/services/email/__init__.py +10 -0
  71. rem/services/email/service.py +459 -0
  72. rem/services/email/templates.py +360 -0
  73. rem/services/embeddings/api.py +4 -4
  74. rem/services/embeddings/worker.py +16 -16
  75. rem/services/phoenix/client.py +154 -14
  76. rem/services/postgres/README.md +197 -15
  77. rem/services/postgres/__init__.py +2 -1
  78. rem/services/postgres/diff_service.py +547 -0
  79. rem/services/postgres/pydantic_to_sqlalchemy.py +470 -140
  80. rem/services/postgres/repository.py +132 -0
  81. rem/services/postgres/schema_generator.py +205 -4
  82. rem/services/postgres/service.py +6 -6
  83. rem/services/rem/parser.py +44 -9
  84. rem/services/rem/service.py +36 -2
  85. rem/services/session/compression.py +137 -51
  86. rem/services/session/reload.py +15 -8
  87. rem/settings.py +515 -27
  88. rem/sql/background_indexes.sql +21 -16
  89. rem/sql/migrations/001_install.sql +387 -54
  90. rem/sql/migrations/002_install_models.sql +2304 -377
  91. rem/sql/migrations/003_optional_extensions.sql +326 -0
  92. rem/sql/migrations/004_cache_system.sql +548 -0
  93. rem/sql/migrations/005_schema_update.sql +145 -0
  94. rem/utils/README.md +45 -0
  95. rem/utils/__init__.py +18 -0
  96. rem/utils/date_utils.py +2 -2
  97. rem/utils/files.py +157 -1
  98. rem/utils/model_helpers.py +156 -1
  99. rem/utils/schema_loader.py +220 -22
  100. rem/utils/sql_paths.py +146 -0
  101. rem/utils/sql_types.py +3 -1
  102. rem/utils/vision.py +1 -1
  103. rem/workers/__init__.py +3 -1
  104. rem/workers/db_listener.py +579 -0
  105. rem/workers/unlogged_maintainer.py +463 -0
  106. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/METADATA +340 -229
  107. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/RECORD +109 -80
  108. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/WHEEL +1 -1
  109. rem/sql/002_install_models.sql +0 -1068
  110. rem/sql/install_models.sql +0 -1051
  111. rem/sql/migrations/003_seed_default_user.sql +0 -48
  112. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,406 @@
1
+ """
2
+ Session sharing endpoints.
3
+
4
+ Enables session sharing between users for collaborative access to conversation history.
5
+
6
+ Endpoints:
7
+ POST /api/v1/sessions/{session_id}/share - Share a session with another user
8
+ DELETE /api/v1/sessions/{session_id}/share/{user_id} - Revoke a share (soft delete)
9
+ GET /api/v1/sessions/shared-with-me - Get users sharing sessions with you
10
+ GET /api/v1/sessions/shared-with-me/{user_id}/messages - Get messages from a user's shared sessions
11
+
12
+ See src/rem/models/entities/shared_session.py for full documentation.
13
+ """
14
+
15
+ from typing import Literal
16
+
17
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
18
+ from loguru import logger
19
+ from pydantic import BaseModel, Field
20
+
21
+ from ..deps import get_current_user, require_auth
22
+ from ...models.entities import (
23
+ Message,
24
+ SharedSession,
25
+ SharedSessionCreate,
26
+ SharedWithMeResponse,
27
+ SharedWithMeSummary,
28
+ )
29
+ from ...services.postgres import get_postgres_service
30
+ from ...settings import settings
31
+ from ...utils.date_utils import utc_now
32
+
33
+ router = APIRouter(prefix="/api/v1")
34
+
35
+
36
+ async def get_connected_postgres():
37
+ """Get a connected PostgresService instance."""
38
+ pg = get_postgres_service()
39
+ if pg and not pg.pool:
40
+ await pg.connect()
41
+ return pg
42
+
43
+
44
+ # =============================================================================
45
+ # Request/Response Models
46
+ # =============================================================================
47
+
48
+
49
+ class PaginationMetadata(BaseModel):
50
+ """Pagination metadata for paginated responses."""
51
+
52
+ total: int = Field(description="Total number of records matching filters")
53
+ page: int = Field(description="Current page number (1-indexed)")
54
+ page_size: int = Field(description="Number of records per page")
55
+ total_pages: int = Field(description="Total number of pages")
56
+ has_next: bool = Field(description="Whether there are more pages after this one")
57
+ has_previous: bool = Field(description="Whether there are pages before this one")
58
+
59
+
60
+ class SharedMessagesResponse(BaseModel):
61
+ """Response for shared messages query."""
62
+
63
+ object: Literal["list"] = "list"
64
+ data: list[Message] = Field(description="List of messages from shared sessions")
65
+ metadata: PaginationMetadata = Field(description="Pagination metadata")
66
+
67
+
68
+ class ShareSessionResponse(BaseModel):
69
+ """Response after sharing a session."""
70
+
71
+ success: bool = True
72
+ message: str
73
+ share: SharedSession
74
+
75
+
76
+ # =============================================================================
77
+ # Share Session Endpoints
78
+ # =============================================================================
79
+
80
+
81
+ @router.post(
82
+ "/sessions/{session_id}/share",
83
+ response_model=ShareSessionResponse,
84
+ status_code=201,
85
+ tags=["sessions"],
86
+ )
87
+ async def share_session(
88
+ request: Request,
89
+ session_id: str,
90
+ body: SharedSessionCreate,
91
+ user: dict = Depends(require_auth),
92
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
93
+ ) -> ShareSessionResponse:
94
+ """
95
+ Share a session with another user.
96
+
97
+ Creates a SharedSession record that grants the recipient access to view
98
+ messages in this session.
99
+
100
+ Args:
101
+ session_id: The session to share
102
+ body: Contains shared_with_user_id - the recipient
103
+
104
+ Returns:
105
+ The created SharedSession record
106
+
107
+ Raises:
108
+ 400: Session already shared with this user
109
+ 503: Database not enabled
110
+ """
111
+ if not settings.postgres.enabled:
112
+ raise HTTPException(status_code=503, detail="Database not enabled")
113
+
114
+ current_user_id = user.get("id", "default")
115
+ pg = await get_connected_postgres()
116
+
117
+ # Check if share already exists (active)
118
+ existing = await pg.fetchrow(
119
+ """
120
+ SELECT id FROM shared_sessions
121
+ WHERE tenant_id = $1
122
+ AND session_id = $2
123
+ AND owner_user_id = $3
124
+ AND shared_with_user_id = $4
125
+ AND deleted_at IS NULL
126
+ """,
127
+ x_tenant_id,
128
+ session_id,
129
+ current_user_id,
130
+ body.shared_with_user_id,
131
+ )
132
+
133
+ if existing:
134
+ raise HTTPException(
135
+ status_code=400,
136
+ detail=f"Session '{session_id}' is already shared with user '{body.shared_with_user_id}'",
137
+ )
138
+
139
+ # Create the share
140
+ result = await pg.fetchrow(
141
+ """
142
+ INSERT INTO shared_sessions (session_id, owner_user_id, shared_with_user_id, tenant_id)
143
+ VALUES ($1, $2, $3, $4)
144
+ RETURNING id, session_id, owner_user_id, shared_with_user_id, tenant_id, created_at, updated_at, deleted_at
145
+ """,
146
+ session_id,
147
+ current_user_id,
148
+ body.shared_with_user_id,
149
+ x_tenant_id,
150
+ )
151
+
152
+ share = SharedSession(
153
+ id=result["id"],
154
+ session_id=result["session_id"],
155
+ owner_user_id=result["owner_user_id"],
156
+ shared_with_user_id=result["shared_with_user_id"],
157
+ tenant_id=result["tenant_id"],
158
+ created_at=result["created_at"],
159
+ updated_at=result["updated_at"],
160
+ deleted_at=result["deleted_at"],
161
+ )
162
+
163
+ logger.debug(
164
+ f"User {current_user_id} shared session '{session_id}' with {body.shared_with_user_id}"
165
+ )
166
+
167
+ return ShareSessionResponse(
168
+ success=True,
169
+ message=f"Session shared with {body.shared_with_user_id}",
170
+ share=share,
171
+ )
172
+
173
+
174
+ @router.delete(
175
+ "/sessions/{session_id}/share/{shared_with_user_id}",
176
+ status_code=200,
177
+ tags=["sessions"],
178
+ )
179
+ async def remove_session_share(
180
+ request: Request,
181
+ session_id: str,
182
+ shared_with_user_id: str,
183
+ user: dict = Depends(require_auth),
184
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
185
+ ) -> dict:
186
+ """
187
+ Remove a session share (soft delete).
188
+
189
+ Sets deleted_at on the SharedSession record. The share can be re-created
190
+ later if needed.
191
+
192
+ Args:
193
+ session_id: The session to unshare
194
+ shared_with_user_id: The user to remove access from
195
+
196
+ Returns:
197
+ Success message
198
+
199
+ Raises:
200
+ 404: Share not found
201
+ 503: Database not enabled
202
+ """
203
+ if not settings.postgres.enabled:
204
+ raise HTTPException(status_code=503, detail="Database not enabled")
205
+
206
+ current_user_id = user.get("id", "default")
207
+ pg = await get_connected_postgres()
208
+
209
+ # Soft delete the share
210
+ result = await pg.fetchrow(
211
+ """
212
+ UPDATE shared_sessions
213
+ SET deleted_at = $1, updated_at = $1
214
+ WHERE tenant_id = $2
215
+ AND session_id = $3
216
+ AND owner_user_id = $4
217
+ AND shared_with_user_id = $5
218
+ AND deleted_at IS NULL
219
+ RETURNING id
220
+ """,
221
+ utc_now(),
222
+ x_tenant_id,
223
+ session_id,
224
+ current_user_id,
225
+ shared_with_user_id,
226
+ )
227
+
228
+ if not result:
229
+ raise HTTPException(
230
+ status_code=404,
231
+ detail=f"Share not found for session '{session_id}' with user '{shared_with_user_id}'",
232
+ )
233
+
234
+ logger.debug(
235
+ f"User {current_user_id} removed share for session '{session_id}' with {shared_with_user_id}"
236
+ )
237
+
238
+ return {
239
+ "success": True,
240
+ "message": f"Share removed for user {shared_with_user_id}",
241
+ }
242
+
243
+
244
+ # =============================================================================
245
+ # Shared With Me Endpoints
246
+ # =============================================================================
247
+
248
+
249
+ @router.get(
250
+ "/sessions/shared-with-me",
251
+ response_model=SharedWithMeResponse,
252
+ tags=["sessions"],
253
+ )
254
+ async def get_shared_with_me(
255
+ request: Request,
256
+ page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
257
+ page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
258
+ user: dict = Depends(require_auth),
259
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
260
+ ) -> SharedWithMeResponse:
261
+ """
262
+ Get aggregate summary of users sharing sessions with you.
263
+
264
+ Returns a paginated list of users who have shared sessions with the
265
+ current user, including message counts and date ranges.
266
+
267
+ Each entry shows:
268
+ - user_id, name, email of the person sharing
269
+ - message_count: total messages across all their shared sessions
270
+ - session_count: number of sessions they've shared
271
+ - first_message_at, last_message_at: date range
272
+
273
+ Results are ordered by most recent message first.
274
+ """
275
+ if not settings.postgres.enabled:
276
+ raise HTTPException(status_code=503, detail="Database not enabled")
277
+
278
+ current_user_id = user.get("id", "default")
279
+ pg = await get_connected_postgres()
280
+ offset = (page - 1) * page_size
281
+
282
+ # Get total count
283
+ count_result = await pg.fetchrow(
284
+ "SELECT fn_count_shared_with_me($1, $2) as total",
285
+ x_tenant_id,
286
+ current_user_id,
287
+ )
288
+ total = count_result["total"] if count_result else 0
289
+
290
+ # Get paginated results
291
+ rows = await pg.fetch(
292
+ "SELECT * FROM fn_get_shared_with_me($1, $2, $3, $4)",
293
+ x_tenant_id,
294
+ current_user_id,
295
+ page_size,
296
+ offset,
297
+ )
298
+
299
+ data = [
300
+ SharedWithMeSummary(
301
+ user_id=row["user_id"],
302
+ name=row["name"],
303
+ email=row["email"],
304
+ message_count=row["message_count"],
305
+ session_count=row["session_count"],
306
+ first_message_at=row["first_message_at"],
307
+ last_message_at=row["last_message_at"],
308
+ )
309
+ for row in rows
310
+ ]
311
+
312
+ total_pages = (total + page_size - 1) // page_size if total > 0 else 1
313
+
314
+ return SharedWithMeResponse(
315
+ data=data,
316
+ metadata={
317
+ "total": total,
318
+ "page": page,
319
+ "page_size": page_size,
320
+ "total_pages": total_pages,
321
+ "has_next": page < total_pages,
322
+ "has_previous": page > 1,
323
+ },
324
+ )
325
+
326
+
327
+ @router.get(
328
+ "/sessions/shared-with-me/{owner_user_id}/messages",
329
+ response_model=SharedMessagesResponse,
330
+ tags=["sessions"],
331
+ )
332
+ async def get_shared_messages(
333
+ request: Request,
334
+ owner_user_id: str,
335
+ page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
336
+ page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
337
+ user: dict = Depends(require_auth),
338
+ x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
339
+ ) -> SharedMessagesResponse:
340
+ """
341
+ Get messages from sessions shared by a specific user.
342
+
343
+ Returns paginated messages from all sessions that owner_user_id has
344
+ shared with the current user. Messages are ordered by created_at DESC.
345
+
346
+ Args:
347
+ owner_user_id: The user who shared the sessions
348
+
349
+ Returns:
350
+ Paginated list of Message objects
351
+ """
352
+ if not settings.postgres.enabled:
353
+ raise HTTPException(status_code=503, detail="Database not enabled")
354
+
355
+ current_user_id = user.get("id", "default")
356
+ pg = await get_connected_postgres()
357
+ offset = (page - 1) * page_size
358
+
359
+ # Get total count
360
+ count_result = await pg.fetchrow(
361
+ "SELECT fn_count_shared_messages($1, $2, $3) as total",
362
+ x_tenant_id,
363
+ current_user_id,
364
+ owner_user_id,
365
+ )
366
+ total = count_result["total"] if count_result else 0
367
+
368
+ # Get paginated messages
369
+ rows = await pg.fetch(
370
+ "SELECT * FROM fn_get_shared_messages($1, $2, $3, $4, $5)",
371
+ x_tenant_id,
372
+ current_user_id,
373
+ owner_user_id,
374
+ page_size,
375
+ offset,
376
+ )
377
+
378
+ # Convert to Message objects
379
+ data = [
380
+ Message(
381
+ id=row["id"],
382
+ content=row["content"],
383
+ message_type=row["message_type"],
384
+ session_id=row["session_id"],
385
+ model=row["model"],
386
+ token_count=row["token_count"],
387
+ created_at=row["created_at"],
388
+ metadata=row["metadata"] or {},
389
+ tenant_id=x_tenant_id,
390
+ )
391
+ for row in rows
392
+ ]
393
+
394
+ total_pages = (total + page_size - 1) // page_size if total > 0 else 1
395
+
396
+ return SharedMessagesResponse(
397
+ data=data,
398
+ metadata=PaginationMetadata(
399
+ total=total,
400
+ page=page,
401
+ page_size=page_size,
402
+ total_pages=total_pages,
403
+ has_next=page < total_pages,
404
+ has_previous=page > 1,
405
+ ),
406
+ )
rem/auth/__init__.py CHANGED
@@ -1,26 +1,36 @@
1
1
  """
2
2
  REM Authentication Module.
3
3
 
4
- OAuth 2.1 compliant authentication with support for:
4
+ Authentication with support for:
5
+ - Email passwordless login (verification codes)
5
6
  - Google OAuth
6
7
  - Microsoft Entra ID (Azure AD) OIDC
7
8
  - Custom OIDC providers
8
9
 
9
10
  Design Pattern:
10
11
  - Provider-agnostic base classes
11
- - PKCE (Proof Key for Code Exchange) for all flows
12
+ - PKCE (Proof Key for Code Exchange) for OAuth flows
12
13
  - State parameter for CSRF protection
13
14
  - Nonce for ID token replay protection
14
15
  - Token validation with JWKS
15
- - Clean separation: providers/ for OAuth logic, middleware.py for FastAPI integration
16
+ - Clean separation: providers/ for auth logic, middleware.py for FastAPI integration
17
+
18
+ Email Auth Flow:
19
+ 1. POST /api/auth/email/send-code with {email}
20
+ 2. User receives code via email
21
+ 3. POST /api/auth/email/verify with {email, code}
22
+ 4. Session created, user authenticated
16
23
  """
17
24
 
18
25
  from .providers.base import OAuthProvider
26
+ from .providers.email import EmailAuthProvider, EmailAuthResult
19
27
  from .providers.google import GoogleOAuthProvider
20
28
  from .providers.microsoft import MicrosoftOAuthProvider
21
29
 
22
30
  __all__ = [
23
31
  "OAuthProvider",
32
+ "EmailAuthProvider",
33
+ "EmailAuthResult",
24
34
  "GoogleOAuthProvider",
25
35
  "MicrosoftOAuthProvider",
26
36
  ]