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
@@ -1,17 +1,43 @@
1
1
  """
2
2
  OpenAI-compatible API models for chat completions.
3
3
 
4
- Design Pattern
4
+ Design Pattern:
5
5
  - Full OpenAI compatibility for drop-in replacement
6
6
  - Support for streaming (SSE) and non-streaming modes
7
7
  - Response format control (text vs json_object)
8
- - Headers map to AgentContext (X-User-Id, X-Tenant-Id, X-Agent-Schema, etc.)
8
+ - Headers map to AgentContext for session/context control
9
+ - Body fields for OpenAI-compatible parameters + metadata
10
+
11
+ Headers (context control):
12
+ X-User-Id → context.user_id (user identifier)
13
+ X-Tenant-Id → context.tenant_id (multi-tenancy, default: "default")
14
+ X-Session-Id → context.session_id (conversation continuity)
15
+ X-Agent-Schema → context.agent_schema_uri (which agent to use, default: "rem")
16
+ X-Model-Name → context.default_model (model override)
17
+ X-Chat-Is-Audio → triggers audio transcription ("true"/"false")
18
+ X-Is-Eval → context.is_eval (marks session as evaluation, sets mode=EVALUATION)
19
+
20
+ Body Fields (OpenAI-compatible + extensions):
21
+ model → LLM model (e.g., "openai:gpt-4.1", "anthropic:claude-sonnet-4-5-20250929")
22
+ messages → Chat conversation history
23
+ temperature → Sampling temperature (0-2)
24
+ max_tokens → Max tokens (deprecated, use max_completion_tokens)
25
+ max_completion_tokens → Max tokens to generate
26
+ stream → Enable SSE streaming
27
+ metadata → Key-value pairs merged with session metadata (for evals/experiments)
28
+ store → Whether to store for distillation/evaluation
29
+ seed → Deterministic sampling seed
30
+ top_p → Nucleus sampling probability
31
+ reasoning_effort → low/medium/high for o-series models
32
+ service_tier → auto/flex/priority/default
9
33
  """
10
34
 
11
- from typing import Literal
35
+ from typing import Any, Literal
12
36
 
13
37
  from pydantic import BaseModel, Field
14
38
 
39
+ from rem.settings import settings
40
+
15
41
 
16
42
  # Request models
17
43
  class ChatMessage(BaseModel):
@@ -44,17 +70,26 @@ class ChatCompletionRequest(BaseModel):
44
70
  Compatible with OpenAI's /v1/chat/completions endpoint.
45
71
 
46
72
  Headers Map to AgentContext:
47
- - X-User-Id → context.user_id
48
- - X-Tenant-Id → context.tenant_id
49
- - X-Session-Id → context.session_id
50
- - X-Agent-Schema → context.agent_schema_uri
73
+ X-User-Id → context.user_id
74
+ X-Tenant-Id → context.tenant_id (default: "default")
75
+ X-Session-Id → context.session_id
76
+ X-Agent-Schema → context.agent_schema_uri (default: "rem")
77
+ X-Model-Name → context.default_model
78
+ X-Chat-Is-Audio → triggers audio transcription
79
+ X-Is-Eval → context.is_eval (sets session mode=EVALUATION)
80
+
81
+ Body Fields for Metadata/Evals:
82
+ metadata → Key-value pairs merged with session metadata
83
+ store → Whether to store for distillation/evaluation
51
84
 
52
85
  Note: Model is specified in body.model (standard OpenAI field), not headers.
53
86
  """
54
87
 
55
- model: str = Field(
56
- default="anthropic:claude-sonnet-4-5-20250929",
57
- description="Model to use (standard OpenAI field)",
88
+ # TODO: default should come from settings.llm.default_model at request time
89
+ # Using None and resolving in endpoint to avoid import-time settings evaluation
90
+ model: str | None = Field(
91
+ default=None,
92
+ description="Model to use. Defaults to LLM__DEFAULT_MODEL from settings.",
58
93
  )
59
94
  messages: list[ChatMessage] = Field(description="Chat conversation history")
60
95
  temperature: float | None = Field(default=None, ge=0, le=2)
@@ -69,6 +104,49 @@ class ChatCompletionRequest(BaseModel):
69
104
  default=None,
70
105
  description="Response format. Set type='json_object' to enable JSON mode.",
71
106
  )
107
+ # Additional OpenAI-compatible fields
108
+ metadata: dict[str, str] | None = Field(
109
+ default=None,
110
+ description="Key-value pairs attached to the request (max 16 keys, 64/512 char limits). "
111
+ "Merged with session metadata for persistence.",
112
+ )
113
+ store: bool | None = Field(
114
+ default=None,
115
+ description="Whether to store for distillation/evaluation purposes.",
116
+ )
117
+ max_completion_tokens: int | None = Field(
118
+ default=None,
119
+ ge=1,
120
+ description="Max tokens to generate (replaces deprecated max_tokens).",
121
+ )
122
+ seed: int | None = Field(
123
+ default=None,
124
+ description="Seed for deterministic sampling (best effort).",
125
+ )
126
+ top_p: float | None = Field(
127
+ default=None,
128
+ ge=0,
129
+ le=1,
130
+ description="Nucleus sampling probability. Use temperature OR top_p, not both.",
131
+ )
132
+ logprobs: bool | None = Field(
133
+ default=None,
134
+ description="Whether to return log probabilities for output tokens.",
135
+ )
136
+ top_logprobs: int | None = Field(
137
+ default=None,
138
+ ge=0,
139
+ le=20,
140
+ description="Number of most likely tokens to return at each position (requires logprobs=true).",
141
+ )
142
+ reasoning_effort: Literal["low", "medium", "high"] | None = Field(
143
+ default=None,
144
+ description="Reasoning effort for o-series models (low/medium/high).",
145
+ )
146
+ service_tier: Literal["auto", "flex", "priority", "default"] | None = Field(
147
+ default=None,
148
+ description="Service tier for processing (flex is 50% cheaper but slower).",
149
+ )
72
150
 
73
151
 
74
152
  # Response models
@@ -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"