remdb 0.3.242__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 -0
- rem/agentic/README.md +760 -0
- rem/agentic/__init__.py +54 -0
- rem/agentic/agents/README.md +155 -0
- rem/agentic/agents/__init__.py +38 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +425 -0
- rem/agentic/context_builder.py +360 -0
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/__init__.py +0 -0
- rem/agentic/mcp/tool_wrapper.py +273 -0
- rem/agentic/otel/__init__.py +5 -0
- rem/agentic/otel/setup.py +240 -0
- rem/agentic/providers/phoenix.py +926 -0
- rem/agentic/providers/pydantic_ai.py +854 -0
- rem/agentic/query.py +117 -0
- rem/agentic/query_helper.py +89 -0
- rem/agentic/schema.py +737 -0
- rem/agentic/serialization.py +245 -0
- rem/agentic/tools/__init__.py +5 -0
- rem/agentic/tools/rem_tools.py +242 -0
- rem/api/README.md +657 -0
- rem/api/deps.py +253 -0
- rem/api/main.py +460 -0
- rem/api/mcp_router/prompts.py +182 -0
- rem/api/mcp_router/resources.py +820 -0
- rem/api/mcp_router/server.py +243 -0
- rem/api/mcp_router/tools.py +1605 -0
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +520 -0
- rem/api/routers/auth.py +898 -0
- rem/api/routers/chat/__init__.py +5 -0
- rem/api/routers/chat/child_streaming.py +394 -0
- rem/api/routers/chat/completions.py +702 -0
- rem/api/routers/chat/json_utils.py +76 -0
- rem/api/routers/chat/models.py +202 -0
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +546 -0
- rem/api/routers/chat/streaming.py +950 -0
- rem/api/routers/chat/streaming_utils.py +327 -0
- rem/api/routers/common.py +18 -0
- rem/api/routers/dev.py +87 -0
- rem/api/routers/feedback.py +276 -0
- rem/api/routers/messages.py +620 -0
- rem/api/routers/models.py +86 -0
- rem/api/routers/query.py +362 -0
- rem/api/routers/shared_sessions.py +422 -0
- rem/auth/README.md +258 -0
- rem/auth/__init__.py +36 -0
- rem/auth/jwt.py +367 -0
- rem/auth/middleware.py +318 -0
- rem/auth/providers/__init__.py +16 -0
- rem/auth/providers/base.py +376 -0
- rem/auth/providers/email.py +215 -0
- rem/auth/providers/google.py +163 -0
- rem/auth/providers/microsoft.py +237 -0
- rem/cli/README.md +517 -0
- rem/cli/__init__.py +8 -0
- rem/cli/commands/README.md +299 -0
- rem/cli/commands/__init__.py +3 -0
- rem/cli/commands/ask.py +549 -0
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +495 -0
- rem/cli/commands/db.py +828 -0
- rem/cli/commands/dreaming.py +324 -0
- rem/cli/commands/experiments.py +1698 -0
- rem/cli/commands/mcp.py +66 -0
- rem/cli/commands/process.py +388 -0
- rem/cli/commands/query.py +109 -0
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +230 -0
- rem/cli/commands/serve.py +106 -0
- rem/cli/commands/session.py +453 -0
- rem/cli/dreaming.py +363 -0
- rem/cli/main.py +123 -0
- rem/config.py +244 -0
- rem/mcp_server.py +41 -0
- rem/models/core/__init__.py +49 -0
- rem/models/core/core_model.py +70 -0
- rem/models/core/engram.py +333 -0
- rem/models/core/experiment.py +672 -0
- rem/models/core/inline_edge.py +132 -0
- rem/models/core/rem_query.py +246 -0
- rem/models/entities/__init__.py +68 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/file.py +57 -0
- rem/models/entities/image_resource.py +88 -0
- rem/models/entities/message.py +64 -0
- rem/models/entities/moment.py +123 -0
- rem/models/entities/ontology.py +181 -0
- rem/models/entities/ontology_config.py +131 -0
- rem/models/entities/resource.py +95 -0
- rem/models/entities/schema.py +87 -0
- rem/models/entities/session.py +84 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +93 -0
- rem/py.typed +0 -0
- rem/registry.py +373 -0
- rem/schemas/README.md +507 -0
- rem/schemas/__init__.py +6 -0
- rem/schemas/agents/README.md +92 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/core/moment-builder.yaml +178 -0
- rem/schemas/agents/core/rem-query-agent.yaml +226 -0
- rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
- rem/schemas/agents/core/simple-assistant.yaml +19 -0
- rem/schemas/agents/core/user-profile-builder.yaml +163 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
- rem/schemas/agents/examples/contract-extractor.yaml +134 -0
- rem/schemas/agents/examples/cv-parser.yaml +263 -0
- rem/schemas/agents/examples/hello-world.yaml +37 -0
- rem/schemas/agents/examples/query.yaml +54 -0
- rem/schemas/agents/examples/simple.yaml +21 -0
- rem/schemas/agents/examples/test.yaml +29 -0
- rem/schemas/agents/rem.yaml +132 -0
- rem/schemas/evaluators/hello-world/default.yaml +77 -0
- rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
- rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
- rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
- rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
- rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
- rem/services/__init__.py +18 -0
- rem/services/audio/INTEGRATION.md +308 -0
- rem/services/audio/README.md +376 -0
- rem/services/audio/__init__.py +15 -0
- rem/services/audio/chunker.py +354 -0
- rem/services/audio/transcriber.py +259 -0
- rem/services/content/README.md +1269 -0
- rem/services/content/__init__.py +5 -0
- rem/services/content/providers.py +760 -0
- rem/services/content/service.py +762 -0
- rem/services/dreaming/README.md +230 -0
- rem/services/dreaming/__init__.py +53 -0
- rem/services/dreaming/affinity_service.py +322 -0
- rem/services/dreaming/moment_service.py +251 -0
- rem/services/dreaming/ontology_service.py +54 -0
- rem/services/dreaming/user_model_service.py +297 -0
- rem/services/dreaming/utils.py +39 -0
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +522 -0
- rem/services/email/templates.py +360 -0
- rem/services/embeddings/__init__.py +11 -0
- rem/services/embeddings/api.py +127 -0
- rem/services/embeddings/worker.py +435 -0
- rem/services/fs/README.md +662 -0
- rem/services/fs/__init__.py +62 -0
- rem/services/fs/examples.py +206 -0
- rem/services/fs/examples_paths.py +204 -0
- rem/services/fs/git_provider.py +935 -0
- rem/services/fs/local_provider.py +760 -0
- rem/services/fs/parsing-hooks-examples.md +172 -0
- rem/services/fs/paths.py +276 -0
- rem/services/fs/provider.py +460 -0
- rem/services/fs/s3_provider.py +1042 -0
- rem/services/fs/service.py +186 -0
- rem/services/git/README.md +1075 -0
- rem/services/git/__init__.py +17 -0
- rem/services/git/service.py +469 -0
- rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
- rem/services/phoenix/README.md +453 -0
- rem/services/phoenix/__init__.py +46 -0
- rem/services/phoenix/client.py +960 -0
- rem/services/phoenix/config.py +88 -0
- rem/services/phoenix/prompt_labels.py +477 -0
- rem/services/postgres/README.md +757 -0
- rem/services/postgres/__init__.py +49 -0
- rem/services/postgres/diff_service.py +599 -0
- rem/services/postgres/migration_service.py +427 -0
- rem/services/postgres/programmable_diff_service.py +635 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
- rem/services/postgres/register_type.py +353 -0
- rem/services/postgres/repository.py +481 -0
- rem/services/postgres/schema_generator.py +661 -0
- rem/services/postgres/service.py +802 -0
- rem/services/postgres/sql_builder.py +355 -0
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +318 -0
- rem/services/rem/__init__.py +23 -0
- rem/services/rem/exceptions.py +71 -0
- rem/services/rem/executor.py +293 -0
- rem/services/rem/parser.py +180 -0
- rem/services/rem/queries.py +196 -0
- rem/services/rem/query.py +371 -0
- rem/services/rem/service.py +608 -0
- rem/services/session/README.md +374 -0
- rem/services/session/__init__.py +13 -0
- rem/services/session/compression.py +488 -0
- rem/services/session/pydantic_messages.py +310 -0
- rem/services/session/reload.py +85 -0
- rem/services/user_service.py +130 -0
- rem/settings.py +1877 -0
- rem/sql/background_indexes.sql +52 -0
- rem/sql/migrations/001_install.sql +983 -0
- rem/sql/migrations/002_install_models.sql +3157 -0
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +282 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
- rem/utils/AGENTIC_CHUNKING.md +597 -0
- rem/utils/README.md +628 -0
- rem/utils/__init__.py +61 -0
- rem/utils/agentic_chunking.py +622 -0
- rem/utils/batch_ops.py +343 -0
- rem/utils/chunking.py +108 -0
- rem/utils/clip_embeddings.py +276 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/dict_utils.py +98 -0
- rem/utils/embeddings.py +436 -0
- rem/utils/examples/embeddings_example.py +305 -0
- rem/utils/examples/sql_types_example.py +202 -0
- rem/utils/files.py +323 -0
- rem/utils/markdown.py +16 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +492 -0
- rem/utils/schema_loader.py +649 -0
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +350 -0
- rem/utils/user_id.py +81 -0
- rem/utils/vision.py +325 -0
- rem/workers/README.md +506 -0
- rem/workers/__init__.py +7 -0
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/dreaming.py +502 -0
- rem/workers/engram_processor.py +312 -0
- rem/workers/sqs_file_processor.py +193 -0
- rem/workers/unlogged_maintainer.py +463 -0
- remdb-0.3.242.dist-info/METADATA +1632 -0
- remdb-0.3.242.dist-info/RECORD +235 -0
- remdb-0.3.242.dist-info/WHEEL +4 -0
- remdb-0.3.242.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,422 @@
|
|
|
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 .common import ErrorResponse
|
|
22
|
+
|
|
23
|
+
from ..deps import get_current_user, require_auth
|
|
24
|
+
from ...models.entities import (
|
|
25
|
+
Message,
|
|
26
|
+
SharedSession,
|
|
27
|
+
SharedSessionCreate,
|
|
28
|
+
SharedWithMeResponse,
|
|
29
|
+
SharedWithMeSummary,
|
|
30
|
+
)
|
|
31
|
+
from ...services.postgres import get_postgres_service
|
|
32
|
+
from ...settings import settings
|
|
33
|
+
from ...utils.date_utils import utc_now
|
|
34
|
+
|
|
35
|
+
router = APIRouter(prefix="/api/v1")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def get_connected_postgres():
|
|
39
|
+
"""Get a connected PostgresService instance."""
|
|
40
|
+
pg = get_postgres_service()
|
|
41
|
+
if pg and not pg.pool:
|
|
42
|
+
await pg.connect()
|
|
43
|
+
return pg
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# Request/Response Models
|
|
48
|
+
# =============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PaginationMetadata(BaseModel):
|
|
52
|
+
"""Pagination metadata for paginated responses."""
|
|
53
|
+
|
|
54
|
+
total: int = Field(description="Total number of records matching filters")
|
|
55
|
+
page: int = Field(description="Current page number (1-indexed)")
|
|
56
|
+
page_size: int = Field(description="Number of records per page")
|
|
57
|
+
total_pages: int = Field(description="Total number of pages")
|
|
58
|
+
has_next: bool = Field(description="Whether there are more pages after this one")
|
|
59
|
+
has_previous: bool = Field(description="Whether there are pages before this one")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SharedMessagesResponse(BaseModel):
|
|
63
|
+
"""Response for shared messages query."""
|
|
64
|
+
|
|
65
|
+
object: Literal["list"] = "list"
|
|
66
|
+
data: list[Message] = Field(description="List of messages from shared sessions")
|
|
67
|
+
metadata: PaginationMetadata = Field(description="Pagination metadata")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ShareSessionResponse(BaseModel):
|
|
71
|
+
"""Response after sharing a session."""
|
|
72
|
+
|
|
73
|
+
success: bool = True
|
|
74
|
+
message: str
|
|
75
|
+
share: SharedSession
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# =============================================================================
|
|
79
|
+
# Share Session Endpoints
|
|
80
|
+
# =============================================================================
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post(
|
|
84
|
+
"/sessions/{session_id}/share",
|
|
85
|
+
response_model=ShareSessionResponse,
|
|
86
|
+
status_code=201,
|
|
87
|
+
tags=["sessions"],
|
|
88
|
+
responses={
|
|
89
|
+
400: {"model": ErrorResponse, "description": "Session already shared with this user"},
|
|
90
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
async def share_session(
|
|
94
|
+
request: Request,
|
|
95
|
+
session_id: str,
|
|
96
|
+
body: SharedSessionCreate,
|
|
97
|
+
user: dict = Depends(require_auth),
|
|
98
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
99
|
+
) -> ShareSessionResponse:
|
|
100
|
+
"""
|
|
101
|
+
Share a session with another user.
|
|
102
|
+
|
|
103
|
+
Creates a SharedSession record that grants the recipient access to view
|
|
104
|
+
messages in this session.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
session_id: The session to share
|
|
108
|
+
body: Contains shared_with_user_id - the recipient
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The created SharedSession record
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
400: Session already shared with this user
|
|
115
|
+
503: Database not enabled
|
|
116
|
+
"""
|
|
117
|
+
if not settings.postgres.enabled:
|
|
118
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
119
|
+
|
|
120
|
+
current_user_id = user.get("id", "default")
|
|
121
|
+
pg = await get_connected_postgres()
|
|
122
|
+
|
|
123
|
+
# Check if share already exists (active)
|
|
124
|
+
existing = await pg.fetchrow(
|
|
125
|
+
"""
|
|
126
|
+
SELECT id FROM shared_sessions
|
|
127
|
+
WHERE tenant_id = $1
|
|
128
|
+
AND session_id = $2
|
|
129
|
+
AND owner_user_id = $3
|
|
130
|
+
AND shared_with_user_id = $4
|
|
131
|
+
AND deleted_at IS NULL
|
|
132
|
+
""",
|
|
133
|
+
x_tenant_id,
|
|
134
|
+
session_id,
|
|
135
|
+
current_user_id,
|
|
136
|
+
body.shared_with_user_id,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if existing:
|
|
140
|
+
raise HTTPException(
|
|
141
|
+
status_code=400,
|
|
142
|
+
detail=f"Session '{session_id}' is already shared with user '{body.shared_with_user_id}'",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Create the share
|
|
146
|
+
result = await pg.fetchrow(
|
|
147
|
+
"""
|
|
148
|
+
INSERT INTO shared_sessions (session_id, owner_user_id, shared_with_user_id, tenant_id)
|
|
149
|
+
VALUES ($1, $2, $3, $4)
|
|
150
|
+
RETURNING id, session_id, owner_user_id, shared_with_user_id, tenant_id, created_at, updated_at, deleted_at
|
|
151
|
+
""",
|
|
152
|
+
session_id,
|
|
153
|
+
current_user_id,
|
|
154
|
+
body.shared_with_user_id,
|
|
155
|
+
x_tenant_id,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
share = SharedSession(
|
|
159
|
+
id=result["id"],
|
|
160
|
+
session_id=result["session_id"],
|
|
161
|
+
owner_user_id=result["owner_user_id"],
|
|
162
|
+
shared_with_user_id=result["shared_with_user_id"],
|
|
163
|
+
tenant_id=result["tenant_id"],
|
|
164
|
+
created_at=result["created_at"],
|
|
165
|
+
updated_at=result["updated_at"],
|
|
166
|
+
deleted_at=result["deleted_at"],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
logger.debug(
|
|
170
|
+
f"User {current_user_id} shared session '{session_id}' with {body.shared_with_user_id}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return ShareSessionResponse(
|
|
174
|
+
success=True,
|
|
175
|
+
message=f"Session shared with {body.shared_with_user_id}",
|
|
176
|
+
share=share,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@router.delete(
|
|
181
|
+
"/sessions/{session_id}/share/{shared_with_user_id}",
|
|
182
|
+
status_code=200,
|
|
183
|
+
tags=["sessions"],
|
|
184
|
+
responses={
|
|
185
|
+
404: {"model": ErrorResponse, "description": "Share not found"},
|
|
186
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
async def remove_session_share(
|
|
190
|
+
request: Request,
|
|
191
|
+
session_id: str,
|
|
192
|
+
shared_with_user_id: str,
|
|
193
|
+
user: dict = Depends(require_auth),
|
|
194
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
195
|
+
) -> dict:
|
|
196
|
+
"""
|
|
197
|
+
Remove a session share (soft delete).
|
|
198
|
+
|
|
199
|
+
Sets deleted_at on the SharedSession record. The share can be re-created
|
|
200
|
+
later if needed.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
session_id: The session to unshare
|
|
204
|
+
shared_with_user_id: The user to remove access from
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Success message
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
404: Share not found
|
|
211
|
+
503: Database not enabled
|
|
212
|
+
"""
|
|
213
|
+
if not settings.postgres.enabled:
|
|
214
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
215
|
+
|
|
216
|
+
current_user_id = user.get("id", "default")
|
|
217
|
+
pg = await get_connected_postgres()
|
|
218
|
+
|
|
219
|
+
# Soft delete the share
|
|
220
|
+
result = await pg.fetchrow(
|
|
221
|
+
"""
|
|
222
|
+
UPDATE shared_sessions
|
|
223
|
+
SET deleted_at = $1, updated_at = $1
|
|
224
|
+
WHERE tenant_id = $2
|
|
225
|
+
AND session_id = $3
|
|
226
|
+
AND owner_user_id = $4
|
|
227
|
+
AND shared_with_user_id = $5
|
|
228
|
+
AND deleted_at IS NULL
|
|
229
|
+
RETURNING id
|
|
230
|
+
""",
|
|
231
|
+
utc_now(),
|
|
232
|
+
x_tenant_id,
|
|
233
|
+
session_id,
|
|
234
|
+
current_user_id,
|
|
235
|
+
shared_with_user_id,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not result:
|
|
239
|
+
raise HTTPException(
|
|
240
|
+
status_code=404,
|
|
241
|
+
detail=f"Share not found for session '{session_id}' with user '{shared_with_user_id}'",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
logger.debug(
|
|
245
|
+
f"User {current_user_id} removed share for session '{session_id}' with {shared_with_user_id}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"success": True,
|
|
250
|
+
"message": f"Share removed for user {shared_with_user_id}",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# =============================================================================
|
|
255
|
+
# Shared With Me Endpoints
|
|
256
|
+
# =============================================================================
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
@router.get(
|
|
260
|
+
"/sessions/shared-with-me",
|
|
261
|
+
response_model=SharedWithMeResponse,
|
|
262
|
+
tags=["sessions"],
|
|
263
|
+
responses={
|
|
264
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
async def get_shared_with_me(
|
|
268
|
+
request: Request,
|
|
269
|
+
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
|
270
|
+
page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
|
|
271
|
+
user: dict = Depends(require_auth),
|
|
272
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
273
|
+
) -> SharedWithMeResponse:
|
|
274
|
+
"""
|
|
275
|
+
Get aggregate summary of users sharing sessions with you.
|
|
276
|
+
|
|
277
|
+
Returns a paginated list of users who have shared sessions with the
|
|
278
|
+
current user, including message counts and date ranges.
|
|
279
|
+
|
|
280
|
+
Each entry shows:
|
|
281
|
+
- user_id, name, email of the person sharing
|
|
282
|
+
- message_count: total messages across all their shared sessions
|
|
283
|
+
- session_count: number of sessions they've shared
|
|
284
|
+
- first_message_at, last_message_at: date range
|
|
285
|
+
|
|
286
|
+
Results are ordered by most recent message first.
|
|
287
|
+
"""
|
|
288
|
+
if not settings.postgres.enabled:
|
|
289
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
290
|
+
|
|
291
|
+
current_user_id = user.get("id", "default")
|
|
292
|
+
pg = await get_connected_postgres()
|
|
293
|
+
offset = (page - 1) * page_size
|
|
294
|
+
|
|
295
|
+
# Get total count
|
|
296
|
+
count_result = await pg.fetchrow(
|
|
297
|
+
"SELECT fn_count_shared_with_me($1, $2) as total",
|
|
298
|
+
x_tenant_id,
|
|
299
|
+
current_user_id,
|
|
300
|
+
)
|
|
301
|
+
total = count_result["total"] if count_result else 0
|
|
302
|
+
|
|
303
|
+
# Get paginated results
|
|
304
|
+
rows = await pg.fetch(
|
|
305
|
+
"SELECT * FROM fn_get_shared_with_me($1, $2, $3, $4)",
|
|
306
|
+
x_tenant_id,
|
|
307
|
+
current_user_id,
|
|
308
|
+
page_size,
|
|
309
|
+
offset,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
data = [
|
|
313
|
+
SharedWithMeSummary(
|
|
314
|
+
user_id=row["user_id"],
|
|
315
|
+
name=row["name"],
|
|
316
|
+
email=row["email"],
|
|
317
|
+
message_count=row["message_count"],
|
|
318
|
+
session_count=row["session_count"],
|
|
319
|
+
first_message_at=row["first_message_at"],
|
|
320
|
+
last_message_at=row["last_message_at"],
|
|
321
|
+
)
|
|
322
|
+
for row in rows
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
326
|
+
|
|
327
|
+
return SharedWithMeResponse(
|
|
328
|
+
data=data,
|
|
329
|
+
metadata={
|
|
330
|
+
"total": total,
|
|
331
|
+
"page": page,
|
|
332
|
+
"page_size": page_size,
|
|
333
|
+
"total_pages": total_pages,
|
|
334
|
+
"has_next": page < total_pages,
|
|
335
|
+
"has_previous": page > 1,
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
@router.get(
|
|
341
|
+
"/sessions/shared-with-me/{owner_user_id}/messages",
|
|
342
|
+
response_model=SharedMessagesResponse,
|
|
343
|
+
tags=["sessions"],
|
|
344
|
+
responses={
|
|
345
|
+
503: {"model": ErrorResponse, "description": "Database not enabled"},
|
|
346
|
+
},
|
|
347
|
+
)
|
|
348
|
+
async def get_shared_messages(
|
|
349
|
+
request: Request,
|
|
350
|
+
owner_user_id: str,
|
|
351
|
+
page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
|
|
352
|
+
page_size: int = Query(default=50, ge=1, le=100, description="Results per page"),
|
|
353
|
+
user: dict = Depends(require_auth),
|
|
354
|
+
x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
|
|
355
|
+
) -> SharedMessagesResponse:
|
|
356
|
+
"""
|
|
357
|
+
Get messages from sessions shared by a specific user.
|
|
358
|
+
|
|
359
|
+
Returns paginated messages from all sessions that owner_user_id has
|
|
360
|
+
shared with the current user. Messages are ordered by created_at DESC.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
owner_user_id: The user who shared the sessions
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Paginated list of Message objects
|
|
367
|
+
"""
|
|
368
|
+
if not settings.postgres.enabled:
|
|
369
|
+
raise HTTPException(status_code=503, detail="Database not enabled")
|
|
370
|
+
|
|
371
|
+
current_user_id = user.get("id", "default")
|
|
372
|
+
pg = await get_connected_postgres()
|
|
373
|
+
offset = (page - 1) * page_size
|
|
374
|
+
|
|
375
|
+
# Get total count
|
|
376
|
+
count_result = await pg.fetchrow(
|
|
377
|
+
"SELECT fn_count_shared_messages($1, $2, $3) as total",
|
|
378
|
+
x_tenant_id,
|
|
379
|
+
current_user_id,
|
|
380
|
+
owner_user_id,
|
|
381
|
+
)
|
|
382
|
+
total = count_result["total"] if count_result else 0
|
|
383
|
+
|
|
384
|
+
# Get paginated messages
|
|
385
|
+
rows = await pg.fetch(
|
|
386
|
+
"SELECT * FROM fn_get_shared_messages($1, $2, $3, $4, $5)",
|
|
387
|
+
x_tenant_id,
|
|
388
|
+
current_user_id,
|
|
389
|
+
owner_user_id,
|
|
390
|
+
page_size,
|
|
391
|
+
offset,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Convert to Message objects
|
|
395
|
+
data = [
|
|
396
|
+
Message(
|
|
397
|
+
id=row["id"],
|
|
398
|
+
content=row["content"],
|
|
399
|
+
message_type=row["message_type"],
|
|
400
|
+
session_id=row["session_id"],
|
|
401
|
+
model=row["model"],
|
|
402
|
+
token_count=row["token_count"],
|
|
403
|
+
created_at=row["created_at"],
|
|
404
|
+
metadata=row["metadata"] or {},
|
|
405
|
+
tenant_id=x_tenant_id,
|
|
406
|
+
)
|
|
407
|
+
for row in rows
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
total_pages = (total + page_size - 1) // page_size if total > 0 else 1
|
|
411
|
+
|
|
412
|
+
return SharedMessagesResponse(
|
|
413
|
+
data=data,
|
|
414
|
+
metadata=PaginationMetadata(
|
|
415
|
+
total=total,
|
|
416
|
+
page=page,
|
|
417
|
+
page_size=page_size,
|
|
418
|
+
total_pages=total_pages,
|
|
419
|
+
has_next=page < total_pages,
|
|
420
|
+
has_previous=page > 1,
|
|
421
|
+
),
|
|
422
|
+
)
|
rem/auth/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# OAuth 2.1 Authentication
|
|
2
|
+
|
|
3
|
+
OAuth 2.1 compliant authentication with Google and Microsoft Entra ID.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **OAuth 2.1 Security Best Practices**
|
|
8
|
+
- PKCE (Proof Key for Code Exchange) - mandatory for all flows
|
|
9
|
+
- State parameter for CSRF protection
|
|
10
|
+
- Nonce for ID token replay protection
|
|
11
|
+
- Token validation with JWKS
|
|
12
|
+
|
|
13
|
+
- **Supported Providers**
|
|
14
|
+
- Google OAuth 2.0 / OIDC
|
|
15
|
+
- Microsoft Entra ID (Azure AD) OIDC
|
|
16
|
+
|
|
17
|
+
- **Minimal Code**
|
|
18
|
+
- Leverages Authlib for standards compliance
|
|
19
|
+
- Authlib handles PKCE, token exchange, JWKS validation
|
|
20
|
+
- Clean integration with FastAPI
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install authlib httpx
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
### Google OAuth Setup
|
|
31
|
+
|
|
32
|
+
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
|
|
33
|
+
2. Create OAuth 2.0 credentials
|
|
34
|
+
3. Add authorized redirect URI: `http://localhost:8000/api/auth/google/callback`
|
|
35
|
+
4. Set environment variables:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
AUTH__ENABLED=true
|
|
39
|
+
AUTH__SESSION_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
|
|
40
|
+
|
|
41
|
+
AUTH__GOOGLE__CLIENT_ID=your-client-id.apps.googleusercontent.com
|
|
42
|
+
AUTH__GOOGLE__CLIENT_SECRET=your-client-secret
|
|
43
|
+
AUTH__GOOGLE__REDIRECT_URI=http://localhost:8000/api/auth/google/callback
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Microsoft Entra ID Setup
|
|
47
|
+
|
|
48
|
+
1. Go to [Azure Portal](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps)
|
|
49
|
+
2. Register new application
|
|
50
|
+
3. Create client secret under "Certificates & secrets"
|
|
51
|
+
4. Add redirect URI: `http://localhost:8000/api/auth/microsoft/callback`
|
|
52
|
+
5. Add API permissions: Microsoft Graph > User.Read (delegated)
|
|
53
|
+
6. Set environment variables:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
AUTH__ENABLED=true
|
|
57
|
+
AUTH__SESSION_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")
|
|
58
|
+
|
|
59
|
+
AUTH__MICROSOFT__CLIENT_ID=your-application-id
|
|
60
|
+
AUTH__MICROSOFT__CLIENT_SECRET=your-client-secret
|
|
61
|
+
AUTH__MICROSOFT__REDIRECT_URI=http://localhost:8000/api/auth/microsoft/callback
|
|
62
|
+
AUTH__MICROSOFT__TENANT=common # or your tenant ID
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Tenant options:**
|
|
66
|
+
- `common` - Multi-tenant + personal Microsoft accounts
|
|
67
|
+
- `organizations` - Work/school accounts only
|
|
68
|
+
- `consumers` - Personal Microsoft accounts only
|
|
69
|
+
- `{tenant-id}` - Single tenant (specific organization)
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
### 1. Start the API server
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd rem
|
|
77
|
+
uv run python -m rem.api.main
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. Initiate login
|
|
81
|
+
|
|
82
|
+
Navigate to:
|
|
83
|
+
- Google: `http://localhost:8000/api/auth/google/login`
|
|
84
|
+
- Microsoft: `http://localhost:8000/api/auth/microsoft/login`
|
|
85
|
+
|
|
86
|
+
### 3. OAuth Flow
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
User Browser API Server OAuth Provider
|
|
90
|
+
| | | |
|
|
91
|
+
|-- Click Login ---->| | |
|
|
92
|
+
| |-- GET /auth/google/login --> |
|
|
93
|
+
| | |-- Generate PKCE ------->|
|
|
94
|
+
| | | (code_verifier) |
|
|
95
|
+
| |<-- Redirect to Google --| |
|
|
96
|
+
|<-- Show Google login --| | |
|
|
97
|
+
| | | |
|
|
98
|
+
|-- Enter credentials --> | |
|
|
99
|
+
| |-- Authorize ----------------------->| |
|
|
100
|
+
| |<-- Redirect with code ----------------| |
|
|
101
|
+
| | | |
|
|
102
|
+
| |-- GET /auth/google/callback?code=xyz ---------->|
|
|
103
|
+
| | |-- Exchange code ------->|
|
|
104
|
+
| | | + code_verifier |
|
|
105
|
+
| | |<-- Tokens --------------|
|
|
106
|
+
| | |-- Validate ID token --->|
|
|
107
|
+
| | | (JWKS) |
|
|
108
|
+
| |<-- Set session cookie --| |
|
|
109
|
+
|<-- Redirect to app ---| | |
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 4. Access protected endpoints
|
|
113
|
+
|
|
114
|
+
After login, session cookie is set automatically:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Get current user
|
|
118
|
+
curl http://localhost:8000/api/auth/me \
|
|
119
|
+
-H "Cookie: rem_session=..."
|
|
120
|
+
|
|
121
|
+
# Protected API endpoint
|
|
122
|
+
curl http://localhost:8000/api/v1/chat/completions \
|
|
123
|
+
-H "Cookie: rem_session=..." \
|
|
124
|
+
-H "Content-Type: application/json" \
|
|
125
|
+
-d '{
|
|
126
|
+
"model": "anthropic:claude-sonnet-4-5-20250929",
|
|
127
|
+
"messages": [{"role": "user", "content": "Hello"}]
|
|
128
|
+
}'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 5. Logout
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
curl -X POST http://localhost:8000/api/auth/logout \
|
|
135
|
+
-H "Cookie: rem_session=..."
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API Endpoints
|
|
139
|
+
|
|
140
|
+
| Method | Path | Description |
|
|
141
|
+
|--------|------|-------------|
|
|
142
|
+
| GET | `/api/auth/google/login` | Initiate Google OAuth flow |
|
|
143
|
+
| GET | `/api/auth/google/callback` | Google OAuth callback |
|
|
144
|
+
| GET | `/api/auth/microsoft/login` | Initiate Microsoft OAuth flow |
|
|
145
|
+
| GET | `/api/auth/microsoft/callback` | Microsoft OAuth callback |
|
|
146
|
+
| POST | `/api/auth/logout` | Clear session |
|
|
147
|
+
| GET | `/api/auth/me` | Get current user info |
|
|
148
|
+
|
|
149
|
+
## Security Features
|
|
150
|
+
|
|
151
|
+
### OAuth 2.1 Compliance
|
|
152
|
+
|
|
153
|
+
- **PKCE**: All flows use code_challenge (S256 method)
|
|
154
|
+
- **State**: CSRF protection on all authorization requests
|
|
155
|
+
- **Nonce**: ID token replay protection
|
|
156
|
+
- **No implicit flow**: Only authorization code flow supported
|
|
157
|
+
- **JWKS validation**: ID tokens validated with provider's public keys
|
|
158
|
+
|
|
159
|
+
### Session Security
|
|
160
|
+
|
|
161
|
+
- **HTTPOnly cookies**: Session cookies not accessible to JavaScript
|
|
162
|
+
- **SameSite=Lax**: CSRF protection for cookie-based auth
|
|
163
|
+
- **Secure flag**: HTTPS-only cookies in production
|
|
164
|
+
- **Short expiration**: 1 hour session lifetime (configurable)
|
|
165
|
+
|
|
166
|
+
### Middleware Protection
|
|
167
|
+
|
|
168
|
+
- Protects `/api/v1/*` endpoints
|
|
169
|
+
- Excludes `/api/auth/*` and public endpoints
|
|
170
|
+
- Returns 401 for API requests (JSON)
|
|
171
|
+
- Redirects to login for browser requests
|
|
172
|
+
|
|
173
|
+
## Provider-Specific Features
|
|
174
|
+
|
|
175
|
+
### Google
|
|
176
|
+
|
|
177
|
+
- **Hosted domain restriction**: Limit to Google Workspace domain
|
|
178
|
+
- **Offline access**: Request refresh tokens
|
|
179
|
+
- **Incremental authorization**: Add scopes incrementally
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
AUTH__GOOGLE__HOSTED_DOMAIN=example.com # Google Workspace only
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Microsoft
|
|
186
|
+
|
|
187
|
+
- **Multi-tenant support**: common/organizations/consumers
|
|
188
|
+
- **Conditional access**: Honors Entra ID policies
|
|
189
|
+
- **Microsoft Graph**: Access user profile via Graph API
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
AUTH__MICROSOFT__TENANT=common # Multi-tenant
|
|
193
|
+
AUTH__MICROSOFT__TENANT=organizations # Work/school only
|
|
194
|
+
AUTH__MICROSOFT__TENANT=consumers # Personal accounts
|
|
195
|
+
AUTH__MICROSOFT__TENANT=contoso.com # Single tenant
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Architecture
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
rem/src/rem/auth/
|
|
202
|
+
├── __init__.py # Module exports
|
|
203
|
+
├── README.md # This file
|
|
204
|
+
├── middleware.py # FastAPI auth middleware
|
|
205
|
+
├── providers/ # OAuth provider implementations
|
|
206
|
+
│ ├── __init__.py
|
|
207
|
+
│ ├── base.py # Base OAuth provider (kept for reference)
|
|
208
|
+
│ ├── google.py # Google provider (kept for reference)
|
|
209
|
+
│ └── microsoft.py # Microsoft provider (kept for reference)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Note**: Provider classes in `providers/` are kept for reference but not used.
|
|
213
|
+
The implementation uses Authlib's built-in provider support via `server_metadata_url`.
|
|
214
|
+
|
|
215
|
+
## Testing
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Test Google login flow
|
|
219
|
+
open http://localhost:8000/api/auth/google/login
|
|
220
|
+
|
|
221
|
+
# Test Microsoft login flow
|
|
222
|
+
open http://localhost:8000/api/auth/microsoft/login
|
|
223
|
+
|
|
224
|
+
# Check current user
|
|
225
|
+
curl http://localhost:8000/api/auth/me
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Troubleshooting
|
|
229
|
+
|
|
230
|
+
### "Authentication is disabled"
|
|
231
|
+
|
|
232
|
+
Set `AUTH__ENABLED=true` in environment or `.env` file.
|
|
233
|
+
|
|
234
|
+
### "Unknown provider: google"
|
|
235
|
+
|
|
236
|
+
Check that `AUTH__GOOGLE__CLIENT_ID` is set. The router only registers providers with valid credentials.
|
|
237
|
+
|
|
238
|
+
### Redirect URI mismatch
|
|
239
|
+
|
|
240
|
+
Ensure redirect URI in environment matches exactly what's registered with provider:
|
|
241
|
+
- Google: Check Google Cloud Console > Credentials
|
|
242
|
+
- Microsoft: Check Azure Portal > App registrations > Authentication
|
|
243
|
+
|
|
244
|
+
### PKCE errors
|
|
245
|
+
|
|
246
|
+
Authlib handles PKCE automatically. If you see PKCE errors:
|
|
247
|
+
1. Clear browser cookies and sessions
|
|
248
|
+
2. Ensure session middleware is registered before auth router
|
|
249
|
+
3. Check that `AUTH__SESSION_SECRET` is set
|
|
250
|
+
|
|
251
|
+
## References
|
|
252
|
+
|
|
253
|
+
- [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11)
|
|
254
|
+
- [OIDC Core](https://openid.net/specs/openid-connect-core-1_0.html)
|
|
255
|
+
- [PKCE RFC](https://datatracker.ietf.org/doc/html/rfc7636)
|
|
256
|
+
- [Authlib Documentation](https://docs.authlib.org/en/latest/)
|
|
257
|
+
- [Google OAuth](https://developers.google.com/identity/protocols/oauth2)
|
|
258
|
+
- [Microsoft identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/)
|