remdb 0.3.103__py3-none-any.whl → 0.3.118__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 (55) hide show
  1. rem/agentic/context.py +28 -24
  2. rem/agentic/mcp/tool_wrapper.py +29 -3
  3. rem/agentic/otel/setup.py +92 -4
  4. rem/agentic/providers/pydantic_ai.py +88 -18
  5. rem/agentic/schema.py +358 -21
  6. rem/agentic/tools/rem_tools.py +3 -3
  7. rem/api/main.py +85 -16
  8. rem/api/mcp_router/resources.py +1 -1
  9. rem/api/mcp_router/server.py +18 -4
  10. rem/api/mcp_router/tools.py +383 -16
  11. rem/api/routers/admin.py +218 -1
  12. rem/api/routers/chat/completions.py +30 -3
  13. rem/api/routers/chat/streaming.py +143 -3
  14. rem/api/routers/feedback.py +12 -319
  15. rem/api/routers/query.py +360 -0
  16. rem/api/routers/shared_sessions.py +13 -13
  17. rem/cli/commands/README.md +237 -64
  18. rem/cli/commands/cluster.py +1300 -0
  19. rem/cli/commands/configure.py +1 -3
  20. rem/cli/commands/db.py +354 -143
  21. rem/cli/commands/process.py +14 -8
  22. rem/cli/commands/schema.py +92 -45
  23. rem/cli/main.py +27 -6
  24. rem/models/core/rem_query.py +5 -2
  25. rem/models/entities/shared_session.py +2 -28
  26. rem/registry.py +10 -4
  27. rem/services/content/service.py +30 -8
  28. rem/services/embeddings/api.py +4 -4
  29. rem/services/embeddings/worker.py +16 -16
  30. rem/services/postgres/README.md +151 -26
  31. rem/services/postgres/__init__.py +2 -1
  32. rem/services/postgres/diff_service.py +531 -0
  33. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  34. rem/services/postgres/schema_generator.py +205 -4
  35. rem/services/postgres/service.py +6 -6
  36. rem/services/rem/parser.py +44 -9
  37. rem/services/rem/service.py +36 -2
  38. rem/services/session/reload.py +1 -1
  39. rem/settings.py +56 -7
  40. rem/sql/background_indexes.sql +19 -24
  41. rem/sql/migrations/001_install.sql +252 -69
  42. rem/sql/migrations/002_install_models.sql +2171 -593
  43. rem/sql/migrations/003_optional_extensions.sql +326 -0
  44. rem/sql/migrations/004_cache_system.sql +548 -0
  45. rem/utils/__init__.py +18 -0
  46. rem/utils/date_utils.py +2 -2
  47. rem/utils/schema_loader.py +17 -13
  48. rem/utils/sql_paths.py +146 -0
  49. rem/workers/__init__.py +2 -1
  50. rem/workers/unlogged_maintainer.py +463 -0
  51. {remdb-0.3.103.dist-info → remdb-0.3.118.dist-info}/METADATA +149 -76
  52. {remdb-0.3.103.dist-info → remdb-0.3.118.dist-info}/RECORD +54 -48
  53. rem/sql/migrations/003_seed_default_user.sql +0 -48
  54. {remdb-0.3.103.dist-info → remdb-0.3.118.dist-info}/WHEEL +0 -0
  55. {remdb-0.3.103.dist-info → remdb-0.3.118.dist-info}/entry_points.txt +0 -0
@@ -1,35 +1,26 @@
1
1
  """
2
- Feedback endpoints for chat message and session feedback.
2
+ Message feedback endpoint.
3
3
 
4
- Provides endpoints for:
5
- - Submitting feedback on messages or sessions
6
- - Listing feedback with filters
7
- - Syncing feedback to Phoenix as annotations (async)
4
+ Provides endpoint for submitting feedback on messages.
8
5
 
9
6
  Endpoints:
10
- POST /api/v1/feedback - Submit feedback
11
- GET /api/v1/feedback - List feedback with filters
12
- GET /api/v1/feedback/{id} - Get specific feedback
13
- GET /api/v1/feedback/categories - List available categories
7
+ POST /api/v1/messages/feedback - Submit feedback on a message
14
8
 
15
9
  Trace Integration:
16
10
  - Feedback can reference trace_id/span_id for OTEL integration
17
- - Phoenix sync attaches feedback as span annotations
11
+ - Phoenix sync attaches feedback as span annotations (async)
18
12
  """
