remdb 0.2.6__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 (104) hide show
  1. rem/__init__.py +129 -2
  2. rem/agentic/README.md +76 -0
  3. rem/agentic/__init__.py +15 -0
  4. rem/agentic/agents/__init__.py +16 -2
  5. rem/agentic/agents/sse_simulator.py +500 -0
  6. rem/agentic/context.py +28 -22
  7. rem/agentic/llm_provider_models.py +301 -0
  8. rem/agentic/mcp/tool_wrapper.py +29 -3
  9. rem/agentic/otel/setup.py +92 -4
  10. rem/agentic/providers/phoenix.py +32 -43
  11. rem/agentic/providers/pydantic_ai.py +168 -24
  12. rem/agentic/schema.py +358 -21
  13. rem/agentic/tools/rem_tools.py +3 -3
  14. rem/api/README.md +238 -1
  15. rem/api/deps.py +255 -0
  16. rem/api/main.py +154 -37
  17. rem/api/mcp_router/resources.py +1 -1
  18. rem/api/mcp_router/server.py +26 -5
  19. rem/api/mcp_router/tools.py +454 -7
  20. rem/api/middleware/tracking.py +172 -0
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +124 -0
  23. rem/api/routers/chat/completions.py +152 -16
  24. rem/api/routers/chat/models.py +7 -3
  25. rem/api/routers/chat/sse_events.py +526 -0
  26. rem/api/routers/chat/streaming.py +608 -45
  27. rem/api/routers/dev.py +81 -0
  28. rem/api/routers/feedback.py +148 -0
  29. rem/api/routers/messages.py +473 -0
  30. rem/api/routers/models.py +78 -0
  31. rem/api/routers/query.py +360 -0
  32. rem/api/routers/shared_sessions.py +406 -0
  33. rem/auth/middleware.py +126 -27
  34. rem/cli/commands/README.md +237 -64
  35. rem/cli/commands/ask.py +15 -11
  36. rem/cli/commands/cluster.py +1300 -0
  37. rem/cli/commands/configure.py +170 -97
  38. rem/cli/commands/db.py +396 -139
  39. rem/cli/commands/experiments.py +278 -96
  40. rem/cli/commands/process.py +22 -15
  41. rem/cli/commands/scaffold.py +47 -0
  42. rem/cli/commands/schema.py +97 -50
  43. rem/cli/main.py +37 -6
  44. rem/config.py +2 -2
  45. rem/models/core/core_model.py +7 -1
  46. rem/models/core/rem_query.py +5 -2
  47. rem/models/entities/__init__.py +21 -0
  48. rem/models/entities/domain_resource.py +38 -0
  49. rem/models/entities/feedback.py +123 -0
  50. rem/models/entities/message.py +30 -1
  51. rem/models/entities/session.py +83 -0
  52. rem/models/entities/shared_session.py +180 -0
  53. rem/models/entities/user.py +10 -3
  54. rem/registry.py +373 -0
  55. rem/schemas/agents/rem.yaml +7 -3
  56. rem/services/content/providers.py +94 -140
  57. rem/services/content/service.py +115 -24
  58. rem/services/dreaming/affinity_service.py +2 -16
  59. rem/services/dreaming/moment_service.py +2 -15
  60. rem/services/embeddings/api.py +24 -17
  61. rem/services/embeddings/worker.py +16 -16
  62. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  63. rem/services/phoenix/client.py +252 -19
  64. rem/services/postgres/README.md +159 -15
  65. rem/services/postgres/__init__.py +2 -1
  66. rem/services/postgres/diff_service.py +531 -0
  67. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  68. rem/services/postgres/repository.py +132 -0
  69. rem/services/postgres/schema_generator.py +291 -9
  70. rem/services/postgres/service.py +6 -6
  71. rem/services/rate_limit.py +113 -0
  72. rem/services/rem/README.md +14 -0
  73. rem/services/rem/parser.py +44 -9
  74. rem/services/rem/service.py +36 -2
  75. rem/services/session/compression.py +17 -1
  76. rem/services/session/reload.py +1 -1
  77. rem/services/user_service.py +98 -0
  78. rem/settings.py +169 -22
  79. rem/sql/background_indexes.sql +21 -16
  80. rem/sql/migrations/001_install.sql +387 -54
  81. rem/sql/migrations/002_install_models.sql +2320 -393
  82. rem/sql/migrations/003_optional_extensions.sql +326 -0
  83. rem/sql/migrations/004_cache_system.sql +548 -0
  84. rem/utils/__init__.py +18 -0
  85. rem/utils/constants.py +97 -0
  86. rem/utils/date_utils.py +228 -0
  87. rem/utils/embeddings.py +17 -4
  88. rem/utils/files.py +167 -0
  89. rem/utils/mime_types.py +158 -0
  90. rem/utils/model_helpers.py +156 -1
  91. rem/utils/schema_loader.py +284 -21
  92. rem/utils/sql_paths.py +146 -0
  93. rem/utils/sql_types.py +3 -1
  94. rem/utils/vision.py +9 -14
  95. rem/workers/README.md +14 -14
  96. rem/workers/__init__.py +2 -1
  97. rem/workers/db_maintainer.py +74 -0
  98. rem/workers/unlogged_maintainer.py +463 -0
  99. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/METADATA +598 -171
  100. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/RECORD +102 -73
  101. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/WHEEL +1 -1
  102. rem/sql/002_install_models.sql +0 -1068
  103. rem/sql/install_models.sql +0 -1038
  104. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/entry_points.txt +0 -0
