remdb 0.3.14__py3-none-any.whl → 0.3.157__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 (112) hide show
  1. rem/agentic/README.md +76 -0
  2. rem/agentic/__init__.py +15 -0
  3. rem/agentic/agents/__init__.py +32 -2
  4. rem/agentic/agents/agent_manager.py +310 -0
  5. rem/agentic/agents/sse_simulator.py +502 -0
  6. rem/agentic/context.py +51 -27
  7. rem/agentic/context_builder.py +5 -3
  8. rem/agentic/llm_provider_models.py +301 -0
  9. rem/agentic/mcp/tool_wrapper.py +155 -18
  10. rem/agentic/otel/setup.py +93 -4
  11. rem/agentic/providers/phoenix.py +371 -108
  12. rem/agentic/providers/pydantic_ai.py +280 -57
  13. rem/agentic/schema.py +361 -21
  14. rem/agentic/tools/rem_tools.py +3 -3
  15. rem/api/README.md +215 -1
  16. rem/api/deps.py +255 -0
  17. rem/api/main.py +132 -40
  18. rem/api/mcp_router/resources.py +1 -1
  19. rem/api/mcp_router/server.py +28 -5
  20. rem/api/mcp_router/tools.py +555 -7
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +278 -4
  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 +697 -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/__init__.py +13 -3
  35. rem/auth/middleware.py +186 -22
  36. rem/auth/providers/__init__.py +4 -1
  37. rem/auth/providers/email.py +215 -0
  38. rem/cli/commands/README.md +237 -64
  39. rem/cli/commands/cluster.py +1808 -0
  40. rem/cli/commands/configure.py +4 -7
  41. rem/cli/commands/db.py +386 -143
  42. rem/cli/commands/experiments.py +468 -76
  43. rem/cli/commands/process.py +14 -8
  44. rem/cli/commands/schema.py +97 -50
  45. rem/cli/commands/session.py +336 -0
  46. rem/cli/dreaming.py +2 -2
  47. rem/cli/main.py +29 -6
  48. rem/config.py +10 -3
  49. rem/models/core/core_model.py +7 -1
  50. rem/models/core/experiment.py +58 -14
  51. rem/models/core/rem_query.py +5 -2
  52. rem/models/entities/__init__.py +25 -0
  53. rem/models/entities/domain_resource.py +38 -0
  54. rem/models/entities/feedback.py +123 -0
  55. rem/models/entities/message.py +30 -1
  56. rem/models/entities/ontology.py +1 -1
  57. rem/models/entities/ontology_config.py +1 -1
  58. rem/models/entities/session.py +83 -0
  59. rem/models/entities/shared_session.py +180 -0
  60. rem/models/entities/subscriber.py +175 -0
  61. rem/models/entities/user.py +1 -0
  62. rem/registry.py +10 -4
  63. rem/schemas/agents/core/agent-builder.yaml +134 -0
  64. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  65. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  66. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  67. rem/schemas/agents/rem.yaml +7 -3
  68. rem/services/__init__.py +3 -1
  69. rem/services/content/service.py +92 -19
  70. rem/services/email/__init__.py +10 -0
  71. rem/services/email/service.py +459 -0
  72. rem/services/email/templates.py +360 -0
  73. rem/services/embeddings/api.py +4 -4
  74. rem/services/embeddings/worker.py +16 -16
  75. rem/services/phoenix/client.py +154 -14
  76. rem/services/postgres/README.md +197 -15
  77. rem/services/postgres/__init__.py +2 -1
  78. rem/services/postgres/diff_service.py +547 -0
  79. rem/services/postgres/pydantic_to_sqlalchemy.py +470 -140
  80. rem/services/postgres/repository.py +132 -0
  81. rem/services/postgres/schema_generator.py +205 -4
  82. rem/services/postgres/service.py +6 -6
  83. rem/services/rem/parser.py +44 -9
  84. rem/services/rem/service.py +36 -2
  85. rem/services/session/compression.py +137 -51
  86. rem/services/session/reload.py +15 -8
  87. rem/settings.py +515 -27
  88. rem/sql/background_indexes.sql +21 -16
  89. rem/sql/migrations/001_install.sql +387 -54
  90. rem/sql/migrations/002_install_models.sql +2304 -377
  91. rem/sql/migrations/003_optional_extensions.sql +326 -0
  92. rem/sql/migrations/004_cache_system.sql +548 -0
  93. rem/sql/migrations/005_schema_update.sql +145 -0
  94. rem/utils/README.md +45 -0
  95. rem/utils/__init__.py +18 -0
  96. rem/utils/date_utils.py +2 -2
  97. rem/utils/files.py +157 -1
  98. rem/utils/model_helpers.py +156 -1
  99. rem/utils/schema_loader.py +220 -22
  100. rem/utils/sql_paths.py +146 -0
  101. rem/utils/sql_types.py +3 -1
  102. rem/utils/vision.py +1 -1
  103. rem/workers/__init__.py +3 -1
  104. rem/workers/db_listener.py +579 -0
  105. rem/workers/unlogged_maintainer.py +463 -0
  106. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/METADATA +340 -229
  107. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/RECORD +109 -80
  108. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/WHEEL +1 -1
  109. rem/sql/002_install_models.sql +0 -1068
  110. rem/sql/install_models.sql +0 -1051
  111. rem/sql/migrations/003_seed_default_user.sql +0 -48
  112. {remdb-0.3.14.dist-info → remdb-0.3.157.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,33 @@
1
+ """OTEL utilities for chat routers."""
2
+
3
+ from loguru import logger
4
+
5
+
6
+ def get_tracer():
7
+ """Get the OpenTelemetry tracer for chat completions."""
8
+ try:
9
+ from opentelemetry import trace
10
+ return trace.get_tracer("rem.chat.completions")
11
+ except Exception:
12
+ return None
13
+
14
+
15
+ def get_current_trace_context() -> tuple[str | None, str | None]:
16
+ """Get trace_id and span_id from current OTEL context.
17
+
18
+ Returns:
19
+ Tuple of (trace_id, span_id) as hex strings, or (None, None) if not available.
20
+ """
21
+ try:
22
+ from opentelemetry import trace
23
+
24
+ span = trace.get_current_span()
25
+ ctx = span.get_span_context()
26
+ if ctx.is_valid:
27
+ trace_id = format(ctx.trace_id, '032x')
28
+ span_id = format(ctx.span_id, '016x')
29
+ return trace_id, span_id
30
+ except Exception as e:
31
+ logger.debug(f"Could not get trace context: {e}")
32
+
33
+ return None, None
@@ -0,0 +1,542 @@
1
+ """
2
+ SSE Event Types for Rich Streaming Responses.
3
+
4
+ This module defines custom Server-Sent Events (SSE) event types that extend
5
+ beyond simple text streaming.
6
+
7
+ ## SSE Protocol
8
+
9
+ Text content uses **OpenAI-compatible format** (plain `data:` prefix):
10
+ ```
11
+ data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"Hello"}}]}
12
+ ```
13
+
14
+ Custom events use **named event format** (`event:` prefix):
15
+ ```
16
+ event: reasoning
17
+ data: {"type": "reasoning", "content": "Analyzing...", "step": 1}
18
+ ```
19
+
20
+ ## Event Types
21
+
22
+ | Event | Format | Purpose |
23
+ |-------|--------|---------|
24
+ | (text) | `data:` (OpenAI) | Content chunks - main response |
25
+ | reasoning | `event:` | Model thinking/chain-of-thought |
26
+ | progress | `event:` | Step indicators |
27
+ | tool_call | `event:` | Tool invocation start/complete |
28
+ | metadata | `event:` | System metadata (confidence, sources) |
29
+ | action_request | `event:` | UI solicitation (buttons, forms) |
30
+ | error | `event:` | Error notifications |
31
+ | done | `event:` | Stream completion marker |
32
+
33
+ ## Action Schema Design
34
+
35
+ - Inspired by Microsoft Adaptive Cards (https://adaptivecards.io/)
36
+ - JSON Schema-based UI element definitions
37
+ - Cross-platform compatibility for React, mobile, etc.
38
+
39
+ ## References
40
+
41
+ - MDN SSE: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
42
+ - Adaptive Cards: https://adaptivecards.io/explorer/
43
+ - Model Context Protocol: https://modelcontextprotocol.io/specification/2025-06-18
44
+ """
45
+
46
+ from enum import Enum
47
+ from typing import Any, Literal
48
+ from pydantic import BaseModel, Field
49
+
50
+
51
+ class SSEEventType(str, Enum):
52
+ """SSE event types for streaming responses."""
53
+
54
+ TEXT_DELTA = "text_delta" # Standard text chunk
55
+ REASONING = "reasoning" # Model thinking/reasoning
56
+ ACTION_REQUEST = "action_request" # UI action solicitation
57
+ METADATA = "metadata" # System metadata
58
+ PROGRESS = "progress" # Progress indicator
59
+ TOOL_CALL = "tool_call" # Tool invocation
60
+ ERROR = "error" # Error notification
61
+ DONE = "done" # Stream complete
62
+
63
+
64
+ # =============================================================================
65
+ # Action Solicitation Schema (Adaptive Cards-inspired)
66
+ # =============================================================================
67
+
68
+ class ActionStyle(str, Enum):
69
+ """Visual style for action buttons."""
70
+
71
+ DEFAULT = "default"
72
+ PRIMARY = "primary"
73
+ SECONDARY = "secondary"
74
+ DESTRUCTIVE = "destructive"
75
+ POSITIVE = "positive"
76
+
77
+
78
+ class ActionSubmit(BaseModel):
79
+ """
80
+ Submit action - triggers callback to server with payload.
81
+
82
+ Inspired by Adaptive Cards Action.Submit:
83
+ https://adaptivecards.io/explorer/Action.Submit.html
84
+ """
85
+
86
+ type: Literal["Action.Submit"] = "Action.Submit"
87
+ id: str = Field(description="Unique action identifier")
88
+ title: str = Field(description="Button label text")
89
+ style: ActionStyle = Field(
90
+ default=ActionStyle.DEFAULT,
91
+ description="Visual style"
92
+ )
93
+ data: dict[str, Any] = Field(
94
+ default_factory=dict,
95
+ description="Payload sent to server when action is triggered"
96
+ )
97
+ tooltip: str | None = Field(
98
+ default=None,
99
+ description="Tooltip text on hover"
100
+ )
101
+ icon_url: str | None = Field(
102
+ default=None,
103
+ description="Optional icon URL"
104
+ )
105
+
106
+
107
+ class ActionOpenUrl(BaseModel):
108
+ """
109
+ Open URL action - navigates to external URL.
110
+
111
+ Inspired by Adaptive Cards Action.OpenUrl:
112
+ https://adaptivecards.io/explorer/Action.OpenUrl.html
113
+ """
114
+
115
+ type: Literal["Action.OpenUrl"] = "Action.OpenUrl"
116
+ id: str = Field(description="Unique action identifier")
117
+ title: str = Field(description="Button label text")
118
+ url: str = Field(description="URL to open")
119
+ style: ActionStyle = Field(default=ActionStyle.DEFAULT)
120
+ tooltip: str | None = None
121
+
122
+
123
+ class ActionShowCard(BaseModel):
124
+ """
125
+ Show card action - reveals nested content inline.
126
+
127
+ Inspired by Adaptive Cards Action.ShowCard:
128
+ https://adaptivecards.io/explorer/Action.ShowCard.html
129
+ """
130
+
131
+ type: Literal["Action.ShowCard"] = "Action.ShowCard"
132
+ id: str = Field(description="Unique action identifier")
133
+ title: str = Field(description="Button label text")
134
+ card: dict[str, Any] = Field(
135
+ description="Nested card content to reveal (Adaptive Card JSON)"
136
+ )
137
+ style: ActionStyle = Field(default=ActionStyle.DEFAULT)
138
+
139
+
140
+ # Union type for all action types
141
+ ActionType = ActionSubmit | ActionOpenUrl | ActionShowCard
142
+
143
+
144
+ class InputText(BaseModel):
145
+ """Text input field for action cards."""
146
+
147
+ type: Literal["Input.Text"] = "Input.Text"
148
+ id: str = Field(description="Input field identifier (used in submit payload)")
149
+ label: str | None = Field(default=None, description="Input label")
150
+ placeholder: str | None = Field(default=None, description="Placeholder text")
151
+ is_required: bool = Field(default=False, description="Whether input is required")
152
+ is_multiline: bool = Field(default=False, description="Multi-line text area")
153
+ max_length: int | None = Field(default=None, description="Maximum character length")
154
+ value: str | None = Field(default=None, description="Default value")
155
+
156
+
157
+ class InputChoiceSet(BaseModel):
158
+ """Choice/select input for action cards."""
159
+
160
+ type: Literal["Input.ChoiceSet"] = "Input.ChoiceSet"
161
+ id: str = Field(description="Input field identifier")
162
+ label: str | None = None
163
+ choices: list[dict[str, str]] = Field(
164
+ description="List of {title, value} choice objects"
165
+ )
166
+ is_required: bool = False
167
+ is_multi_select: bool = Field(default=False, description="Allow multiple selections")
168
+ value: str | None = Field(default=None, description="Default selected value")
169
+
170
+
171
+ class InputToggle(BaseModel):
172
+ """Toggle/checkbox input for action cards."""
173
+
174
+ type: Literal["Input.Toggle"] = "Input.Toggle"
175
+ id: str = Field(description="Input field identifier")
176
+ title: str = Field(description="Toggle label text")
177
+ value: str = Field(default="false", description="Current value ('true'/'false')")
178
+ value_on: str = Field(default="true", description="Value when toggled on")
179
+ value_off: str = Field(default="false", description="Value when toggled off")
180
+
181
+
182
+ # Union type for all input types
183
+ InputType = InputText | InputChoiceSet | InputToggle
184
+
185
+
186
+ class ActionDisplayStyle(str, Enum):
187
+ """How to display the action request in the UI."""
188
+
189
+ INLINE = "inline" # Rendered inline after message content
190
+ FLOATING = "floating" # Floating panel/overlay
191
+ MODAL = "modal" # Modal dialog
192
+
193
+
194
+ class ActionRequestCard(BaseModel):
195
+ """
196
+ Action solicitation card - requests user input or action.
197
+
198
+ This is the main payload for action_request SSE events.
199
+ Uses Adaptive Cards-inspired schema for cross-platform UI compatibility.
200
+
201
+ Example use cases:
202
+ - Confirm/cancel dialogs
203
+ - Form inputs (name, email, etc.)
204
+ - Multi-choice selections
205
+ - Quick reply buttons
206
+ - Feedback collection (thumbs up/down)
207
+
208
+ Example:
209
+ ```json
210
+ {
211
+ "id": "confirm-delete-123",
212
+ "prompt": "Are you sure you want to delete this item?",
213
+ "display_style": "modal",
214
+ "actions": [
215
+ {
216
+ "type": "Action.Submit",
217
+ "id": "confirm",
218
+ "title": "Delete",
219
+ "style": "destructive",
220
+ "data": {"action": "delete", "item_id": "123"}
221
+ },
222
+ {
223
+ "type": "Action.Submit",
224
+ "id": "cancel",
225
+ "title": "Cancel",
226
+ "style": "secondary",
227
+ "data": {"action": "cancel"}
228
+ }
229
+ ],
230
+ "timeout_ms": 30000
231
+ }
232
+ ```
233
+ """
234
+
235
+ id: str = Field(description="Unique card identifier for response correlation")
236
+ prompt: str = Field(description="Prompt text explaining what action is requested")
237
+ display_style: ActionDisplayStyle = Field(
238
+ default=ActionDisplayStyle.INLINE,
239
+ description="How to display in the UI"
240
+ )
241
+ actions: list[ActionType] = Field(
242
+ default_factory=list,
243
+ description="Available actions (buttons)"
244
+ )
245
+ inputs: list[InputType] = Field(
246
+ default_factory=list,
247
+ description="Input fields for data collection"
248
+ )
249
+ timeout_ms: int | None = Field(
250
+ default=None,
251
+ description="Auto-dismiss timeout in milliseconds"
252
+ )
253
+ fallback_text: str | None = Field(
254
+ default=None,
255
+ description="Text to show if card rendering fails"
256
+ )
257
+
258
+
259
+ # =============================================================================
260
+ # SSE Event Payloads
261
+ # =============================================================================
262
+
263
+ class TextDeltaEvent(BaseModel):
264
+ """Text content delta event (OpenAI-compatible)."""
265
+
266
+ type: Literal["text_delta"] = "text_delta"
267
+ content: str = Field(description="Text content chunk")
268
+
269
+
270
+ class ReasoningEvent(BaseModel):
271
+ """
272
+ Reasoning/thinking event.
273
+
274
+ Used to stream model's chain-of-thought reasoning separate from
275
+ the main response content. UI can display this in a collapsible
276
+ "thinking" section.
277
+ """
278
+
279
+ type: Literal["reasoning"] = "reasoning"
280
+ content: str = Field(description="Reasoning text chunk")
281
+ step: int | None = Field(
282
+ default=None,
283
+ description="Reasoning step number (for multi-step reasoning)"
284
+ )
285
+
286
+
287
+ class ActionRequestEvent(BaseModel):
288
+ """
289
+ Action request event - solicits user action.
290
+
291
+ Sent when the agent needs user input or confirmation.
292
+ """
293
+
294
+ type: Literal["action_request"] = "action_request"
295
+ card: ActionRequestCard = Field(description="Action card definition")
296
+
297
+
298
+ class MetadataEvent(BaseModel):
299
+ """
300
+ Metadata event - system information (often hidden from user).
301
+
302
+ Used for confidence scores, sources, model info, message IDs, etc.
303
+ """
304
+
305
+ type: Literal["metadata"] = "metadata"
306
+
307
+ # Message correlation IDs
308
+ message_id: str | None = Field(
309
+ default=None,
310
+ description="Database ID of the assistant message being streamed"
311
+ )
312
+ in_reply_to: str | None = Field(
313
+ default=None,
314
+ description="Database ID of the user message this is responding to"
315
+ )
316
+ session_id: str | None = Field(
317
+ default=None,
318
+ description="Session ID for this conversation"
319
+ )
320
+
321
+ # Agent info
322
+ agent_schema: str | None = Field(
323
+ default=None,
324
+ description="Name of the agent schema used for this response (e.g., 'rem', 'query-assistant')"
325
+ )
326
+
327
+ # Session info
328
+ session_name: str | None = Field(
329
+ default=None,
330
+ description="Short 1-3 phrase name for the session topic (e.g., 'Prescription Drug Questions', 'AWS Setup Help')"
331
+ )
332
+
333
+ # Quality indicators
334
+ confidence: float | None = Field(
335
+ default=None, ge=0, le=1,
336
+ description="Confidence score (0-1)"
337
+ )
338
+ sources: list[str] | None = Field(
339
+ default=None,
340
+ description="Referenced sources/citations"
341
+ )
342
+
343
+ # Model info
344
+ model_version: str | None = Field(
345
+ default=None,
346
+ description="Model version used"
347
+ )
348
+
349
+ # Performance metrics
350
+ latency_ms: int | None = Field(
351
+ default=None,
352
+ description="Response latency in milliseconds"
353
+ )
354
+ token_count: int | None = Field(
355
+ default=None,
356
+ description="Token count for this response"
357
+ )
358
+
359
+ # Trace context for observability (deterministic, captured from OTEL)
360
+ trace_id: str | None = Field(
361
+ default=None,
362
+ description="OTEL trace ID for correlating with Phoenix/observability systems"
363
+ )
364
+ span_id: str | None = Field(
365
+ default=None,
366
+ description="OTEL span ID for correlating with Phoenix/observability systems"
367
+ )
368
+
369
+ # System flags
370
+ flags: list[str] | None = Field(
371
+ default=None,
372
+ description="System flags (e.g., 'uncertain', 'needs_review')"
373
+ )
374
+ hidden: bool = Field(
375
+ default=False,
376
+ description="If true, should not be displayed to user"
377
+ )
378
+ extra: dict[str, Any] | None = Field(
379
+ default=None,
380
+ description="Additional metadata"
381
+ )
382
+
383
+
384
+ class ProgressEvent(BaseModel):
385
+ """Progress indicator event."""
386
+
387
+ type: Literal["progress"] = "progress"
388
+ step: int = Field(description="Current step number")
389
+ total_steps: int = Field(description="Total number of steps")
390
+ label: str = Field(description="Step description")
391
+ status: Literal["pending", "in_progress", "completed", "failed"] = Field(
392
+ description="Step status"
393
+ )
394
+
395
+
396
+ class ToolCallEvent(BaseModel):
397
+ """Tool invocation event."""
398
+
399
+ type: Literal["tool_call"] = "tool_call"
400
+ tool_name: str = Field(description="Name of tool being called")
401
+ tool_id: str | None = Field(
402
+ default=None,
403
+ description="Unique call identifier"
404
+ )
405
+ status: Literal["started", "completed", "failed"] = Field(
406
+ description="Tool call status"
407
+ )
408
+ arguments: dict[str, Any] | None = Field(
409
+ default=None,
410
+ description="Tool arguments (for 'started' status)"
411
+ )
412
+ result: str | None = Field(
413
+ default=None,
414
+ description="Tool result summary (for 'completed' status)"
415
+ )
416
+ error: str | None = Field(
417
+ default=None,
418
+ description="Error message (for 'failed' status)"
419
+ )
420
+
421
+
422
+ class ErrorEvent(BaseModel):
423
+ """Error notification event."""
424
+
425
+ type: Literal["error"] = "error"
426
+ code: str = Field(description="Error code")
427
+ message: str = Field(description="Human-readable error message")
428
+ details: dict[str, Any] | None = Field(
429
+ default=None,
430
+ description="Additional error details"
431
+ )
432
+ recoverable: bool = Field(
433
+ default=True,
434
+ description="Whether error is recoverable"
435
+ )
436
+
437
+
438
+ class DoneEvent(BaseModel):
439
+ """Stream completion event."""
440
+
441
+ type: Literal["done"] = "done"
442
+ reason: Literal["stop", "length", "error", "cancelled"] = Field(
443
+ default="stop",
444
+ description="Completion reason"
445
+ )
446
+
447
+
448
+ # Union type for all SSE events
449
+ SSEEvent = (
450
+ TextDeltaEvent
451
+ | ReasoningEvent
452
+ | ActionRequestEvent
453
+ | MetadataEvent
454
+ | ProgressEvent
455
+ | ToolCallEvent
456
+ | ErrorEvent
457
+ | DoneEvent
458
+ )
459
+
460
+
461
+ # =============================================================================
462
+ # SSE Formatting Helpers
463
+ # =============================================================================
464
+
465
+ def format_sse_event(event: SSEEvent) -> str:
466
+ """
467
+ Format an SSE event for transmission.
468
+
469
+ Standard data: format for text_delta (OpenAI compatibility).
470
+ Named event: format for other event types.
471
+
472
+ Args:
473
+ event: SSE event to format
474
+
475
+ Returns:
476
+ Formatted SSE string ready for transmission
477
+
478
+ Example:
479
+ >>> event = ReasoningEvent(content="Analyzing...")
480
+ >>> format_sse_event(event)
481
+ 'event: reasoning\\ndata: {"type": "reasoning", "content": "Analyzing..."}\\n\\n'
482
+ """
483
+ import json
484
+
485
+ event_json = event.model_dump_json()
486
+
487
+ # TextDeltaEvent uses standard data: format for OpenAI compatibility
488
+ if isinstance(event, TextDeltaEvent):
489
+ return f"data: {event_json}\n\n"
490
+
491
+ # DoneEvent uses special marker
492
+ if isinstance(event, DoneEvent):
493
+ return f"event: done\ndata: {event_json}\n\n"
494
+
495
+ # All other events use named event format
496
+ event_type = event.type
497
+ return f"event: {event_type}\ndata: {event_json}\n\n"
498
+
499
+
500
+ def format_openai_sse_chunk(
501
+ request_id: str,
502
+ created: int,
503
+ model: str,
504
+ content: str | None = None,
505
+ role: str | None = None,
506
+ finish_reason: str | None = None,
507
+ ) -> str:
508
+ """
509
+ Format OpenAI-compatible SSE chunk.
510
+
511
+ Args:
512
+ request_id: Request/response ID
513
+ created: Unix timestamp
514
+ model: Model name
515
+ content: Delta content
516
+ role: Message role (usually 'assistant')
517
+ finish_reason: Finish reason (e.g., 'stop')
518
+
519
+ Returns:
520
+ Formatted SSE data line
521
+ """
522
+ import json
523
+
524
+ delta = {}
525
+ if role:
526
+ delta["role"] = role
527
+ if content is not None:
528
+ delta["content"] = content
529
+
530
+ chunk = {
531
+ "id": request_id,
532
+ "object": "chat.completion.chunk",
533
+ "created": created,
534
+ "model": model,
535
+ "choices": [{
536
+ "index": 0,
537
+ "delta": delta,
538
+ "finish_reason": finish_reason
539
+ }]
540
+ }
541
+
542
+ return f"data: {json.dumps(chunk)}\n\n"