19
13
 
20
- from typing import Literal
21
-
22
- from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
14
+ from fastapi import APIRouter, Header, HTTPException, Request
23
15
  from loguru import logger
24
16
  from pydantic import BaseModel, Field
25
17
 
26
- from ..deps import get_current_user, get_user_filter, get_user_id_from_request, is_admin
27
- from ...models.entities import Feedback, FeedbackCategory, Message
18
+ from ..deps import get_user_id_from_request
19
+ from ...models.entities import Feedback, Message
28
20
  from ...services.postgres import Repository
29
21
  from ...settings import settings
30
- from ...utils.date_utils import utc_now
31
22
 
32
- router = APIRouter(prefix="/api/v1", tags=["feedback"])
23
+ router = APIRouter(prefix="/api/v1", tags=["messages"])
33
24
 
34
25
 
35
26
  # =============================================================================
@@ -51,10 +42,9 @@ class FeedbackCreateRequest(BaseModel):
51
42
  description="Rating: -1 (thumbs down), 1 (thumbs up), or 1-5 scale",
52
43
  )
53
44
  categories: list[str] = Field(
54
- default_factory=list, description="Selected feedback categories"
45
+ default_factory=list, description="Feedback categories"
55
46
  )
56
47
  comment: str | None = Field(default=None, description="Free-text comment")
57
- # Optional trace reference (can be auto-resolved from message)
58
48
  trace_id: str | None = Field(
59
49
  default=None, description="OTEL trace ID (auto-resolved if message has it)"
60
50
  )
@@ -78,132 +68,12 @@ class FeedbackResponse(BaseModel):
78
68
  created_at: str
79
69
 
80
70
 
81
- class FeedbackListResponse(BaseModel):
82
- """Response for feedback list endpoint."""
83
-
84
- object: Literal["list"] = "list"
85
- data: list[Feedback]
86
- total: int
87
- has_more: bool
88
-
89
-
90
- class CategoryInfo(BaseModel):
91
- """Information about a feedback category."""
92
-
93
- value: str
94
- label: str
95
- description: str
96
- sentiment: Literal["positive", "negative", "neutral"]
97
-
98
-
99
- class CategoriesResponse(BaseModel):
100
- """Response for categories endpoint."""
101
-
102
- categories: list[CategoryInfo]
103
-
104
-
105
- # =============================================================================
106
- # Category Definitions
107
71
  # =============================================================================
