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
@@ -0,0 +1,360 @@
1
+ """
2
+ REM Query API - Execute REM dialect or natural language queries.
3
+
4
+ Endpoints:
5
+ POST /api/v1/query - Execute a REM query
6
+
7
+ Modes:
8
+ - rem-dialect (default): Execute REM query syntax directly
9
+ Example: "LOOKUP sarah-chen", "SEARCH resources 'API design'", "TRAVERSE FROM doc-123 DEPTH 2"
10
+
11
+ - natural-language: Convert natural language to REM query via LLM agent
12
+ Example: "Find all documents by Sarah", "What meetings happened last week?"
13
+
14
+ - staged-plan: Execute a multi-stage query plan (query field is ignored)
15
+ Example: Execute a sequence of queries with context passing between stages
16
+ Status: TODO - signature only, implementation pending in RemService
17
+
18
+ Model Selection:
19
+ Default model: openai:gpt-4.1 (widely available, good balance of speed/quality)
20
+
21
+ Recommended for speed: cerebras:qwen-3-32b
22
+ - Cerebras provides extremely fast inference (~1000 tokens/sec)
23
+ - Set CEREBRAS_API_KEY environment variable
24
+ - Pass model="cerebras:qwen-3-32b" in request
25
+
26
+ Example:
27
+ # REM dialect (default)
28
+ curl -X POST http://localhost:8000/api/v1/query \\
29
+ -H "Content-Type: application/json" \\
30
+ -H "X-User-Id: user123" \\
31
+ -d '{"query": "LOOKUP sarah-chen"}'
32
+
33
+ # Natural language
34
+ curl -X POST http://localhost:8000/api/v1/query \\
35
+ -H "Content-Type: application/json" \\
36
+ -H "X-User-Id: user123" \\
37
+ -d '{"query": "Find all documents about API design", "mode": "natural-language"}'
38
+
39
+ # With Cerebras for speed
40
+ curl -X POST http://localhost:8000/api/v1/query \\
41
+ -H "Content-Type: application/json" \\
42
+ -H "X-User-Id: user123" \\
43
+ -d '{"query": "Who is Sarah?", "mode": "natural-language", "model": "cerebras:qwen-3-32b"}'
44
+
45
+ # Staged plan (TODO) - static query stages
46
+ curl -X POST http://localhost:8000/api/v1/query \\
47
+ -H "Content-Type: application/json" \\
48
+ -H "X-User-Id: user123" \\
49
+ -d '{"mode": "staged-plan", "plan": [
50
+ {"stage": 1, "query": "LOOKUP Sarah Chen", "name": "user"},
51
+ {"stage": 2, "query": "TRAVERSE FROM \"Sarah Chen\" DEPTH 2"}
52
+ ]}'
53
+
54
+ # Staged plan with LLM-driven dynamic stages
55
+ curl -X POST http://localhost:8000/api/v1/query \\
56
+ -H "Content-Type: application/json" \\
57
+ -H "X-User-Id: user123" \\
58
+ -d '{"mode": "staged-plan", "plan": [
59
+ {"stage": 1, "query": "LOOKUP Sarah Chen", "name": "user"},
60
+ {"stage": 2, "intent": "find her team members", "depends_on": ["user"]}
61
+ ]}'
62
+
63
+ # Plan continuation - pass previous_results to resume a multi-turn plan
64
+ # Turn 1: Execute stage 1, get back stage_results
65
+ # Turn 2: Continue with stage 2, passing previous results
66
+ curl -X POST http://localhost:8000/api/v1/query \\
67
+ -H "Content-Type: application/json" \\
68
+ -H "X-User-Id: user123" \\
69
+ -d '{
70
+ "mode": "staged-plan",
71
+ "plan": [
72
+ {"stage": 1, "query": "LOOKUP Sarah Chen", "name": "user"},
73
+ {"stage": 2, "intent": "find her team members", "depends_on": ["user"]}
74
+ ],
75
+ "previous_results": [
76
+ {"stage": 1, "name": "user", "query_executed": "LOOKUP Sarah Chen", "results": [...], "count": 1}
77
+ ],
78
+ "resume_from_stage": 2
79
+ }'
80
+ """
81
+
82
+ from enum import Enum
83
+ from typing import Any
84
+
85
+ from fastapi import APIRouter, Header, HTTPException
86
+ from loguru import logger
87
+ from pydantic import BaseModel, Field
88
+
89
+ from ...services.postgres import get_postgres_service
90
+ from ...services.rem.service import RemService
91
+ from ...services.rem.parser import RemQueryParser
92
+ from ...models.core import RemQuery
93
+ from ...settings import settings
94
+
95
+ router = APIRouter(prefix="/api/v1", tags=["query"])
96
+
97
+
98
+ class QueryMode(str, Enum):
99
+ """Query execution mode."""
100
+ REM_DIALECT = "rem-dialect"
101
+ NATURAL_LANGUAGE = "natural-language"
102
+ STAGED_PLAN = "staged-plan"
103
+
104
+
105
+ class StagedPlanResult(BaseModel):
106
+ """Result from a completed stage - used for plan continuation."""
107
+
108
+ stage: int = Field(..., description="Stage number that produced this result")
109
+ name: str | None = Field(default=None, description="Stage name for referencing")
110
+ query_executed: str = Field(..., description="The REM query that was executed")
111
+ results: list[dict[str, Any]] = Field(default_factory=list, description="Query results")
112
+ count: int = Field(default=0, description="Number of results")
113
+
114
+
115
+ class QueryPlanStage(BaseModel):
116
+ """A single stage in a multi-stage query plan.
117
+
118
+ Each stage can be either:
119
+ 1. A static REM dialect query (query field set)
120
+ 2. A dynamic query built by LLM from intent + previous results (intent field set)
121
+
122
+ The LLM interprets the intent along with previous stage results to construct
123
+ the appropriate REM query at runtime.
124
+ """
125
+
126
+ stage: int = Field(..., description="Stage number (1-indexed, executed in order)")
127
+ query: str | None = Field(
128
+ default=None,
129
+ description="Static REM dialect query (mutually exclusive with intent)",
130
+ )
131
+ intent: str | None = Field(
132
+ default=None,
133
+ description="Natural language intent - LLM builds query from this + previous results",
134
+ )
135
+ name: str | None = Field(default=None, description="Optional name for referencing results")
136
+ depends_on: list[str] | None = Field(
137
+ default=None,
138
+ description="Names of previous stages whose results are passed as context to LLM",
139
+ )
140
+
141
+
142
+ class QueryRequest(BaseModel):
143
+ """Request body for REM query execution."""
144
+
145
+ query: str | None = Field(
146
+ default=None,
147
+ description="Query string - either REM dialect syntax or natural language. Required for rem-dialect and natural-language modes.",
148
+ examples=[
149
+ "LOOKUP sarah-chen",
150
+ "SEARCH resources 'API design' LIMIT 10",
151
+ "Find all documents by Sarah",
152
+ ],
153
+ )
154
+
155
+ mode: QueryMode = Field(
156
+ default=QueryMode.REM_DIALECT,
157
+ description="Query mode: 'rem-dialect' (default), 'natural-language', or 'staged-plan'",
158
+ )
159
+
160
+ model: str = Field(
161
+ default="openai:gpt-4.1",
162
+ description=(
163
+ "LLM model for natural-language mode. "
164
+ "Default: openai:gpt-4.1. "
165
+ "Recommended for speed: cerebras:qwen-3-32b (requires CEREBRAS_API_KEY)"
166
+ ),
167
+ )
168
+
169
+ plan_only: bool = Field(
170
+ default=False,
171
+ description="If true with natural-language mode, return generated query without executing",
172
+ )
173
+
174
+ plan: list[QueryPlanStage] | None = Field(
175
+ default=None,
176
+ description="Multi-stage query plan for staged-plan mode. Each stage executes in order.",
177
+ )
178
+
179
+ previous_results: list[StagedPlanResult] | None = Field(
180
+ default=None,
181
+ description=(
182
+ "Results from previous turns for plan continuation. "
183
+ "Pass this back from the response's stage_results to continue a multi-turn plan."
184
+ ),
185
+ )
186
+
187
+ resume_from_stage: int | None = Field(
188
+ default=None,
189
+ description="Stage number to resume from (1-indexed). Stages before this are skipped.",
190
+ )
191
+
192
+
193
+ class QueryResponse(BaseModel):
194
+ """Response from REM query execution."""
195
+
196
+ query_type: str = Field(..., description="Type of query executed (LOOKUP, SEARCH, FUZZY, SQL, TRAVERSE)")
197
+ query: str = Field(..., description="The query that was executed (original or generated)")
198
+ results: list[dict[str, Any]] = Field(default_factory=list, description="Query results")
199
+ count: int = Field(..., description="Number of results")
200
+
201
+ # Natural language mode fields
202
+ mode: QueryMode = Field(..., description="Query mode used")
203
+ generated_query: str | None = Field(default=None, description="Generated REM query (natural-language mode only)")
204
+ confidence: float | None = Field(default=None, description="Confidence score (natural-language mode only)")
205
+ reasoning: str | None = Field(default=None, description="Query reasoning (natural-language mode only)")
206
+ warning: str | None = Field(default=None, description="Warning message if any")
207
+ plan_only: bool = Field(default=False, description="If true, query was not executed (plan mode)")
208
+
209
+ # Staged plan mode fields
210
+ stage_results: list[dict[str, Any]] | None = Field(
211
+ default=None,
212
+ description="Results from each stage (staged-plan mode only)",
213
+ )
214
+
215
+
216
+ @router.post("/query", response_model=QueryResponse)
217
+ async def execute_query(
218
+ request: QueryRequest,
219
+ x_user_id: str | None = Header(default=None, description="User ID for query isolation (optional, uses default if not provided)"),
220
+ ) -> QueryResponse:
221
+ """
222
+ Execute a REM query.
223
+
224
+ Supports three modes:
225
+
226
+ **rem-dialect** (default): Execute REM query syntax directly.
227
+ - LOOKUP "entity-key" - O(1) key-value lookup
228
+ - FUZZY "text" THRESHOLD 0.3 - Fuzzy text matching
229
+ - SEARCH table "semantic query" LIMIT 10 - Vector similarity search
230
+ - TRAVERSE FROM "entity" TYPE "rel" DEPTH 2 - Graph traversal
231
+ - SQL SELECT * FROM table WHERE ... - Direct SQL (SELECT only)
232
+
233
+ **natural-language**: Convert question to REM query via LLM.
234
+ - Uses REM Query Agent to parse intent
235
+ - Auto-executes if confidence >= 0.7
236
+ - Returns warning for low-confidence queries
237
+
238
+ **staged-plan**: Execute a multi-stage query plan.
239
+ - Pass plan=[{stage: 1, query: "...", name: "..."}, ...] instead of query
240
+ - Stages execute in order with context passing between them
241
+ - TODO: Implementation pending in RemService
242
+
243
+ **Model Selection**:
244
+ - Default: openai:gpt-4.1 (reliable, widely available)
245
+ - Speed: cerebras:qwen-3-32b (requires CEREBRAS_API_KEY)
246
+
247
+ Returns:
248
+ QueryResponse with results and metadata
249
+ """
250
+ if not settings.postgres.enabled:
251
+ raise HTTPException(
252
+ status_code=503,
253
+ detail="Database not configured. Set POSTGRES__ENABLED=true",
254
+ )
255
+
256
+ try:
257
+ # Get database service and ensure connected
258
+ db = get_postgres_service()
259
+ if db is None:
260
+ raise HTTPException(status_code=503, detail="Database service unavailable")
261
+
262
+ # Connect if not already connected
263
+ if db.pool is None:
264
+ await db.connect()
265
+
266
+ rem_service = RemService(db)
267
+
268
+ # Use effective_user_id from settings if not provided
269
+ effective_user_id = x_user_id or settings.test.effective_user_id
270
+
271
+ if request.mode == QueryMode.STAGED_PLAN:
272
+ # Staged plan mode - execute multi-stage query plan
273
+ # TODO: Implementation pending in RemService.execute_staged_plan()
274
+ if not request.plan:
275
+ raise HTTPException(
276
+ status_code=400,
277
+ detail="staged-plan mode requires 'plan' field with list of QueryPlanStage",
278
+ )
279
+
280
+ logger.info(f"Staged plan query: {len(request.plan)} stages")
281
+
282
+ # TODO: Call rem_service.execute_staged_plan(request.plan, x_user_id)
283
+ # For now, return a 501 Not Implemented
284
+ raise HTTPException(
285
+ status_code=501,
286
+ detail="staged-plan mode not yet implemented. See RemService TODO.",
287
+ )
288
+
289
+ elif request.mode == QueryMode.NATURAL_LANGUAGE:
290
+ # Natural language mode - use agent to convert
291
+ if not request.query:
292
+ raise HTTPException(
293
+ status_code=400,
294
+ detail="natural-language mode requires 'query' field",
295
+ )
296
+
297
+ logger.info(f"Natural language query: {request.query[:100]}... (model={request.model})")
298
+
299
+ result = await rem_service.ask_rem(
300
+ natural_query=request.query,
301
+ tenant_id=effective_user_id,
302
+ llm_model=request.model,
303
+ plan_mode=request.plan_only,
304
+ )
305
+
306
+ # Build response
307
+ response = QueryResponse(
308
+ query_type=result.get("results", {}).get("query_type", "UNKNOWN"),
309
+ query=request.query,
310
+ results=result.get("results", {}).get("results", []),
311
+ count=result.get("results", {}).get("count", 0),
312
+ mode=QueryMode.NATURAL_LANGUAGE,
313
+ generated_query=result.get("query"),
314
+ confidence=result.get("confidence"),
315
+ reasoning=result.get("reasoning"),
316
+ warning=result.get("warning"),
317
+ plan_only=result.get("plan_mode", False),
318
+ )
319
+
320
+ return response
321
+
322
+ else:
323
+ # REM dialect mode - parse and execute directly
324
+ if not request.query:
325
+ raise HTTPException(
326
+ status_code=400,
327
+ detail="rem-dialect mode requires 'query' field",
328
+ )
329
+
330
+ logger.info(f"REM dialect query: {request.query[:100]}...")
331
+
332
+ parser = RemQueryParser()
333
+ query_type, parameters = parser.parse(request.query)
334
+
335
+ # Create and execute RemQuery
336
+ rem_query = RemQuery.model_validate({
337
+ "query_type": query_type,
338
+ "parameters": parameters,
339
+ "user_id": effective_user_id,
340
+ })
341
+
342
+ result = await rem_service.execute_query(rem_query)
343
+
344
+ return QueryResponse(
345
+ query_type=result["query_type"],
346
+ query=request.query,
347
+ results=result.get("results", []),
348
+ count=result.get("count", 0),
349
+ mode=QueryMode.REM_DIALECT,
350
+ )
351
+
352
+ except HTTPException:
353
+ # Re-raise HTTPExceptions (400, 501, etc.) without wrapping
354
+ raise
355
+ except ValueError as e:
356
+ # Parse errors
357
+ raise HTTPException(status_code=400, detail=str(e))
358
+ except Exception as e:
359
+ logger.exception(f"Query execution failed: {e}")
360
+ raise HTTPException(status_code=500, detail=f"Query execution failed: {str(e)}")
@@ -1,13 +1,13 @@
1
1
  """
2
- Shared Sessions endpoints.
2
+ Session sharing endpoints.
3
3
 
4
4
  Enables session sharing between users for collaborative access to conversation history.
5
5
 
6
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/shared-with-me - Get users sharing sessions with you
10
- GET /api/v1/shared-with-me/{user_id}/messages - Get messages from a user's shared sessions
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
11
 
12
12
  See src/rem/models/entities/shared_session.py for full documentation.
13
13
  """
