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/schema.py CHANGED
@@ -13,7 +13,7 @@ The schema protocol serves as:
13
13
  """
14
14
 
15
15
  from typing import Any, Literal
16
- from pydantic import BaseModel, Field
16
+ from pydantic import BaseModel, Field, field_validator
17
17
 
18
18
 
19
19
  class MCPToolReference(BaseModel):
@@ -23,11 +23,21 @@ class MCPToolReference(BaseModel):
23
23
  Tools are functions that agents can call during execution to
24
24
  interact with external systems, retrieve data, or perform actions.
25
25
 
26
- Example:
26
+ Two usage patterns:
27
+ 1. With mcp_servers config: Just declare name + description, tools loaded from MCP servers
28
+ 2. Explicit MCP server: Specify mcp_server to load tool from specific server
29
+
30
+ Example (declarative with mcp_servers):
31
+ {
32
+ "name": "search_rem",
33
+ "description": "Execute REM queries for entity lookup and search"
34
+ }
35
+
36
+ Example (explicit server):
27
37
  {
28
38
  "name": "lookup_entity",
29
39
  "mcp_server": "rem",
30
- "description": "Lookup entities by exact key with O(1) performance"
40
+ "description": "Lookup entities by exact key"
31
41
  }
32
42
  """
33
43
 
@@ -38,20 +48,20 @@ class MCPToolReference(BaseModel):
38
48
  )
39
49
  )
40
50
 