108
-
109
- CATEGORY_INFO: dict[str, CategoryInfo] = {
110
- FeedbackCategory.INCOMPLETE.value: CategoryInfo(
111
- value=FeedbackCategory.INCOMPLETE.value,
112
- label="Incomplete",
113
- description="Response lacks expected information",
114
- sentiment="negative",
115
- ),
116
- FeedbackCategory.INACCURATE.value: CategoryInfo(
117
- value=FeedbackCategory.INACCURATE.value,
118
- label="Inaccurate",
119
- description="Response contains factual errors",
120
- sentiment="negative",
121
- ),
122
- FeedbackCategory.POOR_TONE.value: CategoryInfo(
123
- value=FeedbackCategory.POOR_TONE.value,
124
- label="Poor Tone",
125
- description="Inappropriate or unprofessional tone",
126
- sentiment="negative",
127
- ),
128
- FeedbackCategory.OFF_TOPIC.value: CategoryInfo(
129
- value=FeedbackCategory.OFF_TOPIC.value,
130
- label="Off Topic",
131
- description="Response doesn't address the question",
132
- sentiment="negative",
133
- ),
134
- FeedbackCategory.TOO_VERBOSE.value: CategoryInfo(
135
- value=FeedbackCategory.TOO_VERBOSE.value,
136
- label="Too Verbose",
137
- description="Unnecessarily long response",
138
- sentiment="negative",
139
- ),
140
- FeedbackCategory.TOO_BRIEF.value: CategoryInfo(
141
- value=FeedbackCategory.TOO_BRIEF.value,
142
- label="Too Brief",
143
- description="Insufficiently detailed response",
144
- sentiment="negative",
145
- ),
146
- FeedbackCategory.CONFUSING.value: CategoryInfo(
147
- value=FeedbackCategory.CONFUSING.value,
148
- label="Confusing",
149
- description="Hard to understand or unclear",
150
- sentiment="negative",
151
- ),
152
- FeedbackCategory.UNSAFE.value: CategoryInfo(
153
- value=FeedbackCategory.UNSAFE.value,
154
- label="Unsafe",
155
- description="Contains potentially harmful content",
156
- sentiment="negative",
157
- ),
158
- FeedbackCategory.HELPFUL.value: CategoryInfo(
159
- value=FeedbackCategory.HELPFUL.value,
160
- label="Helpful",
161
- description="Response was useful and addressed the need",
162
- sentiment="positive",
163
- ),
164
- FeedbackCategory.EXCELLENT.value: CategoryInfo(
165
- value=FeedbackCategory.EXCELLENT.value,
166
- label="Excellent",
167
- description="Exceptionally good response",
168
- sentiment="positive",
169
- ),
170
- FeedbackCategory.ACCURATE.value: CategoryInfo(
171
- value=FeedbackCategory.ACCURATE.value,
172
- label="Accurate",
173
- description="Factually correct and precise",
174
- sentiment="positive",
175
- ),
176
- FeedbackCategory.WELL_WRITTEN.value: CategoryInfo(
177
- value=FeedbackCategory.WELL_WRITTEN.value,
178
- label="Well Written",
179
- description="Clear, well-structured response",
180
- sentiment="positive",
181
- ),
182
- FeedbackCategory.OTHER.value: CategoryInfo(
183
- value=FeedbackCategory.OTHER.value,
184
- label="Other",
185
- description="Other feedback not covered by categories",
186
- sentiment="neutral",
187
- ),
188
- }
189
-
190
-
72
+ # Feedback Endpoint
191
73
  # =============================================================================
192
- # Feedback Endpoints
193
- # =============================================================================
194
-
195
-
196
- @router.get("/feedback/categories", response_model=CategoriesResponse)
197
- async def list_categories() -> CategoriesResponse:
198
- """
199
- List available feedback categories.
200
-
201
- Returns predefined categories with labels, descriptions, and sentiment.
202
- """
203
- return CategoriesResponse(categories=list(CATEGORY_INFO.values()))
204
74
 
205
75
 
