remdb 0.3.0__py3-none-any.whl → 0.3.127__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.
- rem/__init__.py +129 -2
- rem/agentic/README.md +76 -0
- rem/agentic/__init__.py +15 -0
- rem/agentic/agents/__init__.py +16 -2
- rem/agentic/agents/sse_simulator.py +502 -0
- rem/agentic/context.py +51 -25
- rem/agentic/llm_provider_models.py +301 -0
- rem/agentic/mcp/tool_wrapper.py +29 -3
- rem/agentic/otel/setup.py +93 -4
- rem/agentic/providers/phoenix.py +32 -43
- rem/agentic/providers/pydantic_ai.py +168 -24
- rem/agentic/schema.py +358 -21
- rem/agentic/tools/rem_tools.py +3 -3
- rem/api/README.md +238 -1
- rem/api/deps.py +255 -0
- rem/api/main.py +154 -37
- rem/api/mcp_router/resources.py +1 -1
- rem/api/mcp_router/server.py +26 -5
- rem/api/mcp_router/tools.py +465 -7
- rem/api/middleware/tracking.py +172 -0
- rem/api/routers/admin.py +494 -0
- rem/api/routers/auth.py +124 -0
- rem/api/routers/chat/completions.py +402 -20
- rem/api/routers/chat/models.py +88 -10
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +542 -0
- rem/api/routers/chat/streaming.py +642 -45
- rem/api/routers/dev.py +81 -0
- rem/api/routers/feedback.py +268 -0
- rem/api/routers/messages.py +473 -0
- rem/api/routers/models.py +78 -0
- rem/api/routers/query.py +360 -0
- rem/api/routers/shared_sessions.py +406 -0
- rem/auth/middleware.py +126 -27
- rem/cli/commands/README.md +237 -64
- rem/cli/commands/ask.py +13 -10
- rem/cli/commands/cluster.py +1808 -0
- rem/cli/commands/configure.py +5 -6
- rem/cli/commands/db.py +396 -139
- rem/cli/commands/experiments.py +293 -73
- rem/cli/commands/process.py +22 -15
- rem/cli/commands/scaffold.py +47 -0
- rem/cli/commands/schema.py +97 -50
- rem/cli/main.py +29 -6
- rem/config.py +10 -3
- rem/models/core/core_model.py +7 -1
- rem/models/core/rem_query.py +5 -2
- rem/models/entities/__init__.py +21 -0
- rem/models/entities/domain_resource.py +38 -0
- rem/models/entities/feedback.py +123 -0
- rem/models/entities/message.py +30 -1
- rem/models/entities/session.py +83 -0
- rem/models/entities/shared_session.py +180 -0
- rem/models/entities/user.py +10 -3
- rem/registry.py +373 -0
- rem/schemas/agents/rem.yaml +7 -3
- rem/services/content/providers.py +94 -140
- rem/services/content/service.py +92 -20
- rem/services/dreaming/affinity_service.py +2 -16
- rem/services/dreaming/moment_service.py +2 -15
- rem/services/embeddings/api.py +24 -17
- rem/services/embeddings/worker.py +16 -16
- rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
- rem/services/phoenix/client.py +302 -28
- rem/services/postgres/README.md +159 -15
- rem/services/postgres/__init__.py +2 -1
- rem/services/postgres/diff_service.py +531 -0
- rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
- rem/services/postgres/repository.py +132 -0
- rem/services/postgres/schema_generator.py +291 -9
- rem/services/postgres/service.py +6 -6
- rem/services/rate_limit.py +113 -0
- rem/services/rem/README.md +14 -0
- rem/services/rem/parser.py +44 -9
- rem/services/rem/service.py +36 -2
- rem/services/session/compression.py +24 -1
- rem/services/session/reload.py +1 -1
- rem/services/user_service.py +98 -0
- rem/settings.py +313 -29
- rem/sql/background_indexes.sql +21 -16
- rem/sql/migrations/001_install.sql +387 -54
- rem/sql/migrations/002_install_models.sql +2320 -393
- rem/sql/migrations/003_optional_extensions.sql +326 -0
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/constants.py +97 -0
- rem/utils/date_utils.py +228 -0
- rem/utils/embeddings.py +17 -4
- rem/utils/files.py +167 -0
- rem/utils/mime_types.py +158 -0
- rem/utils/model_helpers.py +156 -1
- rem/utils/schema_loader.py +282 -35
- rem/utils/sql_paths.py +146 -0
- rem/utils/sql_types.py +3 -1
- rem/utils/vision.py +9 -14
- rem/workers/README.md +14 -14
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/db_maintainer.py +74 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.0.dist-info → remdb-0.3.127.dist-info}/METADATA +464 -289
- {remdb-0.3.0.dist-info → remdb-0.3.127.dist-info}/RECORD +104 -73
- {remdb-0.3.0.dist-info → remdb-0.3.127.dist-info}/WHEEL +1 -1
- rem/sql/002_install_models.sql +0 -1068
- rem/sql/install_models.sql +0 -1038
- {remdb-0.3.0.dist-info → remdb-0.3.127.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
|
-
|
|
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 =
|
|
76
|
-
) -> str:
|
|
88
|
+
default: str | None = None,
|
|
89
|
+
) -> str | None:
|
|
77
90
|
"""
|
|
78
|
-
Get user_id or
|
|
91
|
+
Get user_id or return None for anonymous access.
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
|
105
|
+
default: Explicit default (only for testing, not auto-generated)
|
|
87
106
|
|
|
88
107
|
Returns:
|
|
89
|
-
user_id if provided, otherwise
|
|
108
|
+
user_id if provided, explicit default if provided, otherwise None
|
|
90
109
|
|
|
91
110
|
Example:
|
|
92
|
-
#
|
|
93
|
-
user_id
|
|
94
|
-
|
|
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
|
|
116
|
+
# In MCP tool - anonymous user sees shared data
|
|
103
117
|
user_id = AgentContext.get_user_id_or_default(
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
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
|
)
|