rem/api/routers/auth.py CHANGED
@@ -49,6 +49,8 @@ from authlib.integrations.starlette_client import OAuth
49
49
  from loguru import logger
50
50
 
51
51
  from ...settings import settings
52
+ from ...services.postgres.service import PostgresService
53
+ from ...services.user_service import UserService
52
54
 
53
55
  router = APIRouter(prefix="/api/auth", tags=["auth"])
54
56
 
@@ -168,6 +170,53 @@ async def callback(provider: str, request: Request):
168
170
  if not user_info:
169
171
  # Fetch from userinfo endpoint if not in ID token
170
172
  user_info = await client.userinfo(token=token)
173
+
174
+ # --- REM Integration Start ---
175
+ if settings.postgres.enabled:
176
+ # Connect to DB
177
+ db = PostgresService()
178
+ try:
179
+ await db.connect()
180
+ user_service = UserService(db)
181
+
182
+ # Get/Create User
183
+ user_entity = await user_service.get_or_create_user(
184
+ email=user_info.get("email"),
185
+ name=user_info.get("name", "New User"),
186
+ avatar_url=user_info.get("picture"),
187
+ tenant_id="default", # Single tenant for now
188
+ )
189
+
190
+ # Link Anonymous Session
191
+ # TrackingMiddleware sets request.state.anon_id
192
+ anon_id = getattr(request.state, "anon_id", None)
193
+ # Fallback to cookie if middleware didn't run or state missing
194
+ if not anon_id:
195
+ # Attempt to parse cookie manually if needed, but middleware
196
+ # usually handles the signature logic.
197
+ # Just check raw cookie for simple case (not recommended if signed)
198
+ pass
199
+
200
+ if anon_id:
201
+ await user_service.link_anonymous_session(user_entity, anon_id)
202
+
203
+ # Enrich session user with DB info
204
+ db_info = {
205
+ "id": str(user_entity.id),
206
+ "tenant_id": user_entity.tenant_id,
207
+ "tier": user_entity.tier.value if user_entity.tier else "free",
208
+ "roles": [user_entity.role] if user_entity.role else [],
209
+ }
210
+
211
+ except Exception as db_e:
212
+ logger.error(f"Database error during auth callback: {db_e}")
213
+ # Continue login even if DB fails, but warn
214
+ db_info = {"id": "db_error", "tier": "free"}
215
+ finally:
216
+ await db.disconnect()
217
+ else:
218
+ db_info = {"id": "no_db", "tier": "free"}
219
+ # --- REM Integration End ---
171
220
 
172
221
  # Store user info in session
173
222
  request.session["user"] = {
@@ -176,6 +225,11 @@ async def callback(provider: str, request: Request):
176
225
  "email": user_info.get("email"),
177
226
  "name": user_info.get("name"),
178
227
  "picture": user_info.get("picture"),
228
+ # Add DB info
229
+ "id": db_info.get("id"),
230
+ "tenant_id": db_info.get("tenant_id", "default"),
231
+ "tier": db_info.get("tier"),
232
+ "roles": db_info.get("roles", []),
179
233
  }
