remdb 0.3.7__py3-none-any.whl → 0.3.133__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.
Files changed (107) 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 +502 -0
  6. rem/agentic/context.py +51 -25
  7. rem/agentic/llm_provider_models.py +301 -0
  8. rem/agentic/mcp/tool_wrapper.py +112 -17
  9. rem/agentic/otel/setup.py +93 -4
  10. rem/agentic/providers/phoenix.py +314 -132
  11. rem/agentic/providers/pydantic_ai.py +215 -26
  12. rem/agentic/schema.py +361 -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 +465 -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 +402 -20
  24. rem/api/routers/chat/models.py +88 -10
  25. rem/api/routers/chat/otel_utils.py +33 -0
  26. rem/api/routers/chat/sse_events.py +542 -0
  27. rem/api/routers/chat/streaming.py +642 -45
  28. rem/api/routers/dev.py +81 -0
  29. rem/api/routers/feedback.py +268 -0
  30. rem/api/routers/messages.py +473 -0
  31. rem/api/routers/models.py +78 -0
  32. rem/api/routers/query.py +360 -0
  33. rem/api/routers/shared_sessions.py +406 -0
  34. rem/auth/middleware.py +126 -27
  35. rem/cli/commands/README.md +237 -64
  36. rem/cli/commands/ask.py +13 -10
  37. rem/cli/commands/cluster.py +1808 -0
  38. rem/cli/commands/configure.py +5 -6
  39. rem/cli/commands/db.py +396 -139
  40. rem/cli/commands/experiments.py +469 -74
  41. rem/cli/commands/process.py +22 -15
  42. rem/cli/commands/scaffold.py +47 -0
  43. rem/cli/commands/schema.py +97 -50
  44. rem/cli/main.py +29 -6
  45. rem/config.py +10 -3
  46. rem/models/core/core_model.py +7 -1
  47. rem/models/core/experiment.py +54 -0
  48. rem/models/core/rem_query.py +5 -2
  49. rem/models/entities/__init__.py +21 -0
  50. rem/models/entities/domain_resource.py +38 -0
  51. rem/models/entities/feedback.py +123 -0
  52. rem/models/entities/message.py +30 -1
  53. rem/models/entities/session.py +83 -0
  54. rem/models/entities/shared_session.py +180 -0
  55. rem/models/entities/user.py +10 -3
  56. rem/registry.py +373 -0
  57. rem/schemas/agents/rem.yaml +7 -3
  58. rem/services/content/providers.py +92 -133
  59. rem/services/content/service.py +92 -20
  60. rem/services/dreaming/affinity_service.py +2 -16
  61. rem/services/dreaming/moment_service.py +2 -15
  62. rem/services/embeddings/api.py +24 -17
  63. rem/services/embeddings/worker.py +16 -16
  64. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  65. rem/services/phoenix/client.py +302 -28
  66. rem/services/postgres/README.md +159 -15
  67. rem/services/postgres/__init__.py +2 -1
  68. rem/services/postgres/diff_service.py +531 -0
  69. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  70. rem/services/postgres/repository.py +132 -0
  71. rem/services/postgres/schema_generator.py +291 -9
  72. rem/services/postgres/service.py +6 -6
  73. rem/services/rate_limit.py +113 -0
  74. rem/services/rem/README.md +14 -0
  75. rem/services/rem/parser.py +44 -9
  76. rem/services/rem/service.py +36 -2
  77. rem/services/session/compression.py +24 -1
  78. rem/services/session/reload.py +1 -1
  79. rem/services/user_service.py +98 -0
  80. rem/settings.py +399 -29
  81. rem/sql/background_indexes.sql +21 -16
  82. rem/sql/migrations/001_install.sql +387 -54
  83. rem/sql/migrations/002_install_models.sql +2320 -393
  84. rem/sql/migrations/003_optional_extensions.sql +326 -0
  85. rem/sql/migrations/004_cache_system.sql +548 -0
  86. rem/utils/__init__.py +18 -0
  87. rem/utils/constants.py +97 -0
  88. rem/utils/date_utils.py +228 -0
  89. rem/utils/embeddings.py +17 -4
  90. rem/utils/files.py +167 -0
  91. rem/utils/mime_types.py +158 -0
  92. rem/utils/model_helpers.py +156 -1
  93. rem/utils/schema_loader.py +282 -35
  94. rem/utils/sql_paths.py +146 -0
  95. rem/utils/sql_types.py +3 -1
  96. rem/utils/vision.py +9 -14
  97. rem/workers/README.md +14 -14
  98. rem/workers/__init__.py +3 -1
  99. rem/workers/db_listener.py +579 -0
  100. rem/workers/db_maintainer.py +74 -0
  101. rem/workers/unlogged_maintainer.py +463 -0
  102. {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/METADATA +460 -303
  103. {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/RECORD +105 -74
  104. {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/WHEEL +1 -1
  105. rem/sql/002_install_models.sql +0 -1068
  106. rem/sql/install_models.sql +0 -1038
  107. {remdb-0.3.7.dist-info → remdb-0.3.133.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,502 @@
1
+ """
2
+ SSE Event Simulator Agent.
3
+
4
+ A programmatic simulator that generates rich SSE events for testing and
5
+ demonstrating the streaming protocol. NOT an LLM-based agent - this is
6
+ pure Python that emits scripted SSE events.
7
+
8
+ Usage:
9
+ from rem.agentic.agents.simulator import stream_simulator_events
10
+
11
+ async for event in stream_simulator_events("demo"):
12
+ yield format_sse_event(event)
13
+
14
+ The simulator demonstrates:
15
+ 1. Reasoning events (thinking process)
16
+ 2. Text deltas (streamed content)
17
+ 3. Progress indicators
18
+ 4. Tool call events
19
+ 5. Action solicitations (user interaction)
20
+ 6. Metadata events
21
+ 7. Done event
22
+
23
+ This is useful for:
24
+ - Frontend development without LLM costs
25
+ - Testing SSE parsing and rendering
26
+ - Demonstrating the full event protocol
27
+ - Load testing streaming infrastructure
28
+ """
29
+
30
+ import asyncio
31
+ import time
32
+ import uuid
33
+ from typing import AsyncGenerator
34
+
35
+ from rem.api.routers.chat.sse_events import (
36
+ ReasoningEvent,
37
+ ActionRequestEvent,
38
+ MetadataEvent,
39
+ ProgressEvent,
40
+ ToolCallEvent,
41
+ DoneEvent,
42
+ ActionRequestCard,
43
+ ActionSubmit,
44
+ ActionStyle,
45
+ InputText,
46
+ InputChoiceSet,
47
+ ActionDisplayStyle,
48
+ format_sse_event,
49
+ )
50
+ from rem.api.routers.chat.models import (
51
+ ChatCompletionStreamResponse,
52
+ ChatCompletionStreamChoice,
53
+ ChatCompletionMessageDelta,
54
+ )
55
+
56
+
57
+ # =============================================================================
58
+ # Demo Content
59
+ # =============================================================================
60
+
61
+ DEMO_REASONING_STEPS = [
62
+ "Analyzing the user's request...",
63
+ "Considering the best approach to demonstrate SSE events...",
64
+ "Planning a response that showcases all event types...",
65
+ "Preparing rich markdown content with examples...",
66
+ ]
67
+
68
+ DEMO_MARKDOWN_CONTENT = """# SSE Streaming Demo
69
+
70
+ This response demonstrates the **rich SSE event protocol** with multiple event types streamed in real-time.
71
+
72
+ ## What You're Seeing
73
+
74
+ 1. **Reasoning Events** - The "thinking" process shown in a collapsible section
75
+ 2. **Text Streaming** - This markdown content, streamed word by word
76
+ 3. **Progress Events** - Step indicators during processing
77
+ 4. **Tool Calls** - Simulated tool invocations
78
+ 5. **Action Requests** - Interactive UI elements for user input
79
+
80
+ ## Code Example
81
+
82
+ ```python
83
+ from rem.agentic.agents.simulator import stream_simulator_events
84
+
85
+ async def demo():
86
+ async for event in stream_simulator_events("demo"):
87
+ print(event.type, event)
88
+ ```
89
+
90
+ ## Features Table
91
+
92
+ | Event Type | Purpose | UI Display |
93
+ |------------|---------|------------|
94
+ | `reasoning` | Model thinking | Collapsible section |
95
+ | `text_delta` | Content chunks | Main response area |
96
+ | `progress` | Step indicators | Progress bar |
97
+ | `tool_call` | Tool invocations | Tool status panel |
98
+ | `action_request` | User input | Buttons/forms |
99
+ | `metadata` | System info | Hidden or badge |
100
+
101
+ ## Summary
102
+
103
+ The SSE protocol enables rich, interactive AI experiences beyond simple text streaming. Each event type serves a specific purpose in the UI.
104
+
105
+ """
106
+
107
+ DEMO_TOOL_CALLS = [
108
+ ("search_knowledge", {"query": "SSE streaming best practices"}),
109
+ ("format_response", {"style": "markdown", "include_examples": True}),
110
+ ]
111
+
112
+ DEMO_PROGRESS_STEPS = [
113
+ "Initializing response",
114
+ "Generating content",
115
+ "Formatting output",
116
+ "Preparing actions",
117
+ ]
118
+
119
+
120
+ # =============================================================================
121
+ # Simulator Functions
122
+ # =============================================================================
123
+
124
+ async def stream_simulator_events(
125
+ prompt: str,
126
+ delay_ms: int = 50,
127
+ include_reasoning: bool = True,
128
+ include_progress: bool = True,
129
+ include_tool_calls: bool = True,
130
+ include_actions: bool = True,
131
+ include_metadata: bool = True,
132
+ # Message correlation IDs
133
+ message_id: str | None = None,
134
+ in_reply_to: str | None = None,
135
+ session_id: str | None = None,
136
+ # Model info
137
+ model: str = "simulator-v1.0.0",
138
+ ) -> AsyncGenerator[str, None]:
139
+ """
140
+ Generate a sequence of SSE events simulating an AI response.
141
+
142
+ This is a programmatic simulator - no LLM calls are made.
143
+ Events are yielded in a realistic order with configurable delays.
144
+
145
+ Text content uses OpenAI-compatible format for consistency with real agents.
146
+ Other events (reasoning, progress, tool_call, metadata) use named SSE events.
147
+
148
+ Args:
149
+ prompt: User prompt (used to vary output slightly)
150
+ delay_ms: Delay between events in milliseconds
151
+ include_reasoning: Whether to emit reasoning events
152
+ include_progress: Whether to emit progress events
153
+ include_tool_calls: Whether to emit tool call events
154
+ include_actions: Whether to emit action request at end
155
+ include_metadata: Whether to emit metadata event
156
+ message_id: Database ID of the assistant message being streamed
157
+ in_reply_to: Database ID of the user message this responds to
158
+ session_id: Session ID for conversation correlation
159
+ model: Model name for response metadata
160
+
161
+ Yields:
162
+ SSE-formatted strings ready for HTTP streaming
163
+
164
+ Example:
165
+ ```python
166
+ async for sse_string in stream_simulator_events("demo"):
167
+ print(sse_string)
168
+ ```
169
+ """
170
+ delay = delay_ms / 1000.0
171
+ request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
172
+ created_at = int(time.time())
173
+ is_first_chunk = True
174
+
175
+ # Phase 1: Reasoning events
176
+ if include_reasoning:
177
+ for i, step in enumerate(DEMO_REASONING_STEPS):
178
+ await asyncio.sleep(delay)
179
+ yield format_sse_event(ReasoningEvent(content=step + "\n", step=i + 1))
180
+
181
+ # Phase 2: Progress - Starting
182
+ if include_progress:
183
+ await asyncio.sleep(delay)
184
+ yield format_sse_event(ProgressEvent(
185
+ step=1,
186
+ total_steps=len(DEMO_PROGRESS_STEPS),
187
+ label=DEMO_PROGRESS_STEPS[0],
188
+ status="in_progress"
189
+ ))
190
+
191
+ # Phase 3: Tool calls
192
+ if include_tool_calls:
193
+ for tool_name, args in DEMO_TOOL_CALLS:
194
+ tool_id = f"call_{uuid.uuid4().hex[:8]}"
195
+
196
+ await asyncio.sleep(delay)
197
+ yield format_sse_event(ToolCallEvent(
198
+ tool_name=tool_name,
199
+ tool_id=tool_id,
200
+ status="started",
201
+ arguments=args
202
+ ))
203
+
204
+ await asyncio.sleep(delay * 3) # Simulate tool execution
205
+ yield format_sse_event(ToolCallEvent(
206
+ tool_name=tool_name,
207
+ tool_id=tool_id,
208
+ status="completed",
209
+ result=f"Retrieved data for {tool_name}"
210
+ ))
211
+
212
+ # Phase 4: Progress - Generating
213
+ if include_progress:
214
+ await asyncio.sleep(delay)
215
+ yield format_sse_event(ProgressEvent(
216
+ step=2,
217
+ total_steps=len(DEMO_PROGRESS_STEPS),
218
+ label=DEMO_PROGRESS_STEPS[1],
219
+ status="in_progress"
220
+ ))
221
+
222
+ # Phase 5: Stream text content in OpenAI format
223
+ words = DEMO_MARKDOWN_CONTENT.split(" ")
224
+ buffer = ""
225
+ for i, word in enumerate(words):
226
+ buffer += word + " "
227
+ # Emit every few words to simulate realistic streaming
228
+ if len(buffer) > 20 or i == len(words) - 1:
229
+ await asyncio.sleep(delay)
230
+ # OpenAI-compatible format
231
+ chunk = ChatCompletionStreamResponse(
232
+ id=request_id,
233
+ created=created_at,
234
+ model=model,
235
+ choices=[
236
+ ChatCompletionStreamChoice(
237
+ index=0,
238
+ delta=ChatCompletionMessageDelta(
239
+ role="assistant" if is_first_chunk else None,
240
+ content=buffer,
241
+ ),
242
+ finish_reason=None,
243
+ )
244
+ ],
245
+ )
246
+ is_first_chunk = False
247
+ yield f"data: {chunk.model_dump_json()}\n\n"
248
+ buffer = ""
249
+
250
+ # Phase 6: Progress - Formatting
251
+ if include_progress:
252
+ await asyncio.sleep(delay)
253
+ yield format_sse_event(ProgressEvent(
254
+ step=3,
255
+ total_steps=len(DEMO_PROGRESS_STEPS),
256
+ label=DEMO_PROGRESS_STEPS[2],
257
+ status="in_progress"
258
+ ))
259
+
260
+ # Phase 7: Metadata (includes message correlation IDs)
261
+ if include_metadata:
262
+ await asyncio.sleep(delay)
263
+ yield format_sse_event(MetadataEvent(
264
+ # Message correlation IDs
265
+ message_id=message_id,
266
+ in_reply_to=in_reply_to,
267
+ session_id=session_id,
268
+ # Session info
269
+ session_name="SSE Demo Session",
270
+ # Quality indicators
271
+ confidence=0.95,
272
+ sources=["rem/api/routers/chat/sse_events.py", "rem/agentic/agents/sse_simulator.py"],
273
+ # Model info
274
+ model_version=model,
275
+ # Performance metrics
276
+ latency_ms=int(len(words) * delay_ms),
277
+ token_count=len(words),
278
+ # System flags
279
+ flags=["demo_mode"],
280
+ hidden=False,
281
+ extra={"prompt_length": len(prompt)}
282
+ ))
283
+
284
+ # Phase 8: Progress - Preparing actions
285
+ if include_progress:
286
+ await asyncio.sleep(delay)
287
+ yield format_sse_event(ProgressEvent(
288
+ step=4,
289
+ total_steps=len(DEMO_PROGRESS_STEPS),
290
+ label=DEMO_PROGRESS_STEPS[3],
291
+ status="in_progress"
292
+ ))
293
+
294
+ # Phase 9: Action solicitation
295
+ if include_actions:
296
+ await asyncio.sleep(delay)
297
+ yield format_sse_event(ActionRequestEvent(
298
+ card=ActionRequestCard(
299
+ id=f"feedback-{uuid.uuid4().hex[:8]}",
300
+ prompt="Was this SSE demonstration helpful?",
301
+ display_style=ActionDisplayStyle.INLINE,
302
+ actions=[
303
+ ActionSubmit(
304
+ id="helpful-yes",
305
+ title="Yes, very helpful!",
306
+ style=ActionStyle.POSITIVE,
307
+ data={"rating": 5, "feedback": "positive"}
308
+ ),
309
+ ActionSubmit(
310
+ id="helpful-somewhat",
311
+ title="Somewhat",
312
+ style=ActionStyle.DEFAULT,
313
+ data={"rating": 3, "feedback": "neutral"}
314
+ ),
315
+ ActionSubmit(
316
+ id="helpful-no",
317
+ title="Not really",
318
+ style=ActionStyle.SECONDARY,
319
+ data={"rating": 1, "feedback": "negative"}
320
+ ),
321
+ ],
322
+ inputs=[
323
+ InputText(
324
+ id="comments",
325
+ label="Any comments?",
326
+ placeholder="Optional feedback...",
327
+ is_multiline=True,
328
+ max_length=500
329
+ ),
330
+ InputChoiceSet(
331
+ id="use_case",
332
+ label="What's your use case?",
333
+ choices=[
334
+ {"title": "Frontend development", "value": "frontend"},
335
+ {"title": "Testing", "value": "testing"},
336
+ {"title": "Learning", "value": "learning"},
337
+ {"title": "Other", "value": "other"},
338
+ ],
339
+ is_required=False
340
+ ),
341
+ ],
342
+ timeout_ms=60000,
343
+ fallback_text="Please provide feedback on this demo."
344
+ )
345
+ ))
346
+
347
+ # Phase 10: Mark all progress complete
348
+ if include_progress:
349
+ for i, label in enumerate(DEMO_PROGRESS_STEPS):
350
+ await asyncio.sleep(delay / 2)
351
+ yield format_sse_event(ProgressEvent(
352
+ step=i + 1,
353
+ total_steps=len(DEMO_PROGRESS_STEPS),
354
+ label=label,
355
+ status="completed"
356
+ ))
357
+
358
+ # Phase 11: Final chunk with finish_reason
359
+ final_chunk = ChatCompletionStreamResponse(
360
+ id=request_id,
361
+ created=created_at,
362
+ model=model,
363
+ choices=[
364
+ ChatCompletionStreamChoice(
365
+ index=0,
366
+ delta=ChatCompletionMessageDelta(),
367
+ finish_reason="stop",
368
+ )
369
+ ],
370
+ )
371
+ yield f"data: {final_chunk.model_dump_json()}\n\n"
372
+
373
+ # Phase 12: Done event
374
+ await asyncio.sleep(delay)
375
+ yield format_sse_event(DoneEvent(reason="stop"))
376
+
377
+ # Phase 13: OpenAI termination marker
378
+ yield "data: [DONE]\n\n"
379
+
380
+
381
+ async def stream_minimal_demo(
382
+ content: str = "Hello from the simulator!",
383
+ delay_ms: int = 30,
384
+ model: str = "simulator-v1.0.0",
385
+ ) -> AsyncGenerator[str, None]:
386
+ """
387
+ Generate a minimal SSE sequence with just text and done.
388
+
389
+ Useful for simple testing without all event types.
390
+ Uses OpenAI-compatible format for text content.
391
+
392
+ Args:
393
+ content: Text content to stream
394
+ delay_ms: Delay between chunks
395
+ model: Model name for response metadata
396
+
397
+ Yields:
398
+ SSE-formatted strings
399
+ """
400
+ delay = delay_ms / 1000.0
401
+ request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
402
+ created_at = int(time.time())
403
+ is_first_chunk = True
404
+
405
+ # Stream content word by word in OpenAI format
406
+ words = content.split(" ")
407
+ for word in words:
408
+ await asyncio.sleep(delay)
409
+ chunk = ChatCompletionStreamResponse(
410
+ id=request_id,
411
+ created=created_at,
412
+ model=model,
413
+ choices=[
414
+ ChatCompletionStreamChoice(
415
+ index=0,
416
+ delta=ChatCompletionMessageDelta(
417
+ role="assistant" if is_first_chunk else None,
418
+ content=word + " ",
419
+ ),
420
+ finish_reason=None,
421
+ )
422
+ ],
423
+ )
424
+ is_first_chunk = False
425
+ yield f"data: {chunk.model_dump_json()}\n\n"
426
+
427
+ # Final chunk with finish_reason
428
+ final_chunk = ChatCompletionStreamResponse(
429
+ id=request_id,
430
+ created=created_at,
431
+ model=model,
432
+ choices=[
433
+ ChatCompletionStreamChoice(
434
+ index=0,
435
+ delta=ChatCompletionMessageDelta(),
436
+ finish_reason="stop",
437
+ )
438
+ ],
439
+ )
440
+ yield f"data: {final_chunk.model_dump_json()}\n\n"
441
+
442
+ await asyncio.sleep(delay)
443
+ yield format_sse_event(DoneEvent(reason="stop"))
444
+ yield "data: [DONE]\n\n"
445
+
446
+
447
+ async def stream_error_demo(
448
+ error_after_words: int = 10,
449
+ model: str = "simulator-v1.0.0",
450
+ ) -> AsyncGenerator[str, None]:
451
+ """
452
+ Generate an SSE sequence that ends with an error.
453
+
454
+ Useful for testing error handling in the frontend.
455
+ Uses OpenAI-compatible format for text content.
456
+
457
+ Args:
458
+ error_after_words: Number of words before error
459
+ model: Model name for response metadata
460
+
461
+ Yields:
462
+ SSE-formatted strings including an error event
463
+ """
464
+ from rem.api.routers.chat.sse_events import ErrorEvent
465
+
466
+ request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
467
+ created_at = int(time.time())
468
+ is_first_chunk = True
469
+
470
+ content = "This is a demo that will encounter an error during streaming. Watch what happens when things go wrong..."
471
+ words = content.split(" ")
472
+
473
+ for i, word in enumerate(words[:error_after_words]):
474
+ await asyncio.sleep(0.03)
475
+ chunk = ChatCompletionStreamResponse(
476
+ id=request_id,
477
+ created=created_at,
478
+ model=model,
479
+ choices=[
480
+ ChatCompletionStreamChoice(
481
+ index=0,
482
+ delta=ChatCompletionMessageDelta(
483
+ role="assistant" if is_first_chunk else None,
484
+ content=word + " ",
485
+ ),
486
+ finish_reason=None,
487
+ )
488
+ ],
489
+ )
490
+ is_first_chunk = False
491
+ yield f"data: {chunk.model_dump_json()}\n\n"
492
+
493
+ await asyncio.sleep(0.1)
494
+ yield format_sse_event(ErrorEvent(
495
+ code="simulated_error",
496
+ message="This is a simulated error for testing purposes",
497
+ details={"words_sent": error_after_words, "demo": True},
498
+ recoverable=True
499
+ ))
500
+
501
+ yield format_sse_event(DoneEvent(reason="error"))
502
+ yield "data: [DONE]\n\n"
rem/agentic/context.py CHANGED
@@ -2,10 +2,18 @@
2
2
  Agent execution context and configuration.
3
3
 
4
4
  Design pattern for session context that can be constructed from:
5
- - HTTP headers (X-User-Id, X-Session-Id, X-Model-Name)
5
+ - HTTP headers (X-User-Id, X-Session-Id, X-Model-Name, X-Is-Eval, etc.)
6
6
  - Direct instantiation for testing/CLI
7
7
 
8
- Key Design Pattern
8
+ Headers Mapping:
9
+ X-User-Id → context.user_id
10
+ X-Tenant-Id → context.tenant_id (default: "default")
11
+ X-Session-Id → context.session_id
12
+ X-Agent-Schema → context.agent_schema_uri (default: "rem")
13
+ X-Model-Name → context.default_model
14
+ X-Is-Eval → context.is_eval (marks session as evaluation)
15
+
16
+ Key Design Pattern:
9
17
  - AgentContext is passed to agent factory, not stored in agents
10
18
  - Enables session tracking across API, CLI, and test execution
11
19
  - Supports header-based configuration override (model, schema URI)
@@ -66,48 +74,59 @@ class AgentContext(BaseModel):
66
74
  description="Agent schema URI (e.g., 'rem-agents-query-agent')",
67
75
  )
68
76
 
77
+ is_eval: bool = Field(
78
+ default=False,
79
+ description="Whether this is an evaluation session (set via X-Is-Eval header)",
80
+ )
81
+
69
82
  model_config = {"populate_by_name": True}
70
83
 
71
84
  @staticmethod
72
85
  def get_user_id_or_default(
73
86
  user_id: str | None,
74
87
  source: str = "context",
75
- default: str = "default",
76
- ) -> str:
88
+ default: str | None = None,
89
+ ) -> str | None:
77
90
  """
78
- Get user_id or fallback to default with logging.
91
+ Get user_id or return None for anonymous access.
79
92
 
80
- Centralized helper for consistent user_id fallback behavior across
81
- API endpoints, MCP tools, CLI commands, and services.
93
+ User ID convention:
94
+ - user_id is a deterministic UUID5 hash of the user's email address
95
+ - Use rem.utils.user_id.email_to_user_id(email) to generate
96
+ - The JWT's `sub` claim is NOT directly used as user_id
97
+ - Authentication middleware extracts email from JWT and hashes it
98
+
99
+ When user_id is None, queries return data with user_id IS NULL
100
+ (shared/public data). This is intentional - no fake user IDs.
82
101
 
83
102
  Args:
84
- user_id: User identifier (may be None)
103
+ user_id: User identifier (UUID5 hash of email, may be None for anonymous)
85
104
  source: Source of the call (for logging clarity)
86
- default: Default value to use (default: "default")
105
+ default: Explicit default (only for testing, not auto-generated)
87
106
 
88
107
  Returns:
89
- user_id if provided, otherwise default
108
+ user_id if provided, explicit default if provided, otherwise None
90
109
 
91
110
  Example:
92
- # In MCP tool
93
- user_id = AgentContext.get_user_id_or_default(
94
- user_id, source="ask_rem_agent"
95
- )
96
-
97
- # In API endpoint
98
- user_id = AgentContext.get_user_id_or_default(
99
- temp_context.user_id, source="chat_completions"
100
- )
111
+ # Generate user_id from email (done by auth middleware)
112
+ from rem.utils.user_id import email_to_user_id
113
+ user_id = email_to_user_id("alice@example.com")
114
+ # -> "2c5ea4c0-4067-5fef-942d-0a20124e06d8"
101
115
 
102
- # In CLI command
116
+ # In MCP tool - anonymous user sees shared data
103
117
  user_id = AgentContext.get_user_id_or_default(
104
- args.user_id, source="rem ask"
118
+ user_id, source="ask_rem_agent"
105
119
  )
120
+ # Returns None if not authenticated -> queries WHERE user_id IS NULL
106
121
  """
107
- if user_id is None:
108
- logger.debug(f"No user_id provided from {source}, using '{default}'")
122
+ if user_id is not None:
123
+ return user_id
124
+ if default is not None:
125
+ logger.debug(f"Using explicit default user_id '{default}' from {source}")
109
126
  return default
110
- return user_id
127
+ # No fake user IDs - return None for anonymous/unauthenticated
128
+ logger.debug(f"No user_id from {source}, using None (anonymous/shared data)")
129
+ return None
111
130
 
112
131
  @classmethod
113
132
  def from_headers(cls, headers: dict[str, str]) -> "AgentContext":
@@ -120,6 +139,7 @@ class AgentContext(BaseModel):
120
139
  - X-Session-Id: Session identifier
121
140
  - X-Model-Name: Model override
122
141
  - X-Agent-Schema: Agent schema URI
142
+ - X-Is-Eval: Whether this is an evaluation session (true/false)
123
143
 
124
144
  Args:
125
145
  headers: Dictionary of HTTP headers (case-insensitive)
@@ -132,17 +152,23 @@ class AgentContext(BaseModel):
132
152
  "X-User-Id": "user123",
133
153
  "X-Tenant-Id": "acme-corp",
134
154
  "X-Session-Id": "sess-456",
135
- "X-Model-Name": "anthropic:claude-opus-4-20250514"
155
+ "X-Model-Name": "anthropic:claude-opus-4-20250514",
156
+ "X-Is-Eval": "true"
136
157
  }
137
158
  context = AgentContext.from_headers(headers)
138
159
  """
139
160
  # Normalize header keys to lowercase for case-insensitive lookup
140
161
  normalized = {k.lower(): v for k, v in headers.items()}
141
162
 
163
+ # Parse X-Is-Eval header (accepts "true", "1", "yes" as truthy)
164
+ is_eval_str = normalized.get("x-is-eval", "").lower()
165
+ is_eval = is_eval_str in ("true", "1", "yes")
166
+
142
167
  return cls(
143
168
  user_id=normalized.get("x-user-id"),
144
169
  tenant_id=normalized.get("x-tenant-id", "default"),
145
170
  session_id=normalized.get("x-session-id"),
146
171
  default_model=normalized.get("x-model-name") or settings.llm.default_model,
147
172
  agent_schema_uri=normalized.get("x-agent-schema"),
173
+ is_eval=is_eval,
148
174
  )