remdb 0.3.14__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 (89) hide show
  1. rem/agentic/README.md +76 -0
  2. rem/agentic/__init__.py +15 -0
  3. rem/agentic/agents/__init__.py +16 -2
  4. rem/agentic/agents/sse_simulator.py +502 -0
  5. rem/agentic/context.py +51 -27
  6. rem/agentic/llm_provider_models.py +301 -0
  7. rem/agentic/mcp/tool_wrapper.py +112 -17
  8. rem/agentic/otel/setup.py +93 -4
  9. rem/agentic/providers/phoenix.py +302 -109
  10. rem/agentic/providers/pydantic_ai.py +215 -26
  11. rem/agentic/schema.py +361 -21
  12. rem/agentic/tools/rem_tools.py +3 -3
  13. rem/api/README.md +215 -1
  14. rem/api/deps.py +255 -0
  15. rem/api/main.py +132 -40
  16. rem/api/mcp_router/resources.py +1 -1
  17. rem/api/mcp_router/server.py +26 -5
  18. rem/api/mcp_router/tools.py +465 -7
  19. rem/api/routers/admin.py +494 -0
  20. rem/api/routers/auth.py +70 -0
  21. rem/api/routers/chat/completions.py +402 -20
  22. rem/api/routers/chat/models.py +88 -10
  23. rem/api/routers/chat/otel_utils.py +33 -0
  24. rem/api/routers/chat/sse_events.py +542 -0
  25. rem/api/routers/chat/streaming.py +642 -45
  26. rem/api/routers/dev.py +81 -0
  27. rem/api/routers/feedback.py +268 -0
  28. rem/api/routers/messages.py +473 -0
  29. rem/api/routers/models.py +78 -0
  30. rem/api/routers/query.py +360 -0
  31. rem/api/routers/shared_sessions.py +406 -0
  32. rem/auth/middleware.py +126 -27
  33. rem/cli/commands/README.md +237 -64
  34. rem/cli/commands/cluster.py +1808 -0
  35. rem/cli/commands/configure.py +1 -3
  36. rem/cli/commands/db.py +386 -143
  37. rem/cli/commands/experiments.py +418 -27
  38. rem/cli/commands/process.py +14 -8
  39. rem/cli/commands/schema.py +97 -50
  40. rem/cli/main.py +27 -6
  41. rem/config.py +10 -3
  42. rem/models/core/core_model.py +7 -1
  43. rem/models/core/experiment.py +54 -0
  44. rem/models/core/rem_query.py +5 -2
  45. rem/models/entities/__init__.py +21 -0
  46. rem/models/entities/domain_resource.py +38 -0
  47. rem/models/entities/feedback.py +123 -0
  48. rem/models/entities/message.py +30 -1
  49. rem/models/entities/session.py +83 -0
  50. rem/models/entities/shared_session.py +180 -0
  51. rem/registry.py +10 -4
  52. rem/schemas/agents/rem.yaml +7 -3
  53. rem/services/content/service.py +92 -20
  54. rem/services/embeddings/api.py +4 -4
  55. rem/services/embeddings/worker.py +16 -16
  56. rem/services/phoenix/client.py +154 -14
  57. rem/services/postgres/README.md +159 -15
  58. rem/services/postgres/__init__.py +2 -1
  59. rem/services/postgres/diff_service.py +531 -0
  60. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  61. rem/services/postgres/repository.py +132 -0
  62. rem/services/postgres/schema_generator.py +205 -4
  63. rem/services/postgres/service.py +6 -6
  64. rem/services/rem/parser.py +44 -9
  65. rem/services/rem/service.py +36 -2
  66. rem/services/session/compression.py +24 -1
  67. rem/services/session/reload.py +1 -1
  68. rem/settings.py +324 -23
  69. rem/sql/background_indexes.sql +21 -16
  70. rem/sql/migrations/001_install.sql +387 -54
  71. rem/sql/migrations/002_install_models.sql +2320 -393
  72. rem/sql/migrations/003_optional_extensions.sql +326 -0
  73. rem/sql/migrations/004_cache_system.sql +548 -0
  74. rem/utils/__init__.py +18 -0
  75. rem/utils/date_utils.py +2 -2
  76. rem/utils/model_helpers.py +156 -1
  77. rem/utils/schema_loader.py +220 -22
  78. rem/utils/sql_paths.py +146 -0
  79. rem/utils/sql_types.py +3 -1
  80. rem/workers/__init__.py +3 -1
  81. rem/workers/db_listener.py +579 -0
  82. rem/workers/unlogged_maintainer.py +463 -0
  83. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/METADATA +335 -226
  84. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/RECORD +86 -66
  85. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/WHEEL +1 -1
  86. rem/sql/002_install_models.sql +0 -1068
  87. rem/sql/install_models.sql +0 -1051
  88. rem/sql/migrations/003_seed_default_user.sql +0 -48
  89. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/entry_points.txt +0 -0
rem/agentic/README.md CHANGED
@@ -640,6 +640,82 @@ If `query_agent_model` is not set, the agent uses `settings.llm.default_model`.
640
640
  - PostgreSQL dialect aware (knows about KV_STORE, embeddings tables)
641
641
  - Can generate multi-step query plans for complex questions