180
234
 
181
235
  # Store tokens in session for API access
@@ -227,3 +281,73 @@ async def me(request: Request):
227
281
  raise HTTPException(status_code=401, detail="Not authenticated")
228
282
 
229
283
  return user
284
+
285
+
286
+ # =============================================================================
287
+ # Development Token Endpoints (non-production only)
288
+ # =============================================================================
289
+
290
+
291
+ def generate_dev_token() -> str:
292
+ """
293
+ Generate a dev token for testing.
294
+
295
+ Token format: dev_<hmac_signature>
296
+ The signature is based on the session secret to ensure only valid tokens work.
297
+ """
298
+ import hashlib
299
+ import hmac
300
+
301
+ # Use session secret as key
302
+ secret = settings.auth.session_secret or "dev-secret"
303
+ message = "test-user:dev-token"
304
+
305
+ signature = hmac.new(
306
+ secret.encode(),
307
+ message.encode(),
308
+ hashlib.sha256
309
+ ).hexdigest()[:32]
310
+
311
+ return f"dev_{signature}"
312
+
313
+
314
+ def verify_dev_token(token: str) -> bool:
315
+ """Verify a dev token is valid."""
316
+ expected = generate_dev_token()
317
+ return token == expected
318
+
319
+
320
+ @router.get("/dev/token")
321
+ async def get_dev_token(request: Request):
322
+ """
323
+ Get a development token for testing (non-production only).
324
+
325
+ This token can be used as a Bearer token to authenticate as the
326
+ test user (test-user / test@rem.local) without going through OAuth.
327
+
328
+ Usage:
329
+ curl -H "Authorization: Bearer <token>" http://localhost:8000/api/v1/...
330
+
331
+ Returns:
332
+ 401 if in production environment
333
+ Token and usage instructions otherwise
334
+ """
335
+ if settings.environment == "production":
336
+ raise HTTPException(
337
+ status_code=401,
338
+ detail="Dev tokens are not available in production"
339
+ )
340
+
341
+ token = generate_dev_token()
342
+
343
+ return {
344
+ "token": token,
345
+ "type": "Bearer",
346
+ "user": {
347
+ "id": "test-user",
348
+ "email": "test@rem.local",
349
+ "name": "Test User",
350
+ },
351
+ "usage": f'curl -H "Authorization: Bearer {token}" http://localhost:8000/api/v1/...',
352
+ "warning": "This token is for development/testing only and will not work in production.",
353
+ }
@@ -70,7 +70,7 @@ from ....agentic.providers.pydantic_ai import create_agent
70
70
  from ....services.audio.transcriber import AudioTranscriber
71
71
  from ....services.session import SessionMessageStore, reload_session
72
72
  from ....settings import settings
73
- from ....utils.schema_loader import load_agent_schema
73
+ from ....utils.schema_loader import load_agent_schema, load_agent_schema_async
74
74
  from .json_utils import extract_json_resilient