206
- @router.post("/feedback", response_model=FeedbackResponse, status_code=201)
76
+ @router.post("/messages/feedback", response_model=FeedbackResponse, status_code=201)
207
77
  async def submit_feedback(
208
78
  request: Request,
209
79
  request_body: FeedbackCreateRequest,
@@ -219,15 +89,12 @@ async def submit_feedback(
219
89
  - Provided explicitly in the request
220
90
  - Auto-resolved from the message if message_id is provided
221
91
 
222
- Phoenix sync happens asynchronously after feedback is stored.
223
-
224
92
  Returns:
225
93
  Created feedback object
226
94
  """
227
95
  if not settings.postgres.enabled:
228
96
  raise HTTPException(status_code=503, detail="Database not enabled")
229
97
 
230
- # Get effective user_id from auth or anonymous tracking
231
98
  effective_user_id = get_user_id_from_request(request)
232
99
 
233
100
  # Resolve trace_id/span_id from message if not provided
@@ -235,14 +102,12 @@ async def submit_feedback(
235
102
  span_id = request_body.span_id
236
103
 
237
104
  if request_body.message_id and (not trace_id or not span_id):
238
- # Try to get trace info from the message
239
105
  message_repo = Repository(Message, table_name="messages")
240
106
  message = await message_repo.get_by_id(request_body.message_id, x_tenant_id)
241
107
  if message:
242
108
  trace_id = trace_id or message.trace_id
243
109
  span_id = span_id or message.span_id
244
110
 
245
- # Create feedback entity
246
111
  feedback = Feedback(
247
112
  session_id=request_body.session_id,
248
113
  message_id=request_body.message_id,
@@ -257,22 +122,17 @@ async def submit_feedback(
257
122
  tenant_id=x_tenant_id,
258
123
  )
259
124
 
260
- # Store feedback (table is "feedbacks" - plural)
261
125
  repo = Repository(Feedback, table_name="feedbacks")
262
126
  result = await repo.upsert(feedback)
263
127
 
264
128
  logger.info(
265
129
  f"Feedback submitted: session={request_body.session_id}, "
266
- f"message={request_body.message_id}, rating={request_body.rating}, "
267
- f"categories={request_body.categories}"
130
+ f"message={request_body.message_id}, rating={request_body.rating}"
268
131
  )
269
132
 
270
133
  # TODO: Async sync to Phoenix if trace_id/span_id available
271
- # This would be done via a background task or queue
272
134
  if trace_id and span_id:
273
135
  logger.debug(f"Feedback has trace info: trace={trace_id}, span={span_id}")
274
- # TODO: Queue for Phoenix annotation sync
275
- # await sync_feedback_to_phoenix(feedback)
276
136
 
277
137
  return FeedbackResponse(
278
138
  id=str(result.id),
@@ -286,170 +146,3 @@ async def submit_feedback(
286
146
  phoenix_synced=result.phoenix_synced,
287
147
  created_at=result.created_at.isoformat() if result.created_at else "",
288
148
  )
289
-
290
-
291
- @router.get("/feedback", response_model=FeedbackListResponse)
292
- async def list_feedback(
293
- request: Request,
294
- session_id: str | None = Query(default=None, description="Filter by session ID"),
295
- message_id: str | None = Query(default=None, description="Filter by message ID"),
296
- rating: int | None = Query(default=None, description="Filter by rating"),
297
- category: str | None = Query(default=None, description="Filter by category"),
298
- phoenix_synced: bool | None = Query(
299
- default=None, description="Filter by Phoenix sync status"
300
- ),
301
- limit: int = Query(default=50, ge=1, le=100, description="Max results"),
302
- offset: int = Query(default=0, ge=0, description="Offset for pagination"),
303
- x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
304
- ) -> FeedbackListResponse:
305
- """
306
- List feedback with optional filters.
307
-
308
- Access Control:
309
- - Regular users: Only see feedback they submitted
310
- - Admin users: Can see all feedback
311
-
312
- Filters:
313
- - session_id: Filter by session
314
- - message_id: Filter by specific message
315
- - rating: Filter by rating value
316
- - category: Filter by category (checks if category in list)
317
- - phoenix_synced: Filter by sync status
318
- """
319
- if not settings.postgres.enabled:
320
- raise HTTPException(status_code=503, detail="Database not enabled")
321
-
322
- repo = Repository(Feedback, table_name="feedbacks")
323
-
324
- # Build user-scoped filters (uses anon_id for anonymous users)
325
- filters = await get_user_filter(request, x_tenant_id=x_tenant_id)
326
-
327
- # Apply optional filters
328
- if session_id:
329
- filters["session_id"] = session_id
330
- if message_id:
331
- filters["message_id"] = message_id
332
- if rating is not None:
333
- filters["rating"] = rating
334
- if phoenix_synced is not None:
335
- filters["phoenix_synced"] = phoenix_synced
336
- # TODO: category filter requires array contains query
337
-
338
- feedback_list = await repo.find(
339
- filters,
340
- order_by="created_at DESC",
341
- limit=limit + 1,
342
- offset=offset,
343
- )
344
-
345
- # Filter by category in Python if specified (until Repository supports array contains)
346
- if category:
347
- feedback_list = [f for f in feedback_list if category in f.categories]
348
-
349
- has_more = len(feedback_list) > limit
350
- if has_more:
351
- feedback_list = feedback_list[:limit]
352
-
353
- total = await repo.count(filters)
354
-
355
- return FeedbackListResponse(data=feedback_list, total=total, has_more=has_more)
356
-
357
-
358
- @router.get("/feedback/{feedback_id}", response_model=Feedback)
359
- async def get_feedback(
360
- request: Request,
361
- feedback_id: str,
362
- x_tenant_id: str = Header(alias="X-Tenant-Id", default="default"),
363
- ) -> Feedback:
364
- """
365
- Get specific feedback by ID.
366
-
367
- Access Control:
368
- - Regular users: Only access their own feedback
369
- - Admin users: Can access any feedback
370
- """
371
- if not settings.postgres.enabled:
372
- raise HTTPException(status_code=503, detail="Database not enabled")
373
-
374
- repo = Repository(Feedback, table_name="feedbacks")
375
- feedback = await repo.get_by_id(feedback_id, x_tenant_id)
376
-
377
- if not feedback:
378
- raise HTTPException(status_code=404, detail=f"Feedback '{feedback_id}' not found")
379
-
380
- # Check access
381
- current_user = get_current_user(request)
382
- if not is_admin(current_user):
383
- user_id = current_user.get("id") if current_user else None
384
- if feedback.user_id and feedback.user_id != user_id:
385
- raise HTTPException(status_code=403, detail="Access denied: not owner")
386
-
387
- return feedback
388
-
389
-
390
- # =============================================================================
391
- # Phoenix Sync (Stub - TODO: Implement background task)
392
- # =============================================================================
393
-
394
-
395
- async def sync_feedback_to_phoenix(feedback: Feedback) -> bool:
396
- """
397
- Sync feedback to Phoenix as a span annotation.
398
-
399
- TODO: Implement this as a background task.
400
-
401
- This should:
402
- 1. Connect to Phoenix client
403
- 2. Resolve trace/span from feedback
404
- 3. Create annotation with feedback data
405
- 4. Update feedback.phoenix_synced = True
406
- 5. Store phoenix_annotation_id
407
-
408
- Args:
409
- feedback: Feedback entity to sync
410
-
411
- Returns:
412
- True if synced successfully
413
- """
414
- if not feedback.span_id:
415
- logger.warning(f"Cannot sync feedback {feedback.id}: no span_id")
416
- return False
417
-
418
- try:
419
- # TODO: Import and use Phoenix client
420
- # from ...services.phoenix import PhoenixClient
421
- # client = PhoenixClient()
422
- #
423
- # # Build annotation from feedback
424
- # label = None
425
- # if feedback.categories:
426
- # label = feedback.categories[0] # Primary category
427
- #
428
- # score = None
429
- # if feedback.rating:
430
- # # Normalize to 0-1 scale
431
- # if feedback.rating == -1:
432
- # score = 0.0
433
- # elif feedback.rating >= 1 and feedback.rating <= 5:
434
- # score = feedback.rating / 5.0
435
- #
436
- # client.add_span_feedback(
437
- # span_id=feedback.span_id,
438
- # annotation_name="user_feedback",
439
- # annotator_kind=feedback.annotator_kind,
440
- # label=label,
441
- # score=score,
442
- # explanation=feedback.comment,
443
- # )
444
- #
445
- # # Update feedback record
446
- # feedback.phoenix_synced = True
447
- # repo = Repository(Feedback, table_name="feedbacks")
448
- # await repo.update(feedback)
449
-
450
- logger.info(f"TODO: Sync feedback {feedback.id} to Phoenix span {feedback.span_id}")
451
- return True
452
-
453
- except Exception as e:
454
- logger.error(f"Failed to sync feedback to Phoenix: {e}")
455
- return False