642
642
 
643
+ ## SSE Simulator
644
+
645
+ The SSE Simulator is a **programmatic** event generator (not an LLM-based agent) that produces
646
+ a scripted sequence of SSE events for testing and demonstrating the streaming protocol.
647
+
648
+ ### Purpose
649
+
650
+ - Frontend development without LLM costs
651
+ - Testing SSE parsing and rendering
652
+ - Demonstrating the full event protocol
653
+ - Load testing streaming infrastructure
654
+
655
+ ### Usage
656
+
657
+ ```python
658
+ from rem.agentic.agents import stream_simulator_events
659
+ from rem.api.routers.chat.sse_events import format_sse_event
660
+
661
+ # Generate all event types
662
+ async for event in stream_simulator_events("demo"):
663
+ print(format_sse_event(event))
664
+
665
+ # Minimal demo (text + done only)
666
+ from rem.agentic.agents import stream_minimal_demo
667
+ async for event in stream_minimal_demo("Hello world!"):
668
+ print(event)
669
+
670
+ # Error simulation
671
+ from rem.agentic.agents import stream_error_demo
672
+ async for event in stream_error_demo(error_after_words=10):
673
+ print(event)
674
+ ```
675
+
676
+ ### Event Sequence
677
+
678
+ The full simulator produces events in this order:
679
+
680
+ 1. **Reasoning** (4 steps) - Model thinking process
681
+ 2. **Progress** (step 1/4) - Starting
682
+ 3. **Tool calls** (2 tools) - Simulated tool invocations
683
+ 4. **Progress** (step 2/4) - Generating
684
+ 5. **Text deltas** - Streamed markdown content
685
+ 6. **Progress** (step 3/4) - Formatting
686
+ 7. **Metadata** - Confidence, sources, flags
687
+ 8. **Progress** (step 4/4) - Preparing actions
688
+ 9. **Action request** - Feedback card with buttons and inputs
689
+ 10. **Progress** (all complete)
690
+ 11. **Done** - Stream completion
691
+
692
+ ### Configuration Options
693
+
694
+ ```python
695
+ await stream_simulator_events(
696
+ prompt="demo",
697
+ delay_ms=50, # Delay between events
698
+ include_reasoning=True, # Emit reasoning events
699
+ include_progress=True, # Emit progress events
700
+ include_tool_calls=True, # Emit tool call events
701
+ include_actions=True, # Emit action request at end
702
+ include_metadata=True, # Emit metadata event
703
+ )
704
+ ```
705
+
706
+ ### HTTP Endpoint
707
+
708
+ Use the simulator via the chat completions endpoint:
709
+
710
+ ```bash
711
+ curl -X POST http://localhost:8000/api/v1/chat/completions \
712
+ -H "Content-Type: application/json" \
713
+ -H "X-Agent-Schema: simulator" \
714
+ -d '{"messages": [{"role": "user", "content": "demo"}], "stream": true}'
715
+ ```
716
+
717
+ See `rem/api/README.md` for full SSE event protocol documentation.
718
+
643
719
  ## Future Work
644
720
 
645
721
  - [ ] Phoenix evaluator integration
rem/agentic/__init__.py CHANGED
@@ -17,6 +17,14 @@ from .schema import (
17
17
  )
18
18
  from .providers.pydantic_ai import create_agent_from_schema_file, create_agent, AgentRuntime
19
19
  from .query_helper import ask_rem, REMQueryOutput
20
+ from .llm_provider_models import (
21
+ ModelInfo,
22
+ AVAILABLE_MODELS,
23
+ ALLOWED_MODEL_IDS,
24
+ is_valid_model,
25
+ get_valid_model_or_default,
26
+ get_model_by_id,
27
+ )
20
28
 
21
29
  __all__ = [
22
30
  # Context and Query
@@ -36,4 +44,11 @@ __all__ = [
36
44
  # REM Query Helpers
37
45
  "ask_rem",
38
46
  "REMQueryOutput",
47
+ # LLM Provider Models
48
+ "ModelInfo",
49
+ "AVAILABLE_MODELS",
50
+ "ALLOWED_MODEL_IDS",
51
+ "is_valid_model",
52
+ "get_valid_model_or_default",
53
+ "get_model_by_id",
39
54
  ]
@@ -1,8 +1,22 @@
1
1
  """
2
2
  REM Agents - Specialized agents for REM operations.
3
3
 
4
- All agents are defined as YAML schemas in src/rem/schemas/agents/.
4
+ Most agents are defined as YAML schemas in src/rem/schemas/agents/.
5
5
  Use create_agent_from_schema_file() to instantiate agents.
6
+
7
+ The SSE Simulator is a special programmatic "agent" that generates
8
+ scripted SSE events for testing and demonstration - it doesn't use an LLM.
6
9
  """
7
10
 
8
- __all__ = []
11
+ from .sse_simulator import (
12
+ stream_simulator_events,
13
+ stream_minimal_demo,
14
+ stream_error_demo,
15
+ )
16
+
17
+ __all__ = [
18
+ # SSE Simulator (programmatic, no LLM)
19
+ "stream_simulator_events",
20
+ "stream_minimal_demo",
21
+ "stream_error_demo",
22
+ ]
@@ -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"