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.

Files changed (235) hide show
  1. rem/__init__.py +129 -0
  2. rem/agentic/README.md +760 -0
  3. rem/agentic/__init__.py +54 -0
  4. rem/agentic/agents/README.md +155 -0
  5. rem/agentic/agents/__init__.py +38 -0
  6. rem/agentic/agents/agent_manager.py +311 -0
  7. rem/agentic/agents/sse_simulator.py +502 -0
  8. rem/agentic/context.py +425 -0
  9. rem/agentic/context_builder.py +360 -0
  10. rem/agentic/llm_provider_models.py +301 -0
  11. rem/agentic/mcp/__init__.py +0 -0
  12. rem/agentic/mcp/tool_wrapper.py +273 -0
  13. rem/agentic/otel/__init__.py +5 -0
  14. rem/agentic/otel/setup.py +240 -0
  15. rem/agentic/providers/phoenix.py +926 -0
  16. rem/agentic/providers/pydantic_ai.py +854 -0
  17. rem/agentic/query.py +117 -0
  18. rem/agentic/query_helper.py +89 -0
  19. rem/agentic/schema.py +737 -0
  20. rem/agentic/serialization.py +245 -0
  21. rem/agentic/tools/__init__.py +5 -0
  22. rem/agentic/tools/rem_tools.py +242 -0
  23. rem/api/README.md +657 -0
  24. rem/api/deps.py +253 -0
  25. rem/api/main.py +460 -0
  26. rem/api/mcp_router/prompts.py +182 -0
  27. rem/api/mcp_router/resources.py +820 -0
  28. rem/api/mcp_router/server.py +243 -0
  29. rem/api/mcp_router/tools.py +1605 -0
  30. rem/api/middleware/tracking.py +172 -0
  31. rem/api/routers/admin.py +520 -0
  32. rem/api/routers/auth.py +898 -0
  33. rem/api/routers/chat/__init__.py +5 -0
  34. rem/api/routers/chat/child_streaming.py +394 -0
  35. rem/api/routers/chat/completions.py +702 -0
  36. rem/api/routers/chat/json_utils.py +76 -0
  37. rem/api/routers/chat/models.py +202 -0
  38. rem/api/routers/chat/otel_utils.py +33 -0
  39. rem/api/routers/chat/sse_events.py +546 -0
  40. rem/api/routers/chat/streaming.py +950 -0
  41. rem/api/routers/chat/streaming_utils.py +327 -0
  42. rem/api/routers/common.py +18 -0
  43. rem/api/routers/dev.py +87 -0
  44. rem/api/routers/feedback.py +276 -0
  45. rem/api/routers/messages.py +620 -0
  46. rem/api/routers/models.py +86 -0
  47. rem/api/routers/query.py +362 -0
  48. rem/api/routers/shared_sessions.py +422 -0
  49. rem/auth/README.md +258 -0
  50. rem/auth/__init__.py +36 -0
  51. rem/auth/jwt.py +367 -0
  52. rem/auth/middleware.py +318 -0
  53. rem/auth/providers/__init__.py +16 -0
  54. rem/auth/providers/base.py +376 -0
  55. rem/auth/providers/email.py +215 -0
  56. rem/auth/providers/google.py +163 -0
  57. rem/auth/providers/microsoft.py +237 -0
  58. rem/cli/README.md +517 -0
  59. rem/cli/__init__.py +8 -0
  60. rem/cli/commands/README.md +299 -0
  61. rem/cli/commands/__init__.py +3 -0
  62. rem/cli/commands/ask.py +549 -0
  63. rem/cli/commands/cluster.py +1808 -0
  64. rem/cli/commands/configure.py +495 -0
  65. rem/cli/commands/db.py +828 -0
  66. rem/cli/commands/dreaming.py +324 -0
  67. rem/cli/commands/experiments.py +1698 -0
  68. rem/cli/commands/mcp.py +66 -0
  69. rem/cli/commands/process.py +388 -0
  70. rem/cli/commands/query.py +109 -0
  71. rem/cli/commands/scaffold.py +47 -0
  72. rem/cli/commands/schema.py +230 -0
  73. rem/cli/commands/serve.py +106 -0
  74. rem/cli/commands/session.py +453 -0
  75. rem/cli/dreaming.py +363 -0
  76. rem/cli/main.py +123 -0
  77. rem/config.py +244 -0
  78. rem/mcp_server.py +41 -0
  79. rem/models/core/__init__.py +49 -0
  80. rem/models/core/core_model.py +70 -0
  81. rem/models/core/engram.py +333 -0
  82. rem/models/core/experiment.py +672 -0
  83. rem/models/core/inline_edge.py +132 -0
  84. rem/models/core/rem_query.py +246 -0
  85. rem/models/entities/__init__.py +68 -0
  86. rem/models/entities/domain_resource.py +38 -0
  87. rem/models/entities/feedback.py +123 -0
  88. rem/models/entities/file.py +57 -0
  89. rem/models/entities/image_resource.py +88 -0
  90. rem/models/entities/message.py +64 -0
  91. rem/models/entities/moment.py +123 -0
  92. rem/models/entities/ontology.py +181 -0
  93. rem/models/entities/ontology_config.py +131 -0
  94. rem/models/entities/resource.py +95 -0
  95. rem/models/entities/schema.py +87 -0
  96. rem/models/entities/session.py +84 -0
  97. rem/models/entities/shared_session.py +180 -0
  98. rem/models/entities/subscriber.py +175 -0
  99. rem/models/entities/user.py +93 -0
  100. rem/py.typed +0 -0
  101. rem/registry.py +373 -0
  102. rem/schemas/README.md +507 -0
  103. rem/schemas/__init__.py +6 -0
  104. rem/schemas/agents/README.md +92 -0
  105. rem/schemas/agents/core/agent-builder.yaml +235 -0
  106. rem/schemas/agents/core/moment-builder.yaml +178 -0
  107. rem/schemas/agents/core/rem-query-agent.yaml +226 -0
  108. rem/schemas/agents/core/resource-affinity-assessor.yaml +99 -0
  109. rem/schemas/agents/core/simple-assistant.yaml +19 -0
  110. rem/schemas/agents/core/user-profile-builder.yaml +163 -0
  111. rem/schemas/agents/examples/contract-analyzer.yaml +317 -0
  112. rem/schemas/agents/examples/contract-extractor.yaml +134 -0
  113. rem/schemas/agents/examples/cv-parser.yaml +263 -0
  114. rem/schemas/agents/examples/hello-world.yaml +37 -0
  115. rem/schemas/agents/examples/query.yaml +54 -0
  116. rem/schemas/agents/examples/simple.yaml +21 -0
  117. rem/schemas/agents/examples/test.yaml +29 -0
  118. rem/schemas/agents/rem.yaml +132 -0
  119. rem/schemas/evaluators/hello-world/default.yaml +77 -0
  120. rem/schemas/evaluators/rem/faithfulness.yaml +219 -0
  121. rem/schemas/evaluators/rem/lookup-correctness.yaml +182 -0
  122. rem/schemas/evaluators/rem/retrieval-precision.yaml +199 -0
  123. rem/schemas/evaluators/rem/retrieval-recall.yaml +211 -0
  124. rem/schemas/evaluators/rem/search-correctness.yaml +192 -0
  125. rem/services/__init__.py +18 -0
  126. rem/services/audio/INTEGRATION.md +308 -0
  127. rem/services/audio/README.md +376 -0
  128. rem/services/audio/__init__.py +15 -0
  129. rem/services/audio/chunker.py +354 -0
  130. rem/services/audio/transcriber.py +259 -0
  131. rem/services/content/README.md +1269 -0
  132. rem/services/content/__init__.py +5 -0
  133. rem/services/content/providers.py +760 -0
  134. rem/services/content/service.py +762 -0
  135. rem/services/dreaming/README.md +230 -0
  136. rem/services/dreaming/__init__.py +53 -0
  137. rem/services/dreaming/affinity_service.py +322 -0
  138. rem/services/dreaming/moment_service.py +251 -0
  139. rem/services/dreaming/ontology_service.py +54 -0
  140. rem/services/dreaming/user_model_service.py +297 -0
  141. rem/services/dreaming/utils.py +39 -0
  142. rem/services/email/__init__.py +10 -0
  143. rem/services/email/service.py +522 -0
  144. rem/services/email/templates.py +360 -0
  145. rem/services/embeddings/__init__.py +11 -0
  146. rem/services/embeddings/api.py +127 -0
  147. rem/services/embeddings/worker.py +435 -0
  148. rem/services/fs/README.md +662 -0
  149. rem/services/fs/__init__.py +62 -0
  150. rem/services/fs/examples.py +206 -0
  151. rem/services/fs/examples_paths.py +204 -0
  152. rem/services/fs/git_provider.py +935 -0
  153. rem/services/fs/local_provider.py +760 -0
  154. rem/services/fs/parsing-hooks-examples.md +172 -0
  155. rem/services/fs/paths.py +276 -0
  156. rem/services/fs/provider.py +460 -0
  157. rem/services/fs/s3_provider.py +1042 -0
  158. rem/services/fs/service.py +186 -0
  159. rem/services/git/README.md +1075 -0
  160. rem/services/git/__init__.py +17 -0
  161. rem/services/git/service.py +469 -0
  162. rem/services/phoenix/EXPERIMENT_DESIGN.md +1146 -0
  163. rem/services/phoenix/README.md +453 -0
  164. rem/services/phoenix/__init__.py +46 -0
  165. rem/services/phoenix/client.py +960 -0
  166. rem/services/phoenix/config.py +88 -0
  167. rem/services/phoenix/prompt_labels.py +477 -0
  168. rem/services/postgres/README.md +757 -0
  169. rem/services/postgres/__init__.py +49 -0
  170. rem/services/postgres/diff_service.py +599 -0
  171. rem/services/postgres/migration_service.py +427 -0
  172. rem/services/postgres/programmable_diff_service.py +635 -0
  173. rem/services/postgres/pydantic_to_sqlalchemy.py +562 -0
  174. rem/services/postgres/register_type.py +353 -0
  175. rem/services/postgres/repository.py +481 -0
  176. rem/services/postgres/schema_generator.py +661 -0
  177. rem/services/postgres/service.py +802 -0
  178. rem/services/postgres/sql_builder.py +355 -0
  179. rem/services/rate_limit.py +113 -0
  180. rem/services/rem/README.md +318 -0
  181. rem/services/rem/__init__.py +23 -0
  182. rem/services/rem/exceptions.py +71 -0
  183. rem/services/rem/executor.py +293 -0
  184. rem/services/rem/parser.py +180 -0
  185. rem/services/rem/queries.py +196 -0
  186. rem/services/rem/query.py +371 -0
  187. rem/services/rem/service.py +608 -0
  188. rem/services/session/README.md +374 -0
  189. rem/services/session/__init__.py +13 -0
  190. rem/services/session/compression.py +488 -0
  191. rem/services/session/pydantic_messages.py +310 -0
  192. rem/services/session/reload.py +85 -0
  193. rem/services/user_service.py +130 -0
  194. rem/settings.py +1877 -0
  195. rem/sql/background_indexes.sql +52 -0
  196. rem/sql/migrations/001_install.sql +983 -0
  197. rem/sql/migrations/002_install_models.sql +3157 -0
  198. rem/sql/migrations/003_optional_extensions.sql +326 -0
  199. rem/sql/migrations/004_cache_system.sql +282 -0
  200. rem/sql/migrations/005_schema_update.sql +145 -0
  201. rem/sql/migrations/migrate_session_id_to_uuid.sql +45 -0
  202. rem/utils/AGENTIC_CHUNKING.md +597 -0
  203. rem/utils/README.md +628 -0
  204. rem/utils/__init__.py +61 -0
  205. rem/utils/agentic_chunking.py +622 -0
  206. rem/utils/batch_ops.py +343 -0
  207. rem/utils/chunking.py +108 -0
  208. rem/utils/clip_embeddings.py +276 -0
  209. rem/utils/constants.py +97 -0
  210. rem/utils/date_utils.py +228 -0
  211. rem/utils/dict_utils.py +98 -0
  212. rem/utils/embeddings.py +436 -0
  213. rem/utils/examples/embeddings_example.py +305 -0
  214. rem/utils/examples/sql_types_example.py +202 -0
  215. rem/utils/files.py +323 -0
  216. rem/utils/markdown.py +16 -0
  217. rem/utils/mime_types.py +158 -0
  218. rem/utils/model_helpers.py +492 -0
  219. rem/utils/schema_loader.py +649 -0
  220. rem/utils/sql_paths.py +146 -0
  221. rem/utils/sql_types.py +350 -0
  222. rem/utils/user_id.py +81 -0
  223. rem/utils/vision.py +325 -0
  224. rem/workers/README.md +506 -0
  225. rem/workers/__init__.py +7 -0
  226. rem/workers/db_listener.py +579 -0
  227. rem/workers/db_maintainer.py +74 -0
  228. rem/workers/dreaming.py +502 -0
  229. rem/workers/engram_processor.py +312 -0
  230. rem/workers/sqs_file_processor.py +193 -0
  231. rem/workers/unlogged_maintainer.py +463 -0
  232. remdb-0.3.242.dist-info/METADATA +1632 -0
  233. remdb-0.3.242.dist-info/RECORD +235 -0
  234. remdb-0.3.242.dist-info/WHEEL +4 -0
  235. remdb-0.3.242.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,620 @@