75
75
  from .models import (
76
76
  ChatCompletionChoice,
@@ -79,9 +79,9 @@ from .models import (
79
79
  ChatCompletionUsage,
80
80
  ChatMessage,
81
81
  )
82
- from .streaming import stream_openai_response
82
+ from .streaming import stream_openai_response, stream_openai_response_with_save, stream_simulator_response
83
83
 
84
- router = APIRouter(prefix="/v1", tags=["chat"])
84
+ router = APIRouter(prefix="/api/v1", tags=["chat"])
85
85
 
86
86
  # Default agent schema file
87
87
  DEFAULT_AGENT_SCHEMA = "rem"
@@ -133,9 +133,114 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
133
133
  temp_context = AgentContext.from_headers(dict(request.headers))
134
134
  schema_name = temp_context.agent_schema_uri or DEFAULT_AGENT_SCHEMA
135
135
 
136
+ # Resolve model: use body.model if provided, otherwise settings default
137
+ if body.model is None:
138
+ body.model = settings.llm.default_model
139
+ logger.debug(f"No model specified, using default: {body.model}")
140
+
141
+ # Special handling for simulator schema - no LLM, just generates demo SSE events
142
+ # Check BEFORE loading schema since simulator doesn't need a schema file
143
+ # Still builds full context and saves messages like a real agent
144
+ if schema_name == "simulator":
145
+ logger.info("Using SSE simulator (no LLM)")
146
+
147
+ # Build context just like real agents (loads session history, user context)
148
+ new_messages = [msg.model_dump() for msg in body.messages]
149
+ context, messages = await ContextBuilder.build_from_headers(
150
+ headers=dict(request.headers),
151
+ new_messages=new_messages,
152
+ )
153
+
154
+ # Get the last user message as prompt
155
+ prompt = body.messages[-1].content if body.messages else "demo"
156
+ request_id = f"sim-{uuid.uuid4().hex[:24]}"
157
+
158
+ # Generate message IDs upfront for correlation
159
+ user_message_id = str(uuid.uuid4())
160
+ assistant_message_id = str(uuid.uuid4())
161
+
162
+ # Simulated assistant response content (for persistence)
163
+ simulated_content = (
164
+ f"[SSE Simulator Response]\n\n"
165
+ f"This is a simulated response demonstrating all SSE event types:\n"
166
+ f"- reasoning events (model thinking)\n"
167
+ f"- text_delta events (streamed content)\n"
168
+ f"- progress events (multi-step operations)\n"
169
+ f"- tool_call events (function invocations)\n"
170
+ f"- action_request events (UI solicitation)\n"
171
+ f"- metadata events (confidence, sources, message IDs)\n\n"
172
+ f"Original prompt: {prompt[:100]}{'...' if len(prompt) > 100 else ''}"
173
+ )
174
+
175
+ # Save messages to database (if session_id and postgres enabled)
176
+ if settings.postgres.enabled and context.session_id:
177
+ user_message = {
178
+ "id": user_message_id,
179
+ "role": "user",
180
+ "content": prompt,
181
+ "timestamp": datetime.utcnow().isoformat(),
182
+ }
183
+ assistant_message = {
184
+ "id": assistant_message_id,
185
+ "role": "assistant",
186
+ "content": simulated_content,
187
+ "timestamp": datetime.utcnow().isoformat(),
188
+ }
189
+
190
+ try:
191
+ store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
192
+ await store.store_session_messages(
193
+ session_id=context.session_id,
194
+ messages=[user_message, assistant_message],
195
+ user_id=context.user_id,
196
+ compress=True,
197
+ )
198
+ logger.info(f"Saved simulator conversation to session {context.session_id}")
199
+ except Exception as e:
200
+ # Log error but don't fail the request - session storage is non-critical
201
+ logger.error(f"Failed to save session messages: {e}", exc_info=True)
202
+
203
+ if body.stream:
204
+ return StreamingResponse(
205
+ stream_simulator_response(
206
+ prompt=prompt,
207
+ model="simulator-v1.0.0",
208
+ # Pass message correlation IDs
209
+ message_id=assistant_message_id,
210
+ in_reply_to=user_message_id,
211
+ session_id=context.session_id,
212
+ ),
213
+ media_type="text/event-stream",
214
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
215
+ )
216
+ else:
217
+ # Non-streaming simulator returns simple JSON
218
+ return ChatCompletionResponse(
219
+ id=request_id,
220
+ created=int(time.time()),
221
+ model="simulator-v1.0.0",
222
+ choices=[
223
+ ChatCompletionChoice(
224
+ index=0,
225
+ message=ChatMessage(
226
+ role="assistant",
227
+ content=simulated_content,
228
+ ),
229
+ finish_reason="stop",
230
+ )
231
+ ],
232
+ usage=ChatCompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
233
+ )
234
+
136
235
  # Load schema using centralized utility
236
+ # Enable database fallback to load dynamic agents stored in schemas table
237
+ # Use async version since we're in an async context (FastAPI endpoint)
238
+ user_id = temp_context.user_id or settings.test.effective_user_id
137
239
  try:
138
- agent_schema = load_agent_schema(schema_name)
240
+ agent_schema = await load_agent_schema_async(
241
+ schema_name,
242
+ user_id=user_id,
243
+ )
139
244
  except FileNotFoundError:
140
245
  # Fallback to default if specified schema not found
141
246
  logger.warning(f"Schema '{schema_name}' not found, falling back to '{DEFAULT_AGENT_SCHEMA}'")
@@ -151,7 +256,7 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
151
256
  detail=f"Agent schema '{schema_name}' not found and default schema unavailable",
152
257
  )