41
- mcp_server: str = Field(
51
+ mcp_server: str | None = Field(
52
+ default=None,
42
53
  description=(
43
- "MCP server identifier. Resolved via environment variable: "
44
- "MCP_SERVER_{NAME} or MCP__{NAME}__URL. "
45
- "Common values: 'rem' (REM knowledge graph), 'filesystem', 'web'."
54
+ "MCP server identifier (optional when using mcp_servers config). "
55
+ "If not specified, tool is expected from configured mcp_servers. "
56
+ "Resolved via environment variable: MCP_SERVER_{NAME} or MCP__{NAME}__URL."
46
57
  )
47
58
  )
48
59
 
49
60
  description: str | None = Field(
50
61
  default=None,
51
62
  description=(
52
- "Optional description override. If provided, replaces the tool's "
53
- "description from the MCP server in the agent's context. "
54
- "Use this to provide agent-specific guidance on tool usage."
63
+ "Tool description for the agent. Explains what the tool does "
64
+ "and when to use it. This is visible to the LLM."
55
65
  ),
56
66
  )
57
67
 
@@ -63,29 +73,92 @@ class MCPResourceReference(BaseModel):
63
73
  Resources are data sources that can be read by agents, such as
64
74
  knowledge graph entities, files, or API endpoints.
65
75
 
66
- Example:
76
+ Two formats supported:
77
+ 1. uri: Exact URI or URI with query params
78
+ 2. uri_pattern: Regex pattern for flexible matching
79
+
80
+ Example (exact URI):
81
+ {
82
+ "uri": "rem://schemas",
83
+ "name": "Agent Schemas",
84
+ "description": "List all available agent schemas"
85
+ }
86
+
87
+ Example (pattern):
67
88
  {
68
89
  "uri_pattern": "rem://resources/.*",
69
90
  "mcp_server": "rem"
70
91
  }
71
92
  """
72
93
 
73
- uri_pattern: str = Field(
94
+ # Support both exact URI and pattern
95
+ uri: str | None = Field(
96
+ default=None,
97
+ description=(
98
+ "Exact resource URI or URI with query parameters. "
99
+ "Examples: 'rem://schemas', 'rem://resources?category=drug.*'"
100
+ )
101
+ )
102
+
103
+ uri_pattern: str | None = Field(
104
+ default=None,
74
105
  description=(
75
106
  "Regex pattern matching resource URIs. "
76
- "Examples: "
77
- "'rem://resources/.*' (all resources), "
78
- "'rem://moments/.*' (all moments), "
79
- "'file:///data/.*' (local files). "
80
- "Supports full regex syntax for flexible matching."
107
+ "Examples: 'rem://resources/.*' (all resources). "
108
+ "Use uri for exact URIs, uri_pattern for regex matching."
109
+ )
110
+ )
111
+
112
+ name: str | None = Field(
113
+ default=None,
114
+ description="Human-readable name for the resource."
115
+ )
116
+
117
+ description: str | None = Field(
118
+ default=None,
119
+ description="Description of what the resource provides."
120
+ )
121
+
122
+ mcp_server: str | None = Field(
123
+ default=None,
124
+ description=(
125
+ "MCP server identifier (optional when using mcp_servers config). "
126
+ "Resolved via environment variable MCP_SERVER_{NAME}."
127
+ )
128
+ )
129
+
130
+
131
+ class MCPServerConfig(BaseModel):
132
+ """
133
+ MCP server configuration for in-process tool loading.
134
+
135
+ Example:
136
+ {
137
+ "type": "local",
138
+ "module": "rem.mcp_server",
139
+ "id": "rem-local"
140
+ }
141
+ """
142
+
143
+ type: Literal["local"] = Field(
144
+ default="local",
145
+ description="Server type. Currently only 'local' (in-process) is supported.",
146
+ )
147
+
148
+ module: str = Field(
149
+ description=(
150
+ "Python module path containing the MCP server. "
151
+ "The module must export an 'mcp' object that supports get_tools(). "
152
+ "Example: 'rem.mcp_server'"
81
153
  )
82
154
  )
83
155
 
84
- mcp_server: str = Field(
156
+ id: str = Field(
157
+ default="mcp-server",
85
158
  description=(
86
- "MCP server identifier that provides these resources. "
87
- "Resolved via environment variable MCP_SERVER_{NAME}. "
88
- "The server must expose resources matching the uri_pattern."
159
+ "Server identifier for logging and debugging. "
160
+ "Defaults to 'mcp-server' if not specified. "
161
+ "Example: 'rem-local'"
89
162
  )
90
163
  )
91
164
 
@@ -130,6 +203,38 @@ class AgentSchemaMetadata(BaseModel):
130
203
  ),
131
204
  )
132
205
 
206
+ # System prompt override (takes precedence over description when present)
207
+ system_prompt: str | None = Field(
208
+ default=None,
209
+ description=(
210
+ "Custom system prompt that overrides or extends the schema description. "
211
+ "When present, this is combined with the main schema.description field "
212
+ "to form the complete system prompt. Use this for detailed instructions "
213
+ "that you don't want in the public schema description."
214
+ ),
215
+ )
216
+
217
+ # Structured output toggle
218
+ structured_output: bool = Field(
219
+ default=True,
220
+ description=(
221
+ "Whether to enforce structured JSON output. "
222
+ "When False, the agent produces free-form text and schema properties "
223
+ "are converted to prompt guidance instead. Default: True (JSON output)."
224
+ ),
225
+ )
226
+
227
+ # MCP server configurations (for dynamic tool loading)
228
+ mcp_servers: list[MCPServerConfig] = Field(
229
+ default_factory=list,
230
+ description=(
231
+ "MCP server configurations for dynamic tool loading. "
232
+ "Servers are loaded in-process at agent creation time. "
233
+ "All tools from configured servers become available to the agent. "
234
+ "If not specified, defaults to rem.mcp_server (REM's built-in tools)."
235
+ ),
236
+ )
237
+
133
238
  tools: list[MCPToolReference] = Field(
134
239
  default_factory=list,
135
240
  description=(
@@ -394,3 +499,238 @@ def create_agent_schema(
394
499
  json_schema_extra=metadata.model_dump(),
395
500
  **kwargs,
396
501
  )
502
+
503
+
504
+ # =============================================================================
505
+ # YAML and Database Serialization
506
+ # =============================================================================
507
+
508
+
509
+ def schema_to_dict(schema: AgentSchema, exclude_none: bool = True) -> dict[str, Any]:
510
+ """
511
+ Serialize AgentSchema to a dictionary suitable for YAML or database storage.
512
+
513
+ This produces the canonical format used in:
514
+ - YAML files (schemas/agents/*.yaml)
515
+ - Database spec column (schemas table)
516
+ - API responses
517
+
518
+ Args:
519
+ schema: AgentSchema instance to serialize
520
+ exclude_none: If True, omit None values from output
521
+
522
+ Returns:
523
+ Dictionary representation of the schema
524
+
525
+ Example:
526
+ >>> schema = AgentSchema(
527
+ ... description="System prompt...",
528
+ ... properties={"answer": {"type": "string"}},
529
+ ... json_schema_extra={"name": "my-agent", "structured_output": False}
530
+ ... )
531
+ >>> d = schema_to_dict(schema)
532
+ >>> d["json_schema_extra"]["name"]
533
+ "my-agent"
534
+ """
535
+ return schema.model_dump(exclude_none=exclude_none)
536
+
537
+
538
+ def schema_from_dict(data: dict[str, Any]) -> AgentSchema:
539
+ """
540
+ Deserialize a dictionary to AgentSchema.
541
+
542
+ This handles:
543
+ - YAML files loaded with yaml.safe_load()
544
+ - Database spec column (JSON)
545
+ - API request bodies
546
+
547
+ Args:
548
+ data: Dictionary containing schema data
549
+
550
+ Returns:
551
+ Validated AgentSchema instance
552
+
553
+ Raises:
554
+ ValidationError: If data doesn't match schema structure
555
+
556
+ Example:
557
+ >>> data = {"type": "object", "description": "...", "properties": {}, "json_schema_extra": {"name": "test"}}
558
+ >>> schema = schema_from_dict(data)
559
+ >>> schema.json_schema_extra["name"]
560
+ "test"
561
+ """
562
+ return AgentSchema.model_validate(data)
563
+
564
+
565
+ def schema_to_yaml(schema: AgentSchema) -> str:
566
+ """
567
+ Serialize AgentSchema to YAML string.
568
+
569
+ The output format matches the canonical schema file format:
570
+ ```yaml
571
+ type: object
572
+ description: |
573
+ System prompt here...
574
+ properties:
575
+ answer:
576
+ type: string
577
+ json_schema_extra:
578
+ name: my-agent
579
+ system_prompt: |
580
+ Extended prompt here...
581
+ ```
582
+
583
+ Args:
584
+ schema: AgentSchema instance to serialize
585
+
586
+ Returns:
587
+ YAML string representation
588
+
589
+ Example:
590
+ >>> schema = create_agent_schema(
591
+ ... description="You are a test agent",
592
+ ... properties={"answer": {"type": "string"}},
593
+ ... required=["answer"],
594
+ ... name="test-agent"
595
+ ... )
596
+ >>> yaml_str = schema_to_yaml(schema)
597
+ >>> "test-agent" in yaml_str
598
+ True
599
+ """
600
+ import yaml
601
+
602
+ return yaml.dump(
603
+ schema_to_dict(schema),
604
+ default_flow_style=False,
605
+ allow_unicode=True,
606
+ sort_keys=False,
607
+ )
608
+
609
+
610
+ def schema_from_yaml(yaml_content: str) -> AgentSchema:
611
+ """
612
+ Deserialize YAML string to AgentSchema.
613
+
614
+ Args:
615
+ yaml_content: YAML string containing schema definition
616
+
617
+ Returns:
618
+ Validated AgentSchema instance
619
+
620
+ Raises:
621
+ yaml.YAMLError: If YAML parsing fails
622
+ ValidationError: If schema structure is invalid
623
+
624
+ Example:
625
+ >>> yaml_str = '''
626
+ ... type: object
627
+ ... description: Test agent
628
+ ... properties:
629
+ ... answer:
630
+ ... type: string
631
+ ... json_schema_extra:
632
+ ... name: test
633
+ ... '''
634
+ >>> schema = schema_from_yaml(yaml_str)
635
+ >>> schema.json_schema_extra["name"]
636
+ "test"
637
+ """
638
+ import yaml
639
+
640
+ data = yaml.safe_load(yaml_content)
641
+ return schema_from_dict(data)
642
+
643
+
644
+ def schema_from_yaml_file(file_path: str) -> AgentSchema:
645
+ """
646
+ Load AgentSchema from a YAML file.
647
+
648
+ Args:
649
+ file_path: Path to YAML file
650
+
651
+ Returns:
652
+ Validated AgentSchema instance
653
+
654
+ Raises:
655
+ FileNotFoundError: If file doesn't exist
656
+ yaml.YAMLError: If YAML parsing fails
657
+ ValidationError: If schema structure is invalid
658
+
659
+ Example:
660
+ >>> schema = schema_from_yaml_file("schemas/agents/rem.yaml")
661
+ >>> schema.json_schema_extra["name"]
662
+ "rem"
663
+ """
664
+ with open(file_path, "r") as f:
665
+ return schema_from_yaml(f.read())
666
+
667
+
668
+ def get_system_prompt(schema: AgentSchema | dict[str, Any]) -> str:
669
+ """
670
+ Extract the complete system prompt from a schema.
671
+
672
+ Combines:
673
+ 1. schema.description (base system prompt / public description)
674
+ 2. json_schema_extra.system_prompt (extended instructions if present)
675
+
676
+ Args:
677
+ schema: AgentSchema instance or raw dict
678
+
679
+ Returns:
680
+ Complete system prompt string
681
+
682
+ Example:
683
+ >>> schema = AgentSchema(
684
+ ... description="Base description",
685
+ ... properties={},
686
+ ... json_schema_extra={"name": "test", "system_prompt": "Extended instructions"}
687
+ ... )
688
+ >>> prompt = get_system_prompt(schema)
689
+ >>> "Base description" in prompt and "Extended instructions" in prompt
690
+ True
691
+ """
692
+ if isinstance(schema, dict):
693
+ base = schema.get("description", "")
694
+ extra = schema.get("json_schema_extra", {})
695
+ custom = extra.get("system_prompt") if isinstance(extra, dict) else None
696
+ else:
697
+ base = schema.description
698
+ extra = schema.json_schema_extra
699
+ if isinstance(extra, dict):
700
+ custom = extra.get("system_prompt")
701
+ elif isinstance(extra, AgentSchemaMetadata):
702
+ custom = extra.system_prompt
703
+ else:
704
+ custom = None
705
+
706
+ if custom:
707
+ return f"{base}\n\n{custom}" if base else custom
708
+ return base
709
+
710
+
711
+ def get_metadata(schema: AgentSchema | dict[str, Any]) -> AgentSchemaMetadata:
712
+ """
713
+ Extract and validate metadata from a schema.
714
+
715
+ Args:
716
+ schema: AgentSchema instance or raw dict
717
+
718
+ Returns:
719
+ Validated AgentSchemaMetadata instance
720
+
721
+ Example:
722
+ >>> schema = {"json_schema_extra": {"name": "test", "system_prompt": "hello"}}
723
+ >>> meta = get_metadata(schema)
724
+ >>> meta.name
725
+ "test"
726
+ >>> meta.system_prompt
727
+ "hello"
728
+ """
729
+ if isinstance(schema, dict):
730
+ extra = schema.get("json_schema_extra", {})
731
+ else:
732
+ extra = schema.json_schema_extra
733
+
734
+ if isinstance(extra, AgentSchemaMetadata):
735
+ return extra
736
+ return AgentSchemaMetadata.model_validate(extra)
@@ -162,10 +162,10 @@ async def search_rem_tool(
162
162
  return {"status": "error", "error": f"Unknown query_type: {query_type}"}
163
163
 
164
164
  # Execute query
165
- logger.info(f"Executing REM query: {query_type} for user {user_id}")
165
+ logger.debug(f"Executing REM query: {query_type} for user {user_id}")
166
166
  result = await rem_service.execute_query(query)
167
167
 
168
- logger.info(f"Query completed: {query_type}")
168
+ logger.debug(f"Query completed: {query_type}")
169
169
  return {
170
170
  "status": "success",
171
171
  "query_type": query_type,
@@ -212,7 +212,7 @@ async def ingest_file_tool(
212
212
  is_local_server=is_local_server,
213
213
  )
214
214
 
215
- logger.info(
215
+ logger.debug(
216
216
  f"File ingestion complete: {result['file_name']} "
217
217
  f"(status: {result['processing_status']}, "
218
218
  f"resources: {result['resources_created']})"
rem/api/README.md CHANGED
@@ -158,9 +158,70 @@ The dreaming worker runs periodically to build user models:
158
158
  - User profile automatically loaded and injected into system message
159
159
  - Simpler for basic chatbots that always need context
160
160
 
161
+ ## Authentication
162
+
163
+ ### Production Authentication
164
+
165
+ When `AUTH__ENABLED=true`, users authenticate via OAuth (Google or Microsoft). The OAuth flow:
166
+
167
+ 1. User visits `/api/auth/google/login` or `/api/auth/microsoft/login`
168
+ 2. User authenticates with provider
169
+ 3. Callback stores user in session cookie
170
+ 4. Subsequent requests use session cookie
171
+
172
+ ### Development Token (Non-Production Only)
173
+
174
+ For local development and testing, you can use a dev token instead of OAuth. This endpoint is available at `/api/dev/token` whenever `ENVIRONMENT != "production"`, regardless of whether auth is enabled.
175
+
176
+ **Get Token:**
177
+ ```bash
178
+ curl http://localhost:8000/api/dev/token
179
+ ```
180
+
181
+ **Response:**
182
+ ```json
183
+ {
184
+ "token": "dev_89737a19376332bfd9a4a06db8b79fd1",
185
+ "type": "Bearer",
186
+ "user": {
187
+ "id": "test-user",
188
+ "email": "test@rem.local",
189
+ "name": "Test User"
190
+ },
191
+ "usage": "curl -H \"Authorization: Bearer dev_...\" http://localhost:8000/api/v1/...",
192
+ "warning": "This token is for development/testing only and will not work in production."
193
+ }
194
+ ```
195
+
196
+ **Use Token:**
197
+ ```bash
198
+ # Get the token
199
+ TOKEN=$(curl -s http://localhost:8000/api/dev/token | jq -r .token)
200
+
201
+ # Use it in requests
202
+ curl -H "Authorization: Bearer $TOKEN" \
203
+ -H "X-Tenant-Id: default" \
204
+ http://localhost:8000/api/v1/shared-with-me
205
+ ```
206
+
207
+ **Security Notes:**
208
+ - Only available when `ENVIRONMENT != "production"`
209
+ - Token is HMAC-signed using session secret
210
+ - Authenticates as `test-user` with `pro` tier and `admin` role
211
+ - Token is deterministic per environment (same secret = same token)
212
+
213
+ ### Anonymous Access
214
+
215
+ When `AUTH__ALLOW_ANONYMOUS=true` (default in development):
216
+ - Requests without authentication are allowed
217
+ - Anonymous users get rate-limited access
218
+ - MCP endpoints still require auth unless `AUTH__MCP_REQUIRES_AUTH=false`
219
+
161
220
  ## Usage Examples
162
221
 
163
- **Note on Authentication**: By default, authentication is disabled (`AUTH__ENABLED=false`) for local development and testing. The examples below work without an `Authorization` header. If authentication is enabled in your environment, add: `-H "Authorization: Bearer your_jwt_token"` to cURL requests or `"Authorization": "Bearer your_jwt_token"` to Python headers.
222
+ **Note on Authentication**: By default, authentication is disabled (`AUTH__ENABLED=false`) for local development and testing. The examples below work without an `Authorization` header. If authentication is enabled, use either:
223
+ - **Dev token**: `-H "Authorization: Bearer $(curl -s http://localhost:8000/api/dev/token | jq -r .token)"`
224
+ - **Session cookie**: Login via OAuth first, then use cookies
164
225
 
165
226
  ### cURL: Simple Chat
166
227
 
@@ -363,6 +424,157 @@ data: {"id":"chatcmpl-abc123","choices":[{"delta":{},"finish_reason":"stop","ind
363
424
  data: [DONE]
364
425
  ```
365
426
 
427
+ ## Extended SSE Event Protocol
428
+
429
+ REM uses OpenAI-compatible format for text content streaming, plus custom named SSE events for rich UI interactions.
430
+
431
+ ### Event Types
432
+
433
+ | Event Type | Format | Purpose | UI Display |
434
+ |------------|--------|---------|------------|
435
+ | (text content) | `data:` (OpenAI format) | Content chunks | Main response area |
436
+ | `reasoning` | `event:` | Model thinking | Collapsible "thinking" section |
437
+ | `progress` | `event:` | Step indicators | Progress bar/stepper |
438
+ | `tool_call` | `event:` | Tool invocations | Tool status panel |
439
+ | `action_request` | `event:` | User input solicitation | Buttons, forms, modals |
440
+ | `metadata` | `event:` | System info | Hidden or badge display |
441
+ | `error` | `event:` | Error notification | Error toast/alert |
442
+ | `done` | `event:` | Stream completion | Cleanup signal |
443
+
444
+ ### Event Format
445
+
446
+ **Text content (OpenAI-compatible `data:` format):**
447
+ ```
448
+ data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1732748123,"model":"gpt-4","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello "},"finish_reason":null}]}
449
+
450
+ data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1732748123,"model":"gpt-4","choices":[{"index":0,"delta":{"content":"world!"},"finish_reason":null}]}
451
+
452
+ data: {"id":"chatcmpl-abc123","object":"chat.completion.chunk","created":1732748123,"model":"gpt-4","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
453
+
454
+ data: [DONE]
455
+ ```
456
+
457
+ **Named events (use `event:` prefix):**
458
+ ```
459
+ event: reasoning
460
+ data: {"type": "reasoning", "content": "Analyzing the request...", "step": 1}
461
+
462
+ event: progress
463
+ data: {"type": "progress", "step": 1, "total_steps": 3, "label": "Searching", "status": "in_progress"}
464
+
465
+ event: tool_call
466
+ data: {"type": "tool_call", "tool_name": "search_rem", "status": "started", "arguments": {"query": "..."}}
467
+
468
+ event: action_request
469
+ data: {"type": "action_request", "card": {"id": "feedback-1", "prompt": "Was this helpful?", "actions": [...]}}
470
+
471
+ event: metadata
472
+ data: {"type": "metadata", "confidence": 0.95, "sources": ["doc1.md"], "hidden": false}
473
+
474
+ event: done
475
+ data: {"type": "done", "reason": "stop"}
476
+ ```
477
+
478
+ ### Action Request Cards (Adaptive Cards-inspired)
479
+
480
+ Action requests solicit user input using a schema inspired by [Microsoft Adaptive Cards](https://adaptivecards.io/):
481
+
482
+ ```json
483
+ {
484
+ "type": "action_request",
485
+ "card": {
486
+ "id": "confirm-delete-123",
487
+ "prompt": "Are you sure you want to delete this item?",
488
+ "display_style": "modal",
489
+ "actions": [
490
+ {
491
+ "type": "Action.Submit",
492
+ "id": "confirm",
493
+ "title": "Delete",
494
+ "style": "destructive",
495
+ "data": {"action": "delete", "item_id": "123"}
496
+ },
497
+ {
498
+ "type": "Action.Submit",
499
+ "id": "cancel",
500
+ "title": "Cancel",
501
+ "style": "secondary",
502
+ "data": {"action": "cancel"}
503
+ }
504
+ ],
505
+ "inputs": [
506
+ {
507
+ "type": "Input.Text",
508
+ "id": "reason",
509
+ "label": "Reason (optional)",
510
+ "placeholder": "Why are you deleting this?"
511
+ }
512
+ ],
513
+ "timeout_ms": 30000
514
+ }
515
+ }
516
+ ```
517
+
518
+ **Action Types:**
519
+ - `Action.Submit` - Send data to server
520
+ - `Action.OpenUrl` - Navigate to URL
521
+ - `Action.ShowCard` - Reveal nested content
522
+
523
+ **Input Types:**
524
+ - `Input.Text` - Text field (single or multiline)
525
+ - `Input.ChoiceSet` - Dropdown/radio selection
526
+ - `Input.Toggle` - Checkbox/toggle
527
+
528
+ ### SSE Simulator Endpoint
529
+
530
+ For frontend development and testing, use the simulator which generates all event types without LLM costs:
531
+
532
+ ```bash
533
+ curl -X POST http://localhost:8000/api/v1/chat/completions \
534
+ -H "Content-Type: application/json" \
535
+ -H "X-Agent-Schema: simulator" \
536
+ -d '{"messages": [{"role": "user", "content": "demo"}], "stream": true}'
537
+ ```
538
+
539
+ The simulator produces a scripted sequence demonstrating:
540
+ 1. Reasoning events (4 steps)
541
+ 2. Progress indicators
542
+ 3. Simulated tool calls
543
+ 4. Rich markdown content
544
+ 5. Metadata with confidence
545
+ 6. Action request for feedback
546
+
547
+ See `rem/agentic/agents/sse_simulator.py` for implementation details.
548
+
549
+ ### Frontend Integration
550
+
551
+ ```typescript
552
+ // Parse SSE events in React/TypeScript
553
+ const eventSource = new EventSource('/api/v1/chat/completions');
554
+
555
+ eventSource.onmessage = (e) => {
556
+ // Default handler for data-only events (text_delta)
557
+ const event = JSON.parse(e.data);
558
+ if (event.type === 'text_delta') {
559
+ appendContent(event.content);
560
+ }
561
+ };
562
+
563
+ eventSource.addEventListener('reasoning', (e) => {
564
+ const event = JSON.parse(e.data);
565
+ appendReasoning(event.content);
566
+ });
567
+
568
+ eventSource.addEventListener('action_request', (e) => {
569
+ const event = JSON.parse(e.data);
570
+ showActionCard(event.card);
571
+ });
572
+
573
+ eventSource.addEventListener('done', () => {
574
+ eventSource.close();
575
+ });
576
+ ```
577
+
366
578
  ## Architecture
367
579
 
368
580
  ### Middleware Ordering
@@ -437,6 +649,8 @@ When a user exceeds their rate limit (based on their tier), the API returns a 42
437
649
  ## Related Documentation
438
650
 
439
651
  - [Chat Router](routers/chat/completions.py) - Chat completions implementation
652
+ - [SSE Events](routers/chat/sse_events.py) - SSE event type definitions
653
+ - [SSE Simulator](../../agentic/agents/sse_simulator.py) - Event simulator for testing
440
654
  - [MCP Router](mcp_router/server.py) - MCP server implementation
441
655
  - [Agent Schemas](../../schemas/agents/) - Available agent schemas
442
656
  - [Session Compression](../../services/session/compression.py) - Compression implementation