1
+ """
2
+ Messages and Sessions endpoints.
3
+
4
+ Provides endpoints for:
5
+ - Listing and filtering messages by date, user_id, session_id
6
+ - Creating and managing sessions (normal or evaluation mode)
7
+
8
+ Endpoints:
9
+ GET /api/v1/messages - List messages with filters
10
+ GET /api/v1/messages/{id} - Get a specific message
11
+
12
+ GET /api/v1/sessions - List sessions
13
+ POST /api/v1/sessions - Create a session
14
+ GET /api/v1/sessions/{id} - Get a specific session
15
+ PUT /api/v1/sessions/{id} - Update a session
16
+ """
17
+
18
+ from datetime import datetime
19
+ from enum import Enum
20
+ from typing import Literal
21
+ from uuid import UUID
22
+
23
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
24
+ from loguru import logger
25
+ from pydantic import BaseModel, Field
26
+
27
+ from .common import ErrorResponse
28
+
29
+ from ..deps import (
30
+ get_current_user,
31
+ get_user_filter,
32
+ is_admin,
33
+ require_admin,
34
+ require_auth,
35
+ )
36
+ from ...models.entities import Message, Session, SessionMode
37
+ from ...services.postgres import Repository, get_postgres_service
38
+ from ...settings import settings
39
+ from ...utils.date_utils import parse_iso, utc_now
40
+
41
+ router = APIRouter(prefix="/api/v1")
42
+
43
+
44
+ # =============================================================================
45
+ # Enums
46
+ # =============================================================================
47
+
48
+
49
+ class SortOrder(str, Enum):
50
+ """Sort order for list queries."""
51
+
52
+ ASC = "asc"
53
+ DESC = "desc"
54
+
55
+
56
+ # =============================================================================
57
+ # Request/Response Models
58
+ # =============================================================================
59
+
60
+
61
+ class MessageListResponse(BaseModel):
62
+ """Response for message list endpoint."""
63
+
64
+ object: Literal["list"] = "list"
65
+ data: list[Message]
66
+ total: int
67
+ has_more: bool
68
+
69
+
70
+ class SessionCreateRequest(BaseModel):
71
+ """Request to create a new session."""
72
+
73
+ name: str = Field(description="Session name/identifier")
74
+ mode: SessionMode = Field(
75
+ default=SessionMode.NORMAL, description="Session mode: 'normal' or 'evaluation'"
76
+ )
77
+ description: str | None = Field(default=None, description="Session description")
78
+ original_trace_id: str | None = Field(
79
+ default=None,
80
+ description="For evaluation: ID of the original session being evaluated",
81
+ )
82
+ settings_overrides: dict | None = Field(
83
+ default=None,
84
+ description="Settings overrides (model, temperature, max_tokens, system_prompt)",
85
+ )
86
+ prompt: str | None = Field(default=None, description="Custom prompt for this session")
87
+ agent_schema_uri: str | None = Field(
88
+ default=None, description="Agent schema URI for this session"
89
+ )
90
+
91
+
92
+ class SessionUpdateRequest(BaseModel):
93
+ """Request to update a session."""
94
+
95
+ description: str | None = None
96
+ settings_overrides: dict | None = None
97
+ prompt: str | None = None
98
+ message_count: int | None = None
99
+ total_tokens: int | None = None
100
+
101
+
102
+ class SessionListResponse(BaseModel):
103
+ """Response for session list endpoint (deprecated, use SessionsQueryResponse)."""
104
+
105
+ object: Literal["list"] = "list"
106
+ data: list[Session]
107
+ total: int
108
+ has_more: bool
109
+
110
+
111
+ class SessionWithUser(BaseModel):
112
+ """Session with user info for admin views."""
113
+
114
+ id: str
115
+ name: str
116
+ mode: str | None = None
117
+ description: str | None = None
118
+ user_id: str | None = None
119
+ user_name: str | None = None
120
+ user_email: str | None = None
121
+ message_count: int = 0
122
+ total_tokens: int | None = None
123
+ created_at: datetime | None = None
124
+ updated_at: datetime | None = None
125
+ metadata: dict | None = None
126
+
127
+
128
+ class PaginationMetadata(BaseModel):
129
+ """Pagination metadata for paginated responses."""
130
+
131
+ total: int = Field(description="Total number of records matching filters")
132
+ page: int = Field(description="Current page number (1-indexed)")
133
+ page_size: int = Field(description="Number of records per page")
134
+ total_pages: int = Field(description="Total number of pages")
135
+ has_next: bool = Field(description="Whether there are more pages after this one")
136
+ has_previous: bool = Field(description="Whether there are pages before this one")
137
+
138
+
139
+ class SessionsQueryResponse(BaseModel):
140
+ """Response for paginated sessions query."""
141
+
142
+ object: Literal["list"] = "list"
143
+ data: list[SessionWithUser] = Field(description="List of sessions for the current page")
144
+ metadata: PaginationMetadata = Field(description="Pagination metadata")
145
+
146
+
147
+ # =============================================================================
148
+ # Messages Endpoints
149
+ # =============================================================================
150
+
151
+
152
+ @router.get(
153
+ "/messages",
154
+ response_model=MessageListResponse,
155
+ tags=["messages"],
156
+ responses={
157
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
158
+ },
159
+ )
160
+ async def list_messages(
161
+ request: Request,
162
+ mine: bool = Query(default=False, description="Only show my messages (uses JWT identity)"),
163
+ user_id: str | None = Query(default=None, description="Filter by user ID (admin only for cross-user)"),
164
+ session_id: str | None = Query(default=None, description="Filter by session ID"),
165
+ start_date: str | None = Query(
166
+ default=None, description="Filter messages after this ISO date"
167
+ ),
168
+ end_date: str | None = Query(
169
+ default=None, description="Filter messages before this ISO date"
170
+ ),
171
+ message_type: str | None = Query(
172
+ default=None, description="Filter by message type (user, assistant, system, tool)"
173
+ ),
174
+ limit: int = Query(default=50, ge=1, le=100, description="Max results to return"),
175
+ offset: int = Query(default=0, ge=0, description="Offset for pagination"),
176
+ sort: SortOrder = Query(default=SortOrder.DESC, description="Sort order by created_at (asc or desc)"),
177
+ ) -> MessageListResponse:
178
+ """
179
+ List messages with optional filters.
180
+
181
+ Access Control:
182
+ - Regular users: Only see their own messages
183
+ - Admin users: Can filter by any user_id or see all messages
184
+ - mine=true: Forces filter to current user (useful for admins to see only their own)
185
+
186
+ Filters can be combined:
187
+ - mine: Only show messages owned by current JWT user (overrides user_id)
188
+ - user_id: Filter by the user who created/owns the message (admin only for cross-user)
189
+ - session_id: Filter by conversation session
190
+ - start_date/end_date: Filter by creation time range (ISO 8601 format)
191
+ - message_type: Filter by role (user, assistant, system, tool)
192
+ - sort: Sort order by created_at (asc or desc, default: desc)
193
+
194
+ Returns paginated results ordered by created_at.
195
+ """
196
+ if not settings.postgres.enabled:
197
+ raise HTTPException(status_code=503, detail="Database not enabled")
198
+
199
+ repo = Repository(Message, table_name="messages")
200
+
201
+ # Get current user for logging
202
+ current_user = get_current_user(request)
203
+ jwt_user_id = current_user.get("id") if current_user else None
204
+
205
+ # If mine=true, force filter to current user's ID from JWT
206
+ effective_user_id = user_id
207
+ if mine:
208
+ if current_user:
209
+ effective_user_id = current_user.get("id")
210
+
211
+ # Build user-scoped filters (admin can see all, regular users see only their own)
212
+ filters = await get_user_filter(request, x_user_id=effective_user_id)
213
+
214
+ # Apply optional filters
215
+ if session_id:
216
+ # session_id is the session UUID - use directly
217
+ filters["session_id"] = session_id
218
+ if message_type:
219
+ filters["message_type"] = message_type
220
+
221
+ # Log the query parameters for debugging
222
+ logger.debug(
223
+ f"[messages] Query: session_id={session_id} | "
224
+ f"jwt_user_id={jwt_user_id} | "
225
+ f"filters={filters}"
226
+ )
227
+
228
+ # Build order_by clause based on sort parameter
229
+ order_by = f"created_at {sort.value.upper()}"
230
+
231
+ # For date filtering, we need custom SQL (not supported by basic Repository)
232
+ # For now, fetch all matching base filters and filter in Python
233
+ # TODO: Extend Repository to support date range filters
234
+ messages = await repo.find(
235
+ filters,
236
+ order_by=order_by,
237
+ limit=limit + 1, # Fetch one extra to determine has_more
238
+ offset=offset,
239
+ )
240
+
241
+ # Apply date filters in Python if provided
242
+ if start_date or end_date:
243
+ start_dt = parse_iso(start_date) if start_date else None
244
+ end_dt = parse_iso(end_date) if end_date else None
245
+
246
+ filtered = []
247
+ for msg in messages:
248
+ if start_dt and msg.created_at < start_dt:
249
+ continue
250
+ if end_dt and msg.created_at > end_dt:
251
+ continue
252
+ filtered.append(msg)
253
+ messages = filtered
254
+
255
+ # Determine if there are more results
256
+ has_more = len(messages) > limit
257
+ if has_more:
258
+ messages = messages[:limit]
259
+
260
+ # Get total count for pagination info
261
+ total = await repo.count(filters)
262
+
263
+ # Log result count
264
+ logger.debug(
265
+ f"[messages] Result: returned={len(messages)} | total={total} | "
266
+ f"session_id={session_id}"
267
+ )
268
+
269
+ return MessageListResponse(data=messages, total=total, has_more=has_more)
270
+
271
+
272
+ @router.get(
273
+ "/messages/{message_id}",
274
+ response_model=Message,
275
+ tags=["messages"],
276
+ responses={
277
+ 403: {"model": ErrorResponse, "description": "Access denied: not owner"},
278
+ 404: {"model": ErrorResponse, "description": "Message not found"},
279
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
280
+ },
281
+ )
282
+ async def get_message(
283
+ request: Request,
284
+ message_id: str,
285
+ ) -> Message:
286
+ """
287
+ Get a specific message by ID.
288
+
289
+ Access Control:
290
+ - Regular users: Only access their own messages
291
+ - Admin users: Can access any message
292
+
293
+ Args:
294
+ message_id: UUID of the message
295
+
296
+ Returns:
297
+ Message object if found
298
+
299
+ Raises:
300
+ 404: Message not found
301
+ 403: Access denied (not owner and not admin)
302
+ """
303
+ if not settings.postgres.enabled:
304
+ raise HTTPException(status_code=503, detail="Database not enabled")
305
+
306
+ repo = Repository(Message, table_name="messages")
307
+ message = await repo.get_by_id(message_id)
308
+
309
+ if not message:
310
+ raise HTTPException(status_code=404, detail=f"Message '{message_id}' not found")
311
+
312
+ # Check access: admin or owner
313
+ current_user = get_current_user(request)
314
+ if not is_admin(current_user):
315
+ user_id = current_user.get("id") if current_user else None
316
+ if message.user_id and message.user_id != user_id:
317
+ raise HTTPException(status_code=403, detail="Access denied: not owner")
318
+
319
+ return message
320
+
321
+
322
+ # =============================================================================
323
+ # Sessions Endpoints
324
+ # =============================================================================
325
+
326
+
327
+ @router.get(
328
+ "/sessions",
329
+ response_model=SessionsQueryResponse,
330
+ tags=["sessions"],
331
+ responses={
332
+ 503: {"model": ErrorResponse, "description": "Database not enabled or connection failed"},
333
+ },
334
+ )
335
+ async def list_sessions(
336
+ request: Request,
337
+ user_id: str | None = Query(default=None, description="Filter by user ID (admin only for cross-user)"),
338
+ user_name: str | None = Query(default=None, description="Filter by user name (partial match, admin only)"),
339
+ user_email: str | None = Query(default=None, description="Filter by user email (partial match, admin only)"),
340
+ mode: SessionMode | None = Query(default=None, description="Filter by session mode"),
341
+ page: int = Query(default=1, ge=1, description="Page number (1-indexed)"),
342
+ page_size: int = Query(default=50, ge=1, le=100, description="Number of results per page"),
343
+ ) -> SessionsQueryResponse:
344
+ """
345
+ List sessions with optional filters and page-based pagination.
346
+
347
+ Access Control:
348
+ - Regular users: Only see their own sessions
349
+ - Admin users: Can filter by any user_id, user_name, user_email, or see all sessions
350
+
351
+ Filters:
352
+ - user_id: Filter by session owner (admin only for cross-user)
353
+ - user_name: Filter by user name partial match (admin only)
354
+ - user_email: Filter by user email partial match (admin only)
355
+ - mode: Filter by session mode (normal or evaluation)
356
+
357
+ Pagination:
358
+ - page: Page number (1-indexed, default: 1)
359
+ - page_size: Number of sessions per page (default: 50, max: 100)
360
+
361
+ Returns paginated results with user info ordered by created_at descending.
362
+ """
363
+ if not settings.postgres.enabled:
364
+ raise HTTPException(status_code=503, detail="Database not enabled")
365
+
366
+ current_user = get_current_user(request)
367
+ admin = is_admin(current_user)
368
+
369
+ # Get postgres service for raw SQL query
370
+ db = get_postgres_service()
371
+ if not db:
372
+ raise HTTPException(status_code=503, detail="Database connection failed")
373
+ if not db.pool:
374
+ await db.connect()
375
+
376
+ # Build effective filters based on user role
377
+ effective_user_id = user_id
378
+ effective_user_name = user_name if admin else None # Only admin can search by name
379
+ effective_user_email = user_email if admin else None # Only admin can search by email
380
+
381
+ if not admin:
382
+ # Non-admin users can only see their own sessions
383
+ effective_user_id = current_user.get("id") if current_user else None
384
+ if not effective_user_id:
385
+ # Anonymous user - return empty
386
+ return SessionsQueryResponse(
387
+ data=[],
388
+ metadata=PaginationMetadata(
389
+ total=0, page=page, page_size=page_size,
390
+ total_pages=0, has_next=False, has_previous=False,
391
+ ),
392
+ )
393
+
394
+ # Call the SQL function for sessions with user info
395
+ async with db.pool.acquire() as conn:
396
+ rows = await conn.fetch(
397
+ """
398
+ SELECT * FROM fn_list_sessions_with_user(
399
+ $1, $2, $3, $4, $5, $6
400
+ )
401
+ """,
402
+ effective_user_id,
403
+ effective_user_name,
404
+ effective_user_email,
405
+ mode.value if mode else None,
406
+ page,
407
+ page_size,
408
+ )
409
+
410
+ # Extract total from first row
411
+ total = rows[0]["total_count"] if rows else 0
412
+
413
+ # Convert rows to SessionWithUser
414
+ data = [
415
+ SessionWithUser(
416
+ id=str(row["id"]),
417
+ name=row["name"],
418
+ mode=row["mode"],
419
+ description=row["description"],
420
+ user_id=row["user_id"],
421
+ user_name=row["user_name"],
422
+ user_email=row["user_email"],
423
+ message_count=row["message_count"] or 0,
424
+ total_tokens=row["total_tokens"],
425
+ created_at=row["created_at"],
426
+ updated_at=row["updated_at"],
427
+ metadata=row["metadata"],
428
+ )
429
+ for row in rows
430
+ ]
431
+
432
+ total_pages = (total + page_size - 1) // page_size if total > 0 else 0
433
+
434
+ return SessionsQueryResponse(
435
+ data=data,
436
+ metadata=PaginationMetadata(
437
+ total=total,
438
+ page=page,
439
+ page_size=page_size,
440
+ total_pages=total_pages,
441
+ has_next=page < total_pages,
442
+ has_previous=page > 1,
443
+ ),
444
+ )
445
+
446
+
447
+ @router.post(
448
+ "/sessions",
449
+ response_model=Session,
450
+ status_code=201,
451
+ tags=["sessions"],
452
+ responses={
453
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
454
+ },
455
+ )
456
+ async def create_session(
457
+ request_body: SessionCreateRequest,
458
+ user: dict = Depends(require_admin),
459
+ x_user_id: str = Header(alias="X-User-Id", default="default"),
460
+ ) -> Session:
461
+ """
462
+ Create a new session.
463
+
464
+ **Requires admin role.**
465
+
466
+ For normal sessions, only name is required.
467
+ For evaluation sessions, you can specify:
468
+ - original_trace_id: The session being re-evaluated
469
+ - settings_overrides: Model, temperature, prompt overrides
470
+ - prompt: Custom prompt to test
471
+
472
+ Headers:
473
+ - X-User-Id: User identifier (owner of the session)
474
+
475
+ Returns:
476
+ Created session object
477
+ """
478
+ if not settings.postgres.enabled:
479
+ raise HTTPException(status_code=503, detail="Database not enabled")
480
+
481
+ # Admin can specify x_user_id, or default to their own
482
+ effective_user_id = x_user_id if x_user_id != "default" else user.get("id", "default")
483
+
484
+ session = Session(
485
+ name=request_body.name,
486
+ mode=request_body.mode,
487
+ description=request_body.description,
488
+ original_trace_id=request_body.original_trace_id,
489
+ settings_overrides=request_body.settings_overrides,
490
+ prompt=request_body.prompt,
491
+ agent_schema_uri=request_body.agent_schema_uri,
492
+ user_id=effective_user_id,
493
+ tenant_id="default", # tenant_id not used for filtering, set to default
494
+ )
495
+
496
+ repo = Repository(Session, table_name="sessions")
497
+ result = await repo.upsert(session)
498
+
499
+ logger.info(
500
+ f"Admin {user.get('email')} created session '{session.name}' "
501
+ f"(mode={session.mode}) for user={effective_user_id}"
502
+ )
503
+
504
+ return result # type: ignore
505
+
506
+
507
+ @router.get(
508
+ "/sessions/{session_id}",
509
+ response_model=Session,
510
+ tags=["sessions"],
511
+ responses={
512
+ 403: {"model": ErrorResponse, "description": "Access denied: not owner"},
513
+ 404: {"model": ErrorResponse, "description": "Session not found"},
514
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
515
+ },
516
+ )
517
+ async def get_session(
518
+ request: Request,
519
+ session_id: str,
520
+ ) -> Session:
521
+ """
522
+ Get a specific session by ID.
523
+
524
+ Access Control:
525
+ - Regular users: Only access their own sessions
526
+ - Admin users: Can access any session
527
+
528
+ Args:
529
+ session_id: UUID of the session
530
+
531
+ Returns:
532
+ Session object if found
533
+
534
+ Raises:
535
+ 404: Session not found
536
+ 403: Access denied (not owner and not admin)
537
+ """
538
+ if not settings.postgres.enabled:
539
+ raise HTTPException(status_code=503, detail="Database not enabled")
540
+
541
+ repo = Repository(Session, table_name="sessions")
542
+ session = await repo.get_by_id(session_id)
543
+
544
+ if not session:
545
+ raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
546
+
547
+ # Check access: admin or owner
548
+ current_user = get_current_user(request)
549
+ if not is_admin(current_user):
550
+ user_id = current_user.get("id") if current_user else None
551
+ if session.user_id and session.user_id != user_id:
552
+ raise HTTPException(status_code=403, detail="Access denied: not owner")
553
+
554
+ return session
555
+
556
+
557
+ @router.put(
558
+ "/sessions/{session_id}",
559
+ response_model=Session,
560
+ tags=["sessions"],
561
+ responses={
562
+ 403: {"model": ErrorResponse, "description": "Access denied: not owner"},
563
+ 404: {"model": ErrorResponse, "description": "Session not found"},
564
+ 503: {"model": ErrorResponse, "description": "Database not enabled"},
565
+ },
566
+ )
567
+ async def update_session(
568
+ request: Request,
569
+ session_id: str,
570
+ request_body: SessionUpdateRequest,
571
+ ) -> Session:
572
+ """
573
+ Update an existing session.
574
+
575
+ Access Control:
576
+ - Regular users: Only update their own sessions
577
+ - Admin users: Can update any session
578
+
579
+ Allows updating:
580
+ - description
581
+ - settings_overrides
582
+ - prompt
583
+ - message_count (typically updated automatically)
584
+ - total_tokens (typically updated automatically)
585
+
586
+ Args:
587
+ session_id: UUID of the session
588
+
589
+ Returns:
590
+ Updated session object
591
+
592
+ Raises:
593
+ 404: Session not found
594
+ 403: Access denied (not owner and not admin)
595
+ """
596
+ if not settings.postgres.enabled:
597
+ raise HTTPException(status_code=503, detail="Database not enabled")
598
+
599
+ repo = Repository(Session, table_name="sessions")
600
+ session = await repo.get_by_id(session_id)
601
+
602
+ if not session:
603
+ raise HTTPException(status_code=404, detail=f"Session '{session_id}' not found")
604
+
605
+ # Check access: admin or owner
606
+ current_user = get_current_user(request)
607
+ if not is_admin(current_user):
608
+ user_id = current_user.get("id") if current_user else None
609
+ if session.user_id and session.user_id != user_id:
610
+ raise HTTPException(status_code=403, detail="Access denied: not owner")
611
+
612
+ # Apply updates
613
+ update_data = request_body.model_dump(exclude_none=True)
614
+ for field, value in update_data.items():
615
+ setattr(session, field, value)
616
+
617
+ session.updated_at = utc_now()
618
+
619
+ result = await repo.update(session)
620
+ return result
@@ -0,0 +1,86 @@
1
+ """
2
+ Models endpoint - List available LLM models.
3
+
4
+ Provides an OpenAI-compatible /v1/models endpoint listing all supported
5
+ LLM providers and their models using the provider:model_id syntax.
6
+
7
+ Endpoint:
8
+ GET /api/v1/models - List all available models
9
+
10
+ Response format matches OpenAI API for drop-in compatibility.
11
+ """
12
+
13
+ from typing import Literal
14
+
15
+ from fastapi import APIRouter, HTTPException
16
+ from pydantic import BaseModel, Field
17
+
18
+ from .common import ErrorResponse
19
+
20
+ from rem.agentic.llm_provider_models import (
21
+ ModelInfo,
22
+ AVAILABLE_MODELS,
23
+ ALLOWED_MODEL_IDS,
24
+ is_valid_model,
25
+ get_valid_model_or_default,
26
+ get_model_by_id,
27
+ )
28
+
29
+ router = APIRouter(prefix="/api/v1", tags=["models"])
30
+
31
+ # Re-export for backwards compatibility
32
+ __all__ = [
33
+ "ModelInfo",
34
+ "AVAILABLE_MODELS",
35
+ "ALLOWED_MODEL_IDS",
36
+ "is_valid_model",
37
+ "get_valid_model_or_default",
38
+ "get_model_by_id",
39
+ ]
40
+
41
+
42
+ class ModelsResponse(BaseModel):
43
+ """Response from /models endpoint."""
44
+
45
+ object: Literal["list"] = "list"
46
+ data: list[ModelInfo]
47
+
48
+
49
+ @router.get("/models", response_model=ModelsResponse)
50
+ async def list_models() -> ModelsResponse:
51
+ """
52
+ List all available LLM models.
53
+
54
+ Returns models from all supported providers (OpenAI, Anthropic, Google, Cerebras)
55
+ with the provider:model_id naming convention.
56
+
57
+ Response format is OpenAI-compatible for drop-in replacement.
58
+ """
59
+ return ModelsResponse(data=AVAILABLE_MODELS)
60
+
61
+
62
+ @router.get(
63
+ "/models/{model_id:path}",
64
+ response_model=ModelInfo,
65
+ responses={
66
+ 404: {"model": ErrorResponse, "description": "Model not found"},
67
+ },
68
+ )
69
+ async def get_model(model_id: str) -> ModelInfo:
70
+ """
71
+ Get information about a specific model.
72
+
73
+ Args:
74
+ model_id: Model identifier in provider:model format (e.g., "openai:gpt-4.1")
75
+
76
+ Returns:
77
+ Model information if found
78
+
79
+ Raises:
80
+ HTTPException: 404 if model not found
81
+ """
82
+ model = get_model_by_id(model_id)
83
+ if model:
84
+ return model
85
+
86
+ raise HTTPException(status_code=404, detail=f"Model '{model_id}' not found")