@@ -82,7 +82,7 @@ class ShareSessionResponse(BaseModel):
82
82
  "/sessions/{session_id}/share",
83
83
  response_model=ShareSessionResponse,
84
84
  status_code=201,
85
- tags=["shared-sessions"],
85
+ tags=["sessions"],
86
86
  )
87
87
  async def share_session(
88
88
  request: Request,
@@ -160,7 +160,7 @@ async def share_session(
160
160
  deleted_at=result["deleted_at"],
161
161
  )
162
162
 
163
- logger.info(
163
+ logger.debug(
164
164
  f"User {current_user_id} shared session '{session_id}' with {body.shared_with_user_id}"
165
165
  )
166
166
 
@@ -174,7 +174,7 @@ async def share_session(
174
174
  @router.delete(
175
175
  "/sessions/{session_id}/share/{shared_with_user_id}",
176
176
  status_code=200,
177
- tags=["shared-sessions"],
177
+ tags=["sessions"],
178
178
  )
179
179
  async def remove_session_share(
180
180
  request: Request,
@@ -231,7 +231,7 @@ async def remove_session_share(
231
231
  detail=f"Share not found for session '{session_id}' with user '{shared_with_user_id}'",
232
232
  )
233
233
 
234
- logger.info(
234
+ logger.debug(
235
235
  f"User {current_user_id} removed share for session '{session_id}' with {shared_with_user_id}"
236
236
  )
237
237
 
@@ -247,9 +247,9 @@ async def remove_session_share(
247
247
 
248
248
 
249
249
  @router.get(
250
- "/shared-with-me",
250
+ "/sessions/shared-with-me",
251
251
  response_model=SharedWithMeResponse,
252
- tags=["shared-sessions"],
252
+ tags=["sessions"],
253
253
  )
254
254
  async def get_shared_with_me(
255
255
  request: Request,
@@ -325,9 +325,9 @@ async def get_shared_with_me(
325
325
 
326
326
 
327
327
  @router.get(
328
- "/shared-with-me/{owner_user_id}/messages",
328
+ "/sessions/shared-with-me/{owner_user_id}/messages",
329
329
  response_model=SharedMessagesResponse,
330
- tags=["shared-sessions"],
330
+ tags=["sessions"],
331
331
  )
332
332
  async def get_shared_messages(
333
333
  request: Request,