153
258
 
154
- logger.info(f"Using agent schema: {schema_name}, model: {body.model}")
259
+ logger.debug(f"Using agent schema: {schema_name}, model: {body.model}")
155
260
 
156
261
  # Check for audio input
157
262
  is_audio = request.headers.get("x-chat-is-audio", "").lower() == "true"
@@ -212,8 +317,35 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
212
317
 
213
318
  # Streaming mode
214
319
  if body.stream:
320
+ # Save user message before streaming starts
321
+ if settings.postgres.enabled and context.session_id:
322
+ user_message = {
323
+ "role": "user",
324
+ "content": body.messages[-1].content if body.messages else "",
325
+ "timestamp": datetime.utcnow().isoformat(),
326
+ }
327
+ try:
328
+ store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
329
+ await store.store_session_messages(
330
+ session_id=context.session_id,
331
+ messages=[user_message],
332
+ user_id=context.user_id,
333
+ compress=False, # User messages are typically short
334
+ )
335
+ logger.debug(f"Saved user message to session {context.session_id}")
336
+ except Exception as e:
337
+ logger.error(f"Failed to save user message: {e}", exc_info=True)
338
+
215
339
  return StreamingResponse(
216
- stream_openai_response(agent, prompt, body.model, request_id),
340
+ stream_openai_response_with_save(
341
+ agent=agent,
342
+ prompt=prompt,
343
+ model=body.model,
344
+ request_id=request_id,
345
+ agent_schema=schema_name,
346
+ session_id=context.session_id,
347
+ user_id=context.user_id,
348
+ ),
217
349
  media_type="text/event-stream",
218
350
  headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
219
351
  )
@@ -250,17 +382,21 @@ async def chat_completions(body: ChatCompletionRequest, request: Request):
250
382
  "timestamp": datetime.utcnow().isoformat(),
251
383
  }
252
384
 
253
- # Store messages with compression
254
- store = SessionMessageStore(user_id=context.user_id or "default")
255
-
256
- await store.store_session_messages(
257
- session_id=context.session_id,
258
- messages=[user_message, assistant_message],
259
- user_id=context.user_id,
260
- compress=True,
261
- )
385
+ try:
386
+ # Store messages with compression
387
+ store = SessionMessageStore(user_id=context.user_id or settings.test.effective_user_id)
388
+
389
+ await store.store_session_messages(
390
+ session_id=context.session_id,
391
+ messages=[user_message, assistant_message],
392
+ user_id=context.user_id,
393
+ compress=True,
394
+ )
262
395
 
263
- logger.info(f"Saved conversation to session {context.session_id}")
396
+ logger.info(f"Saved conversation to session {context.session_id}")
397
+ except Exception as e:
398
+ # Log error but don't fail the request - session storage is non-critical
399
+ logger.error(f"Failed to save session messages: {e}", exc_info=True)
264
400
 
265
401
  return ChatCompletionResponse(
266
402
  id=request_id,
@@ -12,6 +12,8 @@ from typing import Literal
12
12
 
13
13
  from pydantic import BaseModel, Field
14
14
 
15
+ from rem.settings import settings
16
+
15
17
 
16
18
  # Request models
17
19
  class ChatMessage(BaseModel):
@@ -52,9 +54,11 @@ class ChatCompletionRequest(BaseModel):
52
54
  Note: Model is specified in body.model (standard OpenAI field), not headers.
53
55
  """
54
56
 
55
- model: str = Field(
56
- default="anthropic:claude-sonnet-4-5-20250929",
57
- description="Model to use (standard OpenAI field)",
57
+ # TODO: default should come from settings.llm.default_model at request time
58
+ # Using None and resolving in endpoint to avoid import-time settings evaluation
59
+ model: str | None = Field(
60
+ default=None,
61
+ description="Model to use. Defaults to LLM__DEFAULT_MODEL from settings.",
58
62
  )
59
63
  messages: list[ChatMessage] = Field(description="Chat conversation history")
60
64
  temperature: float | None = Field(default=None, ge=